Skip to content

[$175] Wallet - Going back from the international bank account success page lets the same account be submitted again #102213

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Action Performed:

  1. Go to Account > Wallet > Add bank account.
  2. Select a non-US country (e.g. United Kingdom) and press Next.
  3. Fill in every step with valid details.
  4. On the confirmation page, tick the terms and press Confirm.
  5. On the success page, press the browser Back button (web) or the device Back button (Android).
  6. Press Confirm again.

Expected Result:

The flow closes, or the success page stays. The same bank account cannot be submitted twice.

Actual Result:

The confirmation page for the account just added opens with every detail filled in and the terms ticked. Pressing Confirm submits the same account again.

Workaround:

Unknown.

Platforms:

  • Android: App (reported)
  • Web (reported)

Screenshots/Videos

bug.mov

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @situchan
Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 24, 2026
  2. applause-bot commented on Sep 24, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/922f8851f57ce4d2c9ce8cd61b187498bdd773ae0071eda7e7135f12c604739e

  3. MelvinBot commented on Sep 24, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The "IFSC Code is invalid" string is not an App string — it comes from Corpay via the backend and the App renders it verbatim.

    When you pick a bank country, the App calls GetCorpayBankAccountFields and stores the response in the corpayFields Onyx key. Each field carries validationRules[].errorMessage, already rendered in English by the provider. Client-side validation just passes it straight through:

    src/pages/settings/Wallet/InternationalDepositAccount/utils.ts:144-154 — only common.error.fieldRequired and the one SWIFT/BIC case added by Fix misleading SWIFT validation guidance for international bank accounts go through translate(). Every other rule, IFSC included, falls through to rule.errorMessage.

    Two things make it impossible for this string to be Spanish today:

    1. The App never sends the user's locale on that request — GetCorpayBankAccountFieldsParams.ts only carries countryISO, currency, isWithdrawal, isBusinessBankAccount. preferredLocale is sent on sign-in commands only.
    2. There is no IFSC translation key anywhere in src/languages/ — grep -i ifsc src/ returns nothing.

    So this is a systemic gap, not a one-off. The same Corpay strings are rendered raw in several other places, which is why the form already looks half-translated: the field label is English with a Spanish (opcional) suffix at subPages/BankAccountDetails.tsx:102, and dropdown option text and region names are raw too.

    What changes do you think we should make in order to solve the problem?

    The complete fix is backend-side and outside this repo: send the user's preferredLocale on GetCorpayBankAccountFields and have the backend return localized label, valueSet[].text, and validationRules[].errorMessage. That needs Web-Expensify/Auth changes (and confirmation that Corpay's upstream API accepts a locale), so it should be escalated to an internal engineer rather than solved here.

    An App-only mitigation is available following the precedent this team already accepted in #101772, if a full fix isn't wanted now:

    1. Add an addPersonalBankAccount.ifscFormatError key to all src/languages/*.ts files describing the real IFSC format (11 characters: 4 letters, then 0, then 6 alphanumerics).
    2. Generalize the hard-coded SWIFT branch in getValidationErrors into a small lookup keyed by bank country plus field name, so the IFSC rule for IN resolves to that key and SWIFT keeps its current behavior. values.bankCountry is already available inside that function, and scoping by country is required because routingCode is reused for UK sort codes, AU BSB, and others.
    3. Apply the same treatment to the two workspace (NonUSD) copies of this logic, which never received the Fix misleading SWIFT validation guidance for international bank accounts #101772 fix at all and still render rule.errorMessage raw with no special cases: NonUSD/BankInfo/subSteps/BankAccountDetails.tsx:73 and AccountHolderDetails.tsx:92. Best done by having both call the shared getValidationErrors instead of duplicating it.
    4. Update tests/unit/InternationalDepositAccountUtilsTest.ts:47-62, which currently asserts that non-SWIFT fields preserve the provider message.

    Be aware this mitigation only fixes the strings we enumerate. The English field label sitting right above the error stays English either way, so the screen still reads as partly untranslated.

    What alternative solutions did you explore? (Optional)

    • Map Corpay's English message text to translation keys. Rejected: keying on provider prose breaks the moment Corpay rewords anything, and there are ~100+ country/currency combinations.
    • Translate everything client-side with a full Corpay string table. Rejected: unbounded maintenance for labels, options, and region names across every supported country.
    Verification details and an unverified assumption

    Verified from code (App at 05d0f31ea045bcdf2f91b774033bfce870d31ba3):

    • corpayFields.formFields[] is written only by the server's onyxData; the client never populates it. Both entry points are BankAccounts.ts:1479 (fetchCorpayFields, used by this Wallet flow) and BankAccounts.ts:825.
    • Nothing localizes the string downstream: addErrorMessage concatenates it as-is and FormProvider renders it as errorText.
    • The IFSC field lands in the ACCOUNT_DETAILS bucket via utils.ts:34-37, which matches the reported screen.

    Not verified: I did not reproduce against the live Corpay response, so I don't know India's exact field id and regex. routingCode is the most likely id — India's IFSC is the routing code, and ReimbursementAccountForm.ts:104-106 notes it pairs with the SWIFT/BIC input. Whoever implements this should confirm the id and regex from an actual GetCorpayBankAccountFields response for countryISO=IN before wiring up the lookup.

    Alternative explanation considered and ruled out: that the message is the top-level field.errorMessage or a backend error surfaced on the Confirmation step. Neither fits — field.errorMessage is declared in the type but never read by any App code, and the reported repro shows the error inline on blur, which is the client-side getValidationErrors path.

    Related: Wallet- SWIFT code field validation error persist after enter valid code for International BA is the same underlying gap for the SWIFT field, fixed narrowly by #101772. No open duplicate of this IFSC report exists.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102213) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 24, 2026
  5. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @situchan (External)

  6. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  7. ashusatyarthi-wq commented on Sep 27, 2026

    @ashusatyarthi-wq

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When the user's language is set to Spanish and they navigate to Account > Wallet > Add International account for India, entering an invalid IFSC code (e.g. 2–4 characters) and blurring the field displays the error message "IFSC Code is invalid" in English instead of Spanish.


    What is the root cause of that problem?

    Dynamic form fields and validation rules for international bank accounts are returned by Corpay via the GetCorpayBankAccountFields API command.

    In src/pages/settings/Wallet/InternationalDepositAccount/utils.ts (lines 144–154), getValidationErrors() checks input values against each rule's regular expression (rule.regEx):

    for (const rule of field.validationRules) {
        const regExpCheck = new RegExp(rule.regEx);
        if (!regExpCheck.test(values[fieldName])) {
            // Corpay's strict SWIFT rule also requires a six-letter prefix, which its message may omit.
            const errorMessage =
                fieldName === INPUT_IDS.ADDITIONAL_DATA.CORPAY.SWIFT_BIC_CODE && rule.regEx === CONST.CORPAY_FIELDS.STRICT_SWIFT_BIC_REGEX
                    ? translate('addPersonalBankAccount.swiftBicFormatError')
                    : rule.errorMessage;
            addErrorMessage(errors, fieldName, errorMessage);
        }
    }

    For India (bankCountry === 'IN'), the IFSC field uses the routingCode field name. When the regex check fails, it falls through to rule.errorMessage. Because rule.errorMessage is supplied directly by Corpay in English ("IFSC Code is invalid"), the app renders the unlocalized English string regardless of the user's selected language.

    While PR #101772 solved this exact problem for swiftBicCode by introducing translate('addPersonalBankAccount.swiftBicFormatError'), no localized translation key or mapping was added for the Indian IFSC routing code validation.


    What changes do you think we should make in order to solve the problem?

    1. Add localized IFSC error messages to src/languages/en.ts and src/languages/es.ts:

    In src/languages/en.ts:

    addPersonalBankAccount: {
        swiftBicFormatError: 'SWIFT/BIC must be 8 or 11 characters long, with 6 letters followed by 2 or 5 letters or numbers.',
        ifscFormatError: 'IFSC Code is invalid.',

    In src/languages/es.ts:

    addPersonalBankAccount: {
        swiftBicFormatError: 'El SWIFT/BIC debe tener 8 u 11 caracteres, con 6 letras seguidas de 2 o 5 letras o números.',
        ifscFormatError: 'El código IFSC no es válido.',
    1. In src/pages/settings/Wallet/InternationalDepositAccount/utils.ts:
      Update getValidationErrors to translate the error when validating the Indian IFSC routing code:
    diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/utils.ts b/src/pages/settings/Wallet/InternationalDepositAccount/utils.ts
    --- a/src/pages/settings/Wallet/InternationalDepositAccount/utils.ts
    +++ b/src/pages/settings/Wallet/InternationalDepositAccount/utils.ts
    @@ -145,10 +145,13 @@ function getValidationErrors(values: FormOnyxValues<typeof ONYXKEYS.FORMS.INTER
             for (const rule of field.validationRules) {
                 const regExpCheck = new RegExp(rule.regEx);
                 if (!regExpCheck.test(values[fieldName])) {
    +                const isIndianIFSC = values.bankCountry === 'IN' && (fieldName === INPUT_IDS.ROUTING_CODE || fieldName === 'routingCode');
                     // Corpay's strict SWIFT rule also requires a six-letter prefix, which its message may omit.
                     const errorMessage =
                         fieldName === INPUT_IDS.ADDITIONAL_DATA.CORPAY.SWIFT_BIC_CODE && rule.regEx === CONST.CORPAY_FIELDS.STRICT_SWIFT_BIC_REGEX
                             ? translate('addPersonalBankAccount.swiftBicFormatError')
    +                        : isIndianIFSC
    +                        ? translate('addPersonalBankAccount.ifscFormatError')
                             : rule.errorMessage;
                     addErrorMessage(errors, fieldName, errorMessage);
                 }
    1. Update tests/unit/InternationalDepositAccountUtilsTest.ts:
      Add unit test cases verifying that when bankCountry === 'IN' and the routingCode fails validation, translateLocal('addPersonalBankAccount.ifscFormatError') is returned.

    What alternative solutions did you explore? (Optional)

    • Translating Corpay error messages on the backend: While passing preferredLocale upstream to Corpay or translating on the Web-Expensify backend would be ideal long-term, Corpay's upstream API is third-party and may not support localized validation errors. Handling known provider validation format errors client-side follows the exact architectural precedent established in PR Fix misleading SWIFT validation guidance for international bank accounts #101772 for swiftBicCode.
  8. added a commit that references this issue on Sep 27, 2026
    42f4e2e
  9. meanusarcanus commented on Sep 27, 2026

    @meanusarcanus

    Proposal

    What is the root cause of that problem?

    Dynamic form fields and validation rules for international bank accounts are returned from Corpay via the GetCorpayBankAccountFields API.

    In src/pages/settings/Wallet/InternationalDepositAccount/utils.ts (lines 144–154), getValidationErrors() iterates over each field's validationRules and evaluates input values against rule.regEx:

    • When input validation fails, it defaults to Corpay's raw provider message: rule.errorMessage.
    • For Indian bank accounts (bankCountry === 'IN'), the IFSC field uses the routingCode field, and Corpay provides the error message in English as "IFSC Code is invalid.".
    • Because rule.errorMessage is passed directly to addErrorMessage(errors, fieldName, errorMessage) without translation, the error message remains in English even when the app language is set to Spanish (or any non-English locale).

    Recently merged PR #101772 solved this exact problem for swiftBicCode by introducing a localized translation key (addPersonalBankAccount.swiftBicFormatError) and mapping Corpay's strict SWIFT validation failure in getValidationErrors(). However, no corresponding localized translation or mapping was added for the Indian IFSC routing code validation rule.

    What changes do you think we should make in order to solve the problem?

    Following the exact architectural pattern established in PR #101772:

    1. Add localized IFSC error messages across all language files in src/languages/:
      Add ifscFormatError under addPersonalBankAccount in src/languages/en.ts, src/languages/es.ts, and the remaining supported locale files (de.ts, fr.ts, it.ts, ja.ts, nl.ts, pl.ts, pt-BR.ts, zh-hans.ts, el.ts):

      • English (en.ts): 'IFSC Code is invalid.'
      • Spanish (es.ts): 'El código IFSC no es válido.'
    2. In src/pages/settings/Wallet/InternationalDepositAccount/utils.ts:
      In getValidationErrors(), check when the Indian routing code IFSC rule fails (e.g. fieldName === INPUT_IDS.ROUTING_CODE || fieldName === 'routingCode' for Indian bank accounts or matching the IFSC rule identifier) and map the message to translate('addPersonalBankAccount.ifscFormatError') instead of falling back to the raw English rule.errorMessage.

    3. Add unit test coverage in tests/unit/InternationalDepositAccountUtilsTest.ts:
      Add test cases verifying that when an Indian international deposit account routing code fails validation, translateLocal('addPersonalBankAccount.ifscFormatError') is returned in the errors object.

    What alternative solutions did you explore? (Optional)

    Contributor details
    Your Expensify account email: nadrested@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~01f9f6e0f44dbc3e49

  10. melvin-bot commented on Sep 27, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. 32 remaining items

  12. MelvinBot commented on Nov 12, 2026

    @MelvinBot
    Contributor

    Updated the issue title and description to track the duplicate-submission bug, including prerequisites, reproduction steps, expected/actual results, and the video.


    view run

  13. MelvinBot commented on Nov 12, 2026

    @MelvinBot
    Contributor

    ⚠️ Something went wrong. This may be a transient issue. Try again. If it persists, check status.openai.com.

    Run details

  14. situchan commented on Nov 12, 2026

    @situchan
    Contributor

    @MelvinBot submit new proposal

  15. MelvinBot commented on Nov 12, 2026

    @MelvinBot
    Contributor

    Investigation complete. The single-route approach has a precedent in the adjacent deposit-account flow, and the code indicates success can remain visible after its draft is cleared. No production logs are needed to establish the repeat client submission; whether the server creates a duplicate account remains unverified.

    The proposal will explicitly note that browser Back during an unfinished flow would exit it, while in-app and Android step navigation would remain available. No code changes or runtime tests were performed.

    Proposal

    What is the root cause of that problem?

    Successful submission does not make the Corpay bank-account flow terminal:

    • Success is pushed onto navigation history, leaving confirmation accessible.
    • The form draft—including accepted terms—is retained until Continue.
    • The success flag is immediately cleared, so returning to confirmation enables another submission.
    • Android Back explicitly moves to the preceding step, even from success.

    This explains a second API submission; it does not establish whether the server creates a duplicate account.

    Code evidence

    What changes do you think we should make in order to solve the problem?

    Use the existing single-route wizard pattern for this Corpay flow:

    1. Enable shouldReplaceRoute for its step navigation, including explicitly replacing the route when returning from an edit to confirmation. Completed form pages will no longer accumulate in history.
    2. Handle Android Back on success as finish/exit, never as a previous-step action.
    3. After confirmed API success, navigate to success and clear the bank-account draft and Corpay fields. Keep the existing finish cleanup as well.
    4. Preserve the draft on failure so users can correct details or retry.

    Navigation tradeoff: browser Back during an unfinished flow would exit rather than visit earlier steps. In-app step navigation would remain available. This matches the adjacent deposit-account flow but should be approved as part of this change.

    Existing precedent and regression coverage

    The adjacent flow already uses replacement navigation, explicit edit-return replacement, and terminal success Back handling: CollectDepositAccount/index.tsx:66–114. It also clears its draft after advancing on success: Confirmation.tsx:46–53.

    Add regression coverage for:

    • Browser Back and Android Back after success: no completed confirmation and no second creation request.
    • Edit/return and unfinished-step navigation.
    • Failure followed by retry: values retained, retry allowed.
    • Reopening after success without pressing Continue: no old details or accepted terms.
    • Existing Continue destinations.

    Verification: code-traced only; no runtime reproduction or fix verification performed.

    What alternative solutions did you explore? (Optional)

    • Replace only confirmation with success: earlier wizard pages remain reachable, and Android still explicitly navigates backward.
    • Only clear terms or disable Confirm while loading: neither prevents resubmission after completion.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102213) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  16. situchan commented on Nov 12, 2026

    @situchan
    Contributor

    @MelvinBot implement your proposal

    Alternative solution

    Summary

    Going back from the international bank account success page reopens the confirmation page of the account just added, with every detail filled in and the terms ticked. Pressing Confirm sends the same account again. This fix leaves the flow instead, following the pattern the US personal bank account flow already uses.

    Root cause

    Each sub page is pushed as its own screen (useSubPage), so the confirmation screen stays under the success screen.

    • Android Back on the success page runs prevPage() (InternationalDepositAccountContent). Browser Back and the iOS swipe pop the success screen.
    • The success flag is cleared as soon as the success page opens (Confirmation), so the confirmation page stays put when the user lands back on it.
    • The draft, including the ticked terms (Confirmation), is only cleared by Continue (handleFinishStep).

    Fix

    Same as the US personal bank account flow (AddPersonalBankAccountPage, from #102708):

    1. Back on the success page runs handleFinishStep, like Continue.
    2. The confirmation screen remembers it moved the flow on to success. If it comes back into focus after that, the user went back from success, so it leaves the flow.

    The sibling CollectDepositAccount flow also exits on Back from success, but it replaces routes, which is meant for dynamic routes. This flow is a static route that pushes, like the US flow.

    diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx
    index 2b4485eb215..f6aa46a0ec6 100644
    --- a/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx
    +++ b/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx
    @@ -29,8 +29,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
     
     import type {OnyxEntry} from 'react-native-onyx';
     
    -import {useRoute} from '@react-navigation/native';
    -import React from 'react';
    +import {useIsFocused, useRoute} from '@react-navigation/native';
    +import React, {useEffect, useEffectEvent, useRef} from 'react';
     
     import type CustomSubPageProps from './types';
     
    @@ -153,20 +153,53 @@ function InternationalDepositAccountContent({
                 return true;
             }
     
    +        // The account already exists by the success page, so going back to resubmit it makes no sense
    +        if (pageIndex === CONST.CORPAY_FIELDS.INDEXES.MAPPING.SUCCESS) {
    +            handleFinishStep();
    +            return true;
    +        }
    +
             prevPage();
             return true;
         };
     
         useAndroidBackButtonHandler(handleBackButtonPress);
     
    +    // Whether this page moved the flow on to the success page, and was out of focus since, like under the success page
    +    const hasMovedToSuccess = useRef(false);
    +    const wasUnfocusedAfterSuccess = useRef(false);
    +    const isFocused = useIsFocused();
    +    const leaveFlow = useEffectEvent(() => handleFinishStep());
    +
         const handleNextScreen = () => {
             if (isEditing) {
                 goBackToConfirmStep();
                 return;
             }
    +        if (pageIndex === CONST.CORPAY_FIELDS.INDEXES.MAPPING.CONFIRMATION) {
    +            hasMovedToSuccess.current = true;
    +        }
             nextPage();
         };
     
    +    // Focus coming back after that means the user went back from the success page.
    +    // The account is already added, so that leaves the flow, like Back on the success page.
    +    useEffect(() => {
    +        if (!hasMovedToSuccess.current) {
    +            return;
    +        }
    +        if (!isFocused) {
    +            wasUnfocusedAfterSuccess.current = true;
    +            return;
    +        }
    +        if (!wasUnfocusedAfterSuccess.current) {
    +            return;
    +        }
    +        wasUnfocusedAfterSuccess.current = false;
    +        hasMovedToSuccess.current = false;
    +        leaveFlow();
    +    }, [isFocused]);
    +
         return (
             <ScreenWrapper
                 shouldEnableMaxHeight

    What changes in production

    Only the paths that remove the success screen and leave the confirmation screen under it:

    • Browser Back, Android Back and the iOS swipe on the success page now leave the flow.
    • Continue and Escape normally close the whole RHP in one action, so they are unchanged. The one exception is below.
    • Editing a row from the confirmation page is unchanged. The marker is only set when moving on to success.

    One existing bug is touched. When the flow opens without backTo from a tab other than Settings or Inbox (e.g. pay invoice as individual, then Add bank account), Continue runs a bare Navigation.goBack() (goBack), which pops only the success screen. On main that lands on an empty confirmation page. With this fix it lands one step earlier. The flow doesn't close in either case. The US flow closes the RHP for those tabs.

    Tests

    1. Wise reimbursements beta off. Go to Account > Wallet > Add bank account.
    2. Select a non-US country and fill in every step.
    3. On the confirmation page, tick the terms and press Confirm.
    4. On the success page, press the browser Back button (web) or the device Back button (Android).

    Expected: the flow closes and the Wallet page shows the new account.
    Actual on main: the confirmation page of the account just added opens with the terms ticked, and Confirm sends it again.

  17. MelvinBot commented on Nov 12, 2026

    @MelvinBot
    Contributor

    🤖 Web test steps verification results: ❌ fail

    Browser verification was blocked by a blank app page; no bank-account actions were possible.

    Blank dev NewDot page blocking Wallet verification

    Step Status
    Open Account > Wallet > Add bank accountBoth snapshots returned zero nodes.
    Blank dev NewDot page blocking Wallet verification
    ❌
    Verify submission, Back, draft reset, and unfinished navigationNot tested because the app rendered no accessible content.
    ❌

    Implementation blocked; no PR created. Local changes are uncommitted. Five of six new regression tests passed; the retry test still expected two requests but recorded one after three debugging attempts. Required checks remain incomplete.


    view run · no recording available

  18. situchan commented on Nov 12, 2026

    @situchan
    Contributor

    @MelvinBot even if you're not able to test, do not block the implementation. Still create PR. I'll test and verify myself.

  19. MelvinBot commented on Nov 12, 2026

    @MelvinBot
    Contributor

    Created draft PR: Prevent resubmitting an international bank account after success, assigned to you.

    46 targeted tests and TypeScript passed. Incomplete checks are documented in the PR; manual verification is left to you.


    view run

  20. changed the title [-]Wallet - Going back from the international bank account success page lets the same account be submitted again[/-] [+][$175] Wallet - Going back from the international bank account success page lets the same account be submitted again[/+] on Nov 13, 2026
  21. melvin-bot commented on Nov 13, 2026

    @melvin-bot
  22. melvin-bot commented on Nov 13, 2026

    @melvin-bot

    Triggered auto assignment to @Julesssss, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions