Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "feat: Connect USD and nonUSD VBA flows" #57422

Merged
merged 1 commit into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,14 +681,6 @@ const CONST = {
AGREEMENTS: 'AgreementsStep',
FINISH: 'FinishStep',
},
BANK_INFO_STEP_ACH_DATA_INPUT_IDS: {
ACCOUNT_HOLDER_NAME: 'addressName',
ACCOUNT_HOLDER_REGION: 'addressState',
ACCOUNT_HOLDER_CITY: 'addressCity',
ACCOUNT_HOLDER_ADDRESS: 'addressStreet',
ACCOUNT_HOLDER_POSTAL_CODE: 'addressZipCode',
ROUTING_CODE: 'routingNumber',
},
BUSINESS_INFO_STEP: {
PICKLIST: {
ANNUAL_VOLUME_RANGE: 'AnnualVolumeRange',
Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type TimeModalPicker from '@components/TimeModalPicker';
import type UploadFile from '@components/UploadFile';
import type ValuePicker from '@components/ValuePicker';
import type ConstantSelector from '@pages/Debug/ConstantSelector';
import type BusinessTypePicker from '@pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/BusinessTypePicker';
import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker';
import type DimensionTypeSelector from '@pages/workspace/accounting/intacct/import/DimensionTypeSelector';
import type NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker';
Expand Down
10 changes: 5 additions & 5 deletions src/components/SubStepForms/AddressStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import useLocalize from '@hooks/useLocalize';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useThemeStyles from '@hooks/useThemeStyles';
import {getFieldRequiredErrors, isValidAddress, isValidZipCode, isValidZipCodeInternational} from '@libs/ValidationUtils';
import * as ValidationUtils from '@libs/ValidationUtils';

Check failure on line 9 in src/components/SubStepForms/AddressStep.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields';
import HelpLinks from '@pages/ReimbursementAccount/USD/Requestor/PersonalInfo/HelpLinks';
import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
import type {OnyxFormValuesMapping} from '@src/ONYXKEYS';

type AddressValues = {
Expand Down Expand Up @@ -95,16 +95,16 @@

const validate = useCallback(
(values: FormOnyxValues<TFormID>): FormInputErrors<TFormID> => {
const errors = getFieldRequiredErrors(values, stepFields);
const errors = ValidationUtils.getFieldRequiredErrors(values, stepFields);

const street = values[inputFieldsIDs.street as keyof typeof values];
if (street && !isValidAddress(street as FormValue)) {
if (street && !ValidationUtils.isValidAddress(street as FormValue)) {
// @ts-expect-error type mismatch to be fixed
errors[inputFieldsIDs.street] = translate('bankAccount.error.addressStreet');
}

const zipCode = values[inputFieldsIDs.zipCode as keyof typeof values];
if (zipCode && (shouldDisplayCountrySelector ? !isValidZipCodeInternational(zipCode as string) : !isValidZipCode(zipCode as string))) {
if (zipCode && (shouldDisplayCountrySelector ? !ValidationUtils.isValidZipCodeInternational(zipCode as string) : !ValidationUtils.isValidZipCode(zipCode as string))) {
// @ts-expect-error type mismatch to be fixed
errors[inputFieldsIDs.zipCode] = translate('bankAccount.error.zipCode');
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/SubStepForms/FullNameStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useThemeStyles from '@hooks/useThemeStyles';
import {getFieldRequiredErrors, isRequiredFulfilled, isValidLegalName} from '@libs/ValidationUtils';
import HelpLinks from '@pages/ReimbursementAccount/USD/Requestor/PersonalInfo/HelpLinks';
import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
import CONST from '@src/CONST';
import type {OnyxFormValuesMapping} from '@src/ONYXKEYS';

Expand Down
2 changes: 1 addition & 1 deletion src/components/SubStepForms/SingleFieldStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useThemeStyles from '@hooks/useThemeStyles';
import HelpLinks from '@pages/ReimbursementAccount/USD/Requestor/PersonalInfo/HelpLinks';
import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
import CONST from '@src/CONST';
import type {OnyxFormValuesMapping} from '@src/ONYXKEYS';

Expand Down
2 changes: 1 addition & 1 deletion src/libs/API/parameters/RestartBankAccountSetupParams.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type RestartBankAccountSetupParams = {
bankAccountID: number;
ownerEmail: string;
policyID: string | undefined;
policyID: string;
};

export default RestartBankAccountSetupParams;
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Onyx.connect({
/**
* Reset user's reimbursement account. This will delete the bank account.
*/
function resetFreePlanBankAccount(bankAccountID: number | undefined, session: OnyxEntry<OnyxTypes.Session>, policyID: string | undefined) {
function resetFreePlanBankAccount(bankAccountID: number | undefined, session: OnyxEntry<OnyxTypes.Session>, policyID: string) {
if (!bankAccountID) {
throw new Error('Missing bankAccountID when attempting to reset free plan bank account');
}
Expand Down
15 changes: 15 additions & 0 deletions src/pages/ReimbursementAccount/ACHContractStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import CompleteVerification from './CompleteVerification/CompleteVerification';

type ACHContractStepProps = {
/** Goes to the previous step */
onBackButtonPress: () => void;
};

function ACHContractStep({onBackButtonPress}: ACHContractStepProps) {
return <CompleteVerification onBackButtonPress={onBackButtonPress} />;
}

ACHContractStep.displayName = 'ACHContractStep';

export default ACHContractStep;
1 change: 0 additions & 1 deletion src/pages/ReimbursementAccount/AddressFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ function AddressFormFields({
modalHeaderTitle={translate('countryStep.selectCountry')}
searchInputTitle={translate('countryStep.findCountry')}
value={values?.country}
defaultValue={defaultValues?.country}
onValueChange={handleCountryChange}
stateInputIDToReset={inputKeys.state ?? 'stateInput'}
/>
Expand Down
251 changes: 251 additions & 0 deletions src/pages/ReimbursementAccount/BankAccountStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import React, {useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import LottieAnimations from '@components/LottieAnimations';
import MenuItem from '@components/MenuItem';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getEarliestErrorField, getLatestErrorField} from '@libs/ErrorUtils';
import getPlaidDesktopMessage from '@libs/getPlaidDesktopMessage';
import {REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils';
import {openPlaidView, setBankAccountSubStep} from '@userActions/BankAccounts';
import {openExternalLink, openExternalLinkWithToken} from '@userActions/Link';
import {updateReimbursementAccountDraft} from '@userActions/ReimbursementAccount';
import {clearContactMethodErrors, requestValidateCodeAction, validateSecondaryLogin} from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import BankInfo from './BankInfo/BankInfo';

type BankAccountStepProps = {
/** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */
receivedRedirectURI?: string | null;

/** During the OAuth flow we need to use the plaidLink token that we initially connected with */
plaidLinkOAuthToken?: OnyxEntry<string>;

/* The workspace name */
policyName?: string;

/* The workspace ID */
policyID?: string;

/** The bank account currently in setup */
reimbursementAccount: OnyxEntry<OnyxTypes.ReimbursementAccount>;

/** Goes to the previous step */
onBackButtonPress: () => void;

/** Should ValidateCodeActionModal be displayed or not */
isValidateCodeActionModalVisible?: boolean;

/** Toggle ValidateCodeActionModal */
toggleValidateCodeActionModal?: (isVisible: boolean) => void;
};

const bankInfoStepKeys = INPUT_IDS.BANK_INFO_STEP;

function BankAccountStep({
plaidLinkOAuthToken = '',
policyID = '',
policyName = '',
receivedRedirectURI,
reimbursementAccount,
onBackButtonPress,
isValidateCodeActionModalVisible,
toggleValidateCodeActionModal,
}: BankAccountStepProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED);
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const contactMethod = account?.primaryLogin ?? '';
const selectedSubStep = useRef('');

const loginData = useMemo(() => loginList?.[contactMethod], [loginList, contactMethod]);
const validateLoginError = getEarliestErrorField(loginData, 'validateLogin');
const hasMagicCodeBeenSent = !!loginData?.validateCodeSent;

let subStep = reimbursementAccount?.achData?.subStep ?? '';
const shouldReinitializePlaidLink = plaidLinkOAuthToken && receivedRedirectURI && subStep !== CONST.BANK_ACCOUNT.SUBSTEP.MANUAL;
if (shouldReinitializePlaidLink) {
subStep = CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID;
}
const plaidDesktopMessage = getPlaidDesktopMessage();
const bankAccountRoute = `${ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_INITIAL.getRoute(policyID))}`;
const personalBankAccounts = bankAccountList ? Object.keys(bankAccountList).filter((key) => bankAccountList[key].accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) : [];

useEffect(() => {
if (!account?.validated) {
return;
}

if (selectedSubStep.current === CONST.BANK_ACCOUNT.SUBSTEP.MANUAL) {
setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL);
} else if (selectedSubStep.current === CONST.BANK_ACCOUNT.SUBSTEP.PLAID) {
openPlaidView();
}
}, [account?.validated]);

const removeExistingBankAccountDetails = () => {
const bankAccountData: Partial<ReimbursementAccountForm> = {
[bankInfoStepKeys.ROUTING_NUMBER]: '',
[bankInfoStepKeys.ACCOUNT_NUMBER]: '',
[bankInfoStepKeys.PLAID_MASK]: '',
[bankInfoStepKeys.IS_SAVINGS]: undefined,
[bankInfoStepKeys.BANK_NAME]: '',
[bankInfoStepKeys.PLAID_ACCOUNT_ID]: '',
[bankInfoStepKeys.PLAID_ACCESS_TOKEN]: '',
};
updateReimbursementAccountDraft(bankAccountData);
};

if (subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID || subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) {
return (
<BankInfo
onBackButtonPress={onBackButtonPress}
policyID={policyID}
/>
);
}

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={BankAccountStep.displayName}
>
<View style={[styles.flex1, styles.justifyContentBetween]}>
<HeaderWithBackButton
title={translate('workspace.common.connectBankAccount')}
subtitle={policyName}
stepCounter={subStep ? {step: 1, total: 5} : undefined}
onBackButtonPress={onBackButtonPress}
shouldShowGetAssistanceButton
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT}
/>
<ScrollView style={styles.flex1}>
<Section
title={translate('workspace.bankAccount.streamlinePayments')}
illustration={LottieAnimations.FastMoney}
subtitle={translate('bankAccount.toGetStarted')}
subtitleMuted
illustrationBackgroundColor={theme.fallbackIconColor}
isCentralPane
>
{!!plaidDesktopMessage && (
<View style={[styles.mt3, styles.flexRow, styles.justifyContentBetween]}>
<TextLink onPress={() => openExternalLinkWithToken(bankAccountRoute)}>{translate(plaidDesktopMessage)}</TextLink>
</View>
)}
{!!personalBankAccounts.length && (
<View style={[styles.flexRow, styles.mt4, styles.alignItemsCenter, styles.pb1, styles.pt1]}>
<Icon
src={Expensicons.Lightbulb}
fill={theme.icon}
additionalStyles={styles.mr2}
medium
/>
<Text
style={[styles.textLabelSupportingNormal, styles.flex1]}
suppressHighlighting
>
{translate('workspace.bankAccount.connectBankAccountNote')}
</Text>
</View>
)}
<View style={styles.mt3}>
<MenuItem
icon={Expensicons.Bank}
title={translate('bankAccount.connectOnlineWithPlaid')}
disabled={!!isPlaidDisabled}
onPress={() => {
if (isPlaidDisabled) {
return;
}
if (!account?.validated) {
selectedSubStep.current = CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID;
toggleValidateCodeActionModal?.(true);
return;
}
removeExistingBankAccountDetails();
openPlaidView();
}}
shouldShowRightIcon
wrapperStyle={[styles.sectionMenuItemTopDescription]}
/>
</View>
<View style={styles.mb3}>
<MenuItem
icon={Expensicons.Connect}
title={translate('bankAccount.connectManually')}
onPress={() => {
if (!account?.validated) {
selectedSubStep.current = CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL;
toggleValidateCodeActionModal?.(true);
return;
}
removeExistingBankAccountDetails();
setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL);
}}
shouldShowRightIcon
wrapperStyle={[styles.sectionMenuItemTopDescription]}
/>
</View>
</Section>
<View style={[styles.mv0, styles.mh5, styles.flexRow, styles.justifyContentBetween]}>
<TextLink href={CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}>{translate('common.privacy')}</TextLink>
<PressableWithoutFeedback
onPress={() => openExternalLink(CONST.ENCRYPTION_AND_SECURITY_HELP_URL)}
style={[styles.flexRow, styles.alignItemsCenter]}
accessibilityLabel={translate('bankAccount.yourDataIsSecure')}
>
<TextLink href={CONST.ENCRYPTION_AND_SECURITY_HELP_URL}>{translate('bankAccount.yourDataIsSecure')}</TextLink>
<View style={styles.ml1}>
<Icon
src={Expensicons.Lock}
fill={theme.link}
/>
</View>
</PressableWithoutFeedback>
</View>
</ScrollView>
<ValidateCodeActionModal
title={translate('contacts.validateAccount')}
descriptionPrimary={translate('contacts.featureRequiresValidate')}
descriptionSecondary={translate('contacts.enterMagicCode', {contactMethod})}
isVisible={!!isValidateCodeActionModalVisible}
hasMagicCodeBeenSent={hasMagicCodeBeenSent}
validatePendingAction={loginData?.pendingFields?.validateCodeSent}
sendValidateCode={() => requestValidateCodeAction()}
handleSubmitForm={(validateCode) => validateSecondaryLogin(loginList, contactMethod, validateCode)}
validateError={!isEmptyObject(validateLoginError) ? validateLoginError : getLatestErrorField(loginData, 'validateCodeSent')}
clearError={() => clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')}
onClose={() => toggleValidateCodeActionModal?.(false)}
/>
</View>
</ScreenWrapper>
);
}

BankAccountStep.displayName = 'BankAccountStep';

export default BankAccountStep;
Loading
Loading