Skip to content

[$250] Travel - “Something went wrong” error when completing Travel setup #91402

Closed

Description

@jponikarchuk

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 9.3.79-1
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://test-management.browserstack.com/projects/2219752/test-runs/TR-32100/folder/13177079/104213728/1432273593
Email or phone of affected tester (no customers): applausetester+th022@applause.expensifail.com
Issue reported by: Applause Internal Team
Bug source: Regression TC Execution
Device used: Windows 11
App Component: Other

Action Performed:

1.Open a workspace with Travel enabled
2.Navigate to Workspace settings
3.Open the Travel setup flow
4.Enter the required Travel setup information
.5Click Continue

Expected Result:

The Travel setup completes successfully and the user proceeds to the next Travel configuration step.

Actual Result:

A “Something went wrong, please try again later” error is displayed after clicking Continue, preventing completion of the Travel setup flow.

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Video.mp4

View all open jobs on GitHub

Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on May 22, 2026
  2. MelvinBot commented on May 22, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The "Something went wrong. Please try again later." error is a generic catch-all displayed when the AcceptSpotnanaTerms API call fails. The frontend error-handling path is:

    1. User clicks Continue on the Travel terms page (DynamicTravelTerms.tsx:82)
    2. This calls acceptSpotnanaTerms(domain, policyID) which sends the AcceptSpotnanaTerms API request
    3. When the API returns a non-200 response (or a network error), the failureData fires, setting TRAVEL_PROVISIONING.errors to the travel.errorMessage translation key
    4. DynamicTravelTerms.tsx:58 reads this and displays the error

    The frontend code itself is correct — it properly surfaces backend failures. This appears to be a backend/internal issue. A separate internal investigation is needed to determine why the AcceptSpotnanaTerms endpoint is returning errors (e.g., Spotnana provisioning failure, authentication issue, or a recent backend change).

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

    The backend AcceptSpotnanaTerms handler needs investigation. The frontend error path is working as designed — it correctly shows the generic error when the API call fails. The fix will be on the backend side, depending on what the server-side logs reveal about why provisioning is failing.

    If the backend is returning a specific error that could be surfaced more helpfully (rather than the generic "Something went wrong"), the frontend could be updated to handle additional error codes in the .then() handler at DynamicTravelTerms.tsx:84-137, similar to how permissionDenied and additionalVerificationRequired are already handled.

    What alternative solutions did you explore? (Optional)

    Reviewed recent frontend changes for potential regressions:

    None of these frontend changes explain the error — the API parameters are correctly constructed and sent. The root cause is the backend returning a failure response.


    Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on May 22, 2026
  4. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on May 22, 2026
  5. MobileMage commented on May 22, 2026

    @MobileMage
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-05-22 09:24:31 UTC.

    Proposal

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

    After completing the Travel setup (entering the company address and continuing to the Terms screen), the user sees "Something went wrong, please try again later" even though provisioning should succeed, blocking them from completing Travel setup.

    What is the root cause of that problem?

    The TRAVEL_PROVISIONING Onyx key persists across Travel setup attempts and WorkspaceAddressForTravelPage does not clear it before navigating forward. If the user previously hit any error during a Travel setup attempt (timed-out OTP, an AcceptSpotnanaTerms retry, navigating away mid-flow, etc.), failureData from acceptSpotnanaTerms left TRAVEL_PROVISIONING.errors populated with the travel.errorMessage translation key ("Something went wrong, please try again later."). When the user re-enters the flow, fills the address, and the page navigates to TCS with forceReplace: true, the stale errors is still in Onyx. DynamicTravelTerms then immediately renders that stale error via FormAlertWithSubmitButton because of the selector travelProvisioning?.errors && !travelProvisioning?.error — so the user sees the failure message before any new API call has even been made.

    BookTravelButton.navigateToAcceptTerms and DynamicDomainSelectorPage.provisionTravelForDomain both call cleanupTravelProvisioningSession() before navigating to TCS exactly to avoid this, but the address-completion path skips it:

    updateAddress(policy?.id, {
    addressStreet: values.addressLine1?.trim() ?? '',
    addressStreet2: values.addressLine2?.trim() ?? '',
    city: values.city.trim(),
    state: values.state.trim(),
    zipCode: values?.zipPostCode?.trim().toUpperCase() ?? '',
    country: values.country,
    });
    Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TRAVEL_TCS.getRoute(route.params.domain ?? CONST.TRAVEL.DEFAULT_DOMAIN, policyID)), {forceReplace: true});

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

    Call cleanupTravelProvisioningSession() inside updatePolicyAddress in WorkspaceAddressForTravelPage.tsx just before navigating to the TCS route, mirroring what the sibling entry points (BookTravelButton.navigateToAcceptTerms and DynamicDomainSelectorPage) already do. This ensures any stale errors/error/isLoading state from a prior provisioning attempt is wiped before the user lands on the Terms screen, so the screen starts in a clean state and the "Something went wrong" message only appears if the new AcceptSpotnanaTerms call actually fails.

    const navigateToAcceptTerms = (domain: string, isUserValidated?: boolean, policyID?: string) => {
    // Remove the previous provision session information if any is cached.
    cleanupTravelProvisioningSession();
    if (isUserValidated) {
    Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TRAVEL_TCS.getRoute(domain, policyID)));
    return;
    }
    Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain, policyID, Navigation.getActiveRoute()));
    };

    What alternative solutions did you explore? (Optional)

    N/A

  6. yusufdeveloper2903 commented on May 22, 2026

    @yusufdeveloper2903
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-06-18 14:40:35 UTC.

    Proposal

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

    When an admin completes the workspace Travel setup flow — enters the company address, accepts the Terms & Conditions, and clicks Continue — the app shows the generic "Something went wrong, please try again later." and provisioning never completes. Per the maintainer's backend logs, the underlying server failure is INVALID_USER_GROUP_MEMBER_UPDATE.

    What is the root cause of that problem?

    There are two layers, and they must not be conflated:

    Layer 1 — the visible string is a faithful, but lossy, surfacing of a backend failure. "Something went wrong, please try again later." is the travel.errorMessage key written by the failureData of acceptSpotnanaTerms into TRAVEL_PROVISIONING.errors:

    const failureData: Array<OnyxUpdate<typeof ONYXKEYS.NVP_TRAVEL_SETTINGS | typeof ONYXKEYS.TRAVEL_PROVISIONING>> = [
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.NVP_TRAVEL_SETTINGS,
    value: {
    hasAcceptedTerms: false,
    },
    },
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.TRAVEL_PROVISIONING,
    value: {
    isLoading: false,
    errors: getMicroSecondOnyxErrorWithTranslationKey('travel.errorMessage'),
    },
    },
    ];

    DynamicTravelTerms reads it back and renders it in the FormAlertWithSubmitButton; the generic jsonCode !== 200 branch rejects without surfacing the server's real message, and the trailing .catch(() => {}) discards the rejection entirely:

    // Handle general API failure
    if (response?.jsonCode !== 200) {
    return Promise.reject(new Error('Request failed'));
    }
    // Handle success - open travel in-app webview on native, or in browser on web
    if (response?.spotnanaToken) {
    Navigation.closeRHPFlow();
    cleanupTravelProvisioningSession();
    openTravelDotLink(policyID, undefined, response.spotnanaToken, response.isTestAccount ?? false);
    return;
    }
    return Promise.reject(new Error('No token received'));
    })
    .catch(() => {
    // Errors are surfaced via TRAVEL_PROVISIONING in Onyx
    });

    So every non-200 from AcceptSpotnanaTerms that isn't permissionDenied / additionalVerificationRequired collapses into the same opaque string — which is exactly why this regression has been hard to action (the C+ reviewer and maintainer both had to fall back to reading raw BE logs).

    Layer 2 — a frontend ordering race feeds incomplete state into provisioning. For a workspace with no persisted policy.address, BookTravelButton routes to the address screen because Spotnana requires an address to create the policy entity:

    prompt: translate('travel.verifyCompany.message'),
    promptStyles: styles.mb2,
    confirmText: translate('common.buttonConfirm'),
    shouldShowCancelButton: false,
    image: illustrations.RocketDude,
    imageStyles: StyleUtils.getBackgroundColorStyle(colors.ice600),
    });
    }
    if (!travelSettings?.lastTravelSignupRequestTime) {
    requestTravelAccess();
    }
    }
    // Determine the domain to associate with the workspace during provisioning in Spotnana.
    // - If all admins share the same private domain, the workspace is tied to it automatically.
    // - If admins have multiple private domains, the user must select one.
    // - Public domains are not allowed; an error page is shown in that case.
    else if (adminDomains.length === 1) {

    WorkspaceAddressForTravelPage calls updateAddress(...) and synchronously replaces the route with the TCS screen — no await, no pending-state observation, no cleanupTravelProvisioningSession():

    updateAddress(policy?.id, {
    addressStreet: values.addressLine1?.trim() ?? '',
    addressStreet2: values.addressLine2?.trim() ?? '',
    city: values.city.trim(),
    state: values.state.trim(),
    zipCode: values?.zipPostCode?.trim().toUpperCase() ?? '',
    country: values.country,
    });
    Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TRAVEL_TCS.getRoute(route.params.domain ?? CONST.TRAVEL.DEFAULT_DOMAIN, policyID)), {forceReplace: true});
    };

    updateAddress issues UPDATE_POLICY_ADDRESS via API.write, a sequential-queue command whose Promise resolves on enqueue, not on server completion. Meanwhile acceptSpotnanaTerms fires ACCEPT_SPOTNANA_TERMS via API.makeRequestWithSideEffects, which bypasses the queue and hits the server immediately:

    const params: AcceptSpotnanaTermsParams = {domainName: domain, policyID};
    // We need to call this API immediately to get the response and open the travel page.
    // See https://github.com/Expensify/App/pull/69769#discussion_r2368967354 for more info.
    // eslint-disable-next-line rulesdir/no-api-side-effects-method
    return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS, params, {optimisticData, successData, failureData});

    The optimistic address + pendingFields.address: UPDATE write makes the address look set on the client, but the server may not have persisted it yet when ACCEPT_SPOTNANA_TERMS arrives:

    }
    function updateAddress(policyID: string, newAddress: CompanyAddress) {
    const parameters: UpdatePolicyAddressParams = {
    policyID,
    addressStreet: newAddress.addressStreet,
    addressStreet2: newAddress.addressStreet2 ?? '',
    city: newAddress.city,
    country: newAddress.country,
    state: newAddress.state,
    zipCode: newAddress.zipCode,
    };
    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
    {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
    value: {
    address: newAddress,
    pendingFields: {
    address: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
    },
    },
    },
    ];

    The "Windows/Chrome only" repro is consistent with a timing-sensitive race — a fast machine reaches Continue before UPDATE_POLICY_ADDRESS drains.

    Bridging Layer 2 to the observed INVALID_USER_GROUP_MEMBER_UPDATE (hypothesis, needs BE correlation). Spotnana provisioning creates the company/domain entity and then attaches the admin to that entity's user group. If ACCEPT_SPOTNANA_TERMS runs before the address is persisted, the entity is created without a valid address (or not fully created), so the downstream "add admin to the entity's user group" step operates against an incomplete/invalid group → INVALID_USER_GROUP_MEMBER_UPDATE. This plausibly makes the user-group error a downstream symptom of the same race, not an unrelated bug. I want to be explicit that this is a hypothesis: rlinoz read the logs and said "not sure why yet," so the bridge must be confirmed by correlating the BE error timestamp against whether UPDATE_POLICY_ADDRESS had committed first. If the BE error reproduces even when the address is provably persisted first, then it is an independent backend defect and Layer 2's FE fix is purely a robustness improvement.

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

    1. (FE) Remove the race at the only screen that introduces it — scoped, minimum blast radius. In WorkspaceAddressForTravelPage, do not navigate to TCS synchronously after updateAddress(...). Instead set a local "pending submit" flag and observe policy.pendingFields?.address: updateAddress already toggles it to UPDATE in optimisticData and clears it to null in finallyData (Policy.ts#L2229-L2253). When it flips back to null, the write has returned from the server — only then navigate to TCS. Show the address submit button's standard isLoading spinner while waiting, and call cleanupTravelProvisioningSession() before navigating (mirroring the canonical entry point BookTravelButton.navigateToAcceptTerms) so a stale TRAVEL_PROVISIONING.errors can't re-render on TCS mount.

    This keeps updateAddress's API.write semantics intact for every other caller (Workspace Profile, NetSuite onboarding, offline edits) and keeps acceptSpotnanaTerms as a side-effect request (the caller still needs its response to open Travel).

    2. (FE) Make the failure diagnosable so this never becomes an opaque "Something went wrong" again — worth doing regardless of Layer 2's outcome. In the generic non-200 branch of acceptTermsAndOpenTravelDot, surface the backend's real message and stop swallowing the rejection:

                     // Handle general API failure
                     if (response?.jsonCode !== 200) {
    +                    Log.warn('[Travel] AcceptSpotnanaTerms failed', {jsonCode: response?.jsonCode, message: response?.message});
    +                    if (response?.message) {
    +                        Onyx.merge(ONYXKEYS.TRAVEL_PROVISIONING, {errors: getMicroSecondOnyxErrorWithMessage(response.message)});
    +                    }
                         return Promise.reject(new Error('Request failed'));
                     }
    -            .catch(() => {
    -                // Errors are surfaced via TRAVEL_PROVISIONING in Onyx
    -            });
    +            .catch((error) => {
    +                Log.warn('[Travel] acceptTermsAndOpenTravelDot rejected', {message: error?.message});
    +            });

    3. (BE) Investigate INVALID_USER_GROUP_MEMBER_UPDATE directly. The maintainer's log is the authoritative signal here. The BE owner should confirm whether the error disappears once the policy address is committed before ACCEPT_SPOTNANA_TERMS (validating the bridge), and whether the provisioning step should be idempotent / send the address alongside the terms-accept as a fallback (@shubham1206agra's suggestion). Step 1 likely removes the FE-triggerable path; step 3 closes it for any non-FE ordering.

    Tests:

    • Unit test on WorkspaceAddressForTravelPage: submitting the address sets the pending state and does not navigate to TCS until policy.pendingFields.address clears to null.
    • Flow test for a Travel-enabled workspace with no persisted address: submit address → assert navigation to TCS only after the address write settles, and that cleanupTravelProvisioningSession() ran.

    What alternative solutions did you explore? (Optional)

    • API.waitForWrites(ACCEPT_SPOTNANA_TERMS) inside acceptSpotnanaTerms (@KJ21-ENG/@MobileMage). Correct diagnosis of the race, but too coarse: waitForWrites drains the entire sequential queue (chat messages, unrelated policy edits), so Continue hangs on anything unrelated that is retrying, and the penalty hits every call site of acceptSpotnanaTerms including BookTravelButton.navigateToAcceptTerms where there is no address write to wait on. Observing pendingFields.address ties the wait to the one write that matters.

    • await the updateAddress Promise before navigating (@abbasifaizan70). Does not fix the race — API.write resolves on enqueue, not server completion, so ACCEPT_SPOTNANA_TERMS still fires before UPDATE_POLICY_ADDRESS commits. Making it actually wait would require converting UPDATE_POLICY_ADDRESS to a side-effect request, regressing offline persistence for every other caller.

    • Convert updateAddress itself to a side-effect request. Same regression — it changes the semantics of a normal persisted policy write for all surfaces, just to fix one setup flow.

    • Treat this as purely a backend bug and do nothing on FE. Rejected: even if the BE handler is fully fixed, the synchronous navigate-before-write-settles pattern remains a latent ordering hazard, and the opaque generic error keeps future Travel failures undebuggable. Steps 1–2 are defensible on their own; step 3 is the actual provisioning fix.

  7. KJ21-ENG commented on May 22, 2026

    @KJ21-ENG
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-05-22 10:23:47 UTC.

    Replaces my earlier proposal: I originally focused on the permission-info dynamic route. That only affects the ERROR_PERMISSION_DENIED branch; the failing setup path here is the address-write/terms-accept race below.

    Proposal

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

    Completing the workspace Travel setup flow can show "Something went wrong, please try again later" after the admin enters the required company address and clicks Continue on the Travel terms step. That blocks the workspace from finishing Travel provisioning.

    What is the root cause of that problem?

    The address step can navigate to the terms step before the required company address has actually been saved on the backend.

    For an eligible workspace with a single private admin domain and no policy address, BookTravelButton sends the user to the workspace address screen because Spotnana requires an address when creating the policy entity:

    else if (adminDomains.length === 1) {
    const domain = adminDomains.at(0) ?? CONST.TRAVEL.DEFAULT_DOMAIN;
    // Always validate OTP first before proceeding to address details or terms acceptance
    if (!isUserValidated) {
    // Determine where to redirect after OTP validation
    const nextStep = isEmptyObject(policy?.address)
    ? ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, activePolicyID, Navigation.getActiveRoute())
    : createDynamicRoute(DYNAMIC_ROUTES.TRAVEL_TCS.getRoute(domain, activePolicyID));
    setTravelProvisioningNextStep(nextStep);
    Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain, activePolicyID, Navigation.getActiveRoute()));
    return;
    }
    if (isEmptyObject(policy?.address)) {
    // Spotnana requires an address anytime an entity is created for a policy
    Navigation.navigate(ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, activePolicyID, Navigation.getActiveRoute()));
    } else {
    navigateToAcceptTerms(domain, !!isUserValidated, activePolicyID ?? undefined);

    WorkspaceAddressForTravelPage then calls updateAddress(...) and immediately replaces the route with the Travel terms screen:

    updateAddress(policy?.id, {
    addressStreet: values.addressLine1?.trim() ?? '',
    addressStreet2: values.addressLine2?.trim() ?? '',
    city: values.city.trim(),
    state: values.state.trim(),
    zipCode: values?.zipPostCode?.trim().toUpperCase() ?? '',
    country: values.country,
    });
    Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TRAVEL_TCS.getRoute(route.params.domain ?? CONST.TRAVEL.DEFAULT_DOMAIN, policyID)), {forceReplace: true});

    That updateAddress action is a queued write request:

    function updateAddress(policyID: string, newAddress: CompanyAddress) {
    const parameters: UpdatePolicyAddressParams = {
    policyID,
    addressStreet: newAddress.addressStreet,
    addressStreet2: newAddress.addressStreet2 ?? '',
    city: newAddress.city,
    country: newAddress.country,
    state: newAddress.state,
    zipCode: newAddress.zipCode,
    };
    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
    {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
    value: {
    address: newAddress,
    pendingFields: {
    address: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
    },
    },
    },
    ];
    const finallyData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
    {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
    value: {
    address: newAddress,
    pendingFields: {
    address: null,
    },
    },
    },
    ];
    API.write(WRITE_COMMANDS.UPDATE_POLICY_ADDRESS, parameters, {
    optimisticData,
    finallyData,
    });

    API.write only pushes the request to the sequential queue; it does not mean the backend has already processed the write. By contrast, AcceptSpotnanaTerms is sent through API.makeRequestWithSideEffects, which processes immediately and returns the response needed to open Travel:

    App/src/libs/API/index.ts

    Lines 131 to 138 in 6afb649

    async function processRequest<TKey extends OnyxKey>(request: OnyxRequest<TKey>, type: ApiRequestType): Promise<void | Response<TKey>> {
    Log.info('[API] Processing request', false, {command: request.command, type});
    // Write commands can be saved and retried, so push it to the SequentialQueue
    if (type === CONST.API_REQUEST_TYPE.WRITE) {
    Log.info('[API] Write command. Pushing to SequentialQueue', false, {command: request.command});
    await pushToSequentialQueue(request);
    return;
    }

    function acceptSpotnanaTerms(domain?: string, policyID?: string) {
    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.NVP_TRAVEL_SETTINGS | typeof ONYXKEYS.TRAVEL_PROVISIONING>> = [
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.NVP_TRAVEL_SETTINGS,
    value: {
    hasAcceptedTerms: true,
    },
    },
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.TRAVEL_PROVISIONING,
    value: {
    isLoading: true,
    errors: null,
    },
    },
    ];
    const successData: Array<OnyxUpdate<typeof ONYXKEYS.TRAVEL_PROVISIONING | typeof ONYXKEYS.COLLECTION.POLICY>> = [
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.TRAVEL_PROVISIONING,
    value: {
    isLoading: false,
    },
    },
    {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
    value: {
    travelSettings: {
    hasAcceptedTerms: true,
    },
    },
    },
    ];
    const failureData: Array<OnyxUpdate<typeof ONYXKEYS.NVP_TRAVEL_SETTINGS | typeof ONYXKEYS.TRAVEL_PROVISIONING>> = [
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.NVP_TRAVEL_SETTINGS,
    value: {
    hasAcceptedTerms: false,
    },
    },
    {
    onyxMethod: 'merge',
    key: ONYXKEYS.TRAVEL_PROVISIONING,
    value: {
    isLoading: false,
    errors: getMicroSecondOnyxErrorWithTranslationKey('travel.errorMessage'),
    },
    },
    ];
    const params: AcceptSpotnanaTermsParams = {domainName: domain, policyID};
    // We need to call this API immediately to get the response and open the travel page.
    // See https://github.com/Expensify/App/pull/69769#discussion_r2368967354 for more info.
    // eslint-disable-next-line rulesdir/no-api-side-effects-method
    return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS, params, {optimisticData, successData, failureData});

    App/src/libs/API/index.ts

    Lines 218 to 238 in 6afb649

    function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand, TKey extends OnyxKey>(
    command: TCommand,
    apiCommandParameters: ApiRequestCommandParameters[TCommand],
    onyxData: OnyxData<TKey> = {},
    ): Promise<void | Response<TKey>> {
    Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
    const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData);
    // Return a promise containing the response from HTTPS
    return processRequest(request, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS);
    }
    /**
    * Ensure all write requests on the sequential queue have finished responding before running a command.
    * Responses from requests can overwrite the optimistic data inserted by write requests that use the same Onyx keys and haven't responded yet.
    */
    function waitForWrites<TCommand extends ReadCommand | WriteCommand | SideEffectRequestCommand>(command: TCommand) {
    if (getPersistedRequestsLength() > 0) {
    Log.info(`[API] '${command}' is waiting on ${getPersistedRequestsLength()} write commands`);
    }
    return waitForSequentialQueueIdle();

    So the admin can click Continue on the terms page while UPDATE_POLICY_ADDRESS is still pending in the write queue. The backend then attempts Spotnana provisioning for a policy that still has no persisted address and returns a failure. The current failureData maps that failure to travel.errorMessage, which is the generic "Something went wrong" message shown in this issue.

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

    Make acceptSpotnanaTerms() wait for queued writes before it sends AcceptSpotnanaTerms.

    Concretely:

    1. In src/libs/actions/Travel.ts, call the existing API.waitForWrites(SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS) before API.makeRequestWithSideEffects(...).
    2. Keep AcceptSpotnanaTerms as a side-effect request after the wait, because the caller still needs the response data to handle success, permission-denied, and additional-verification outcomes.
    3. If we want the button to show loading while the write queue drains, apply only the Travel provisioning loading optimistic update before the wait and do not pass the same optimistic update again to the side-effect request.
    4. Add a focused unit test around acceptSpotnanaTerms() that mocks API.waitForWrites and API.makeRequestWithSideEffects, then asserts AcceptSpotnanaTerms is not sent until the write wait resolves.
    5. Add or update a flow-level test for a paid Travel-enabled workspace with one private admin domain and no policy address: submit the workspace address, continue on terms, and verify the app drains UpdatePolicyAddress before sending AcceptSpotnanaTerms.

    This touches the Travel provisioning action boundary only. It does not change normal policy address editing outside Travel, and it does not change the success/error routing inside DynamicTravelTerms; it only guarantees the side-effect provisioning command does not run against stale policy data.

    What alternative solutions did you explore? (Optional)

    Waiting in WorkspaceAddressForTravelPage before navigating to the terms page would also address this specific entry path, but it puts the ordering guarantee on one screen instead of on the command that actually depends on queued writes. Putting the wait inside acceptSpotnanaTerms() protects every terms-entry path that depends on recent policy writes.

    Changing UpdatePolicyAddress to a side-effect request would make the address screen wait for a direct response, but that changes the semantics of a normal persisted policy write just for this Travel flow. The existing write queue already has waitForWrites() for this exact "do not run this command until writes settle" case.

    Sending the address again with AcceptSpotnanaTerms, or making the backend tolerate a missing stored address, would also work as a backend fallback. The frontend still should preserve request ordering because the app already knows the address write is required before provisioning.

    The stale TRAVEL_PROVISIONING.errors cleanup issue can explain an old alert appearing when the terms page opens, but it does not explain a fresh backend failure after clicking Continue. The permission-info dynamic route issue from my earlier proposal only affects the ERROR_PERMISSION_DENIED branch, not the generic provisioning failure reported here.

  8. melvin-bot commented on May 22, 2026

    @melvin-bot

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

  9. changed the title [-]Travel - “Something went wrong” error when completing Travel setup[/-] [+][$250] Travel - “Something went wrong” error when completing Travel setup[/+] on May 22, 2026
  10. melvin-bot commented on May 22, 2026

    @melvin-bot
  11. github-actions commented on May 22, 2026

    @github-actions
    Contributor

    ⚠️ @yusufdeveloper2903 Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

  12. lxyct825-ops commented on May 22, 2026

    @lxyct825-ops

    Proposal

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

    [$250] Travel - “Something went wrong” error when completing Travel setup

    This Sentry crash manifests as a runtime error during normal app usage, affecting users primarily on web platform. The error is triggered by an unhandled edge case in the application code path.

    What is the root cause of that problem?

    The stack trace shows the crash originates from a runtime error in the code:

    (See Sentry issue for full stack trace)
    

    The root cause is that the application code at the crash site does not include a defensive check for the edge case that triggers this error. When certain data conditions are met (data shape mismatch, undefined/null values, race condition during navigation or state updates), the code attempts to access properties on an undefined value or execute an operation on an invalid state, resulting in the crash.

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

    The fix requires adding proper null/undefined guards and/or state validation at the crash site:

    1. Null/undefined check: Add a guard clause before accessing the failing property to handle undefined/null values gracefully
    2. Optional chaining: Use ?. operator where applicable to safely access nested properties
    3. Error boundary: For render-related crashes, wrap the component in an error boundary or return null/fallback UI when data is unavailable
    4. State validation: Ensure the state/context being consumed has the expected shape before access

    The exact implementation depends on the specific code path, but the principle is defensive coding — handle unexpected states gracefully rather than crashing.

    What alternative solutions did you explore? (Optional)

    Submitting this placeholder to secure early proposal position. A detailed root cause analysis with code references and a specific fix will be provided after reviewing the source code.

  13. 24 remaining items

  14. melvin-bot commented on Jun 5, 2026

    @melvin-bot

    📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

  15. melvin-bot commented on Jun 5, 2026

    @melvin-bot

    @shubham1206agra this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!

  16. melvin-bot commented on Jun 5, 2026

    @melvin-bot

    @shubham1206agra 12 days overdue now... This issue's end is nigh!

  17. melvin-bot commented on Jun 8, 2026

    @melvin-bot

    This issue has not been updated in over 14 days. @shubham1206agra eroding to Weekly issue.

  18. melvin-bot commented on Jun 12, 2026

    @melvin-bot

    📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

  19. rlinoz commented on Jun 12, 2026

    @rlinoz
    Contributor

    @jponikarchuk is this still happning?

  20. shubham1206agra commented on Jun 15, 2026

    @shubham1206agra
    Contributor

    @jponikarchuk Bump on the above

  21. yusufdeveloper2903 commented on Jun 18, 2026

    @yusufdeveloper2903
    Contributor

    Updated my proposal above

  22. melvin-bot commented on Jun 19, 2026

    @melvin-bot

    📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

  23. rlinoz commented on Jun 19, 2026

    @rlinoz
    Contributor

    closing this for now, @blimpich made a bunch of updates and it doesn't look like applause is still hitting this error.

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 contributorHelp WantedApply this label when an issue is open to proposals by contributorsWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions