Skip to content

KYC: Fix - Remove ongoing process modal from SumSub launch - #2403

Open
cmgustavo wants to merge 1 commit into
bitpay:developfrom
cmgustavo:bug/kyc-modal-verify-identity-01
Open

cmgustavo wants to merge 1 commit into
bitpay:developfrom
cmgustavo:bug/kyc-modal-verify-identity-01

Conversation

@cmgustavo

Copy link
Copy Markdown
Member

On iOS the SDK is presented from keyWindow.rootViewController's topmost presentedViewController, so launching while the ongoing process modal was still dismissing handed SumSub a view controller RN was tearing down: the flow never opened and the orphaned VC swallowed every touch. The token mint is a single fast RPC, so the spinner is dropped rather than timed around.
Also waits out the SumSub VC dismissal before the "Failed" error modal.

@JohnathanWhite JohnathanWhite left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traced the control flow and the iOS rationale holds up. Two things worth addressing before merge, one of them a behavioral regression.

What I confirmed is sound:

  • The no-token / Failed asymmetry is correct. The Failed path (:136) and the catch-all (:155) both run after the SumSub VC has been presented, so they genuinely need to wait out a native dismissal — adding the delay at :136 makes them consistent. The no-token path runs before any SDK launch, and with the ongoing-process modal gone there is nothing left dismissing, so dropping its delay is right rather than an oversight.
  • The only caller can't collide. VerifyIdentity.tsx:147 handleResume is a plain button press on a full screen, not a sheet dismissal, and GetVerifiedModal only navigates from onModalHide. No competing dismissal at the moment the no-token modal fires.
  • The import removal is clean. No remaining ongoingProcessManager reference in the file; sleep and MODAL_HANDOFF_DELAY are still used by both the Failed and exception paths.
  • No coverage lost in the refactor. Deleting the spinner describe matches the production deletion, and the two merged not-eligible tests kept their assertions.

} finally {
ongoingProcessManager.hide();
}
const accessToken = await getAccessToken();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the ongoing-process modal also drops the only thing that was suppressing input during the token fetch. OngoingProcess.tsx renders a full-screen BottomSheetBackdrop with pressBehavior={'none'} (src/components/modal/ongoing-process/OngoingProcess.tsx:110), so while it was up the button underneath could not be tapped again.

With it gone nothing guards re-entry. Button's debounce is debounceTime || 0 with {leading: true, trailing: false} (src/components/button/Button.tsx:405-417) and VerifyIdentity passes no debounceTime, so a second tap fires immediately. startKycVerification has no in-flight guard of its own. On a slow connection a double tap therefore gets you two fetchAccessToken calls, two startKycAttempt calls — server-side state, not just a read — and two launchSumSubSdk launches.

The modal-free approach is the right fix; it just needs a non-modal guard in its place. Button already accepts state?: ButtonState with 'loading' (src/components/button/Button.tsx:41,61), used inline in CardIntro.tsx:107, UpdateCardName.tsx:60 and ActivateScreen.tsx:178. That renders inside the RN view hierarchy rather than as a presented view controller, so it cannot recreate the teardown race this PR is fixing — and it restores the tap feedback that is otherwise missing for however long the RPC takes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the spinner was doing double duty and I only accounted for the presentation side. Fixed with state={buttonState} on the three handleResume buttons: Button.tsx:387 bails out of onPress while state is truthy, so it is a real guard and not just feedback.
It stays on for the whole session rather than only the mint, so a second attempt cannot be opened while the SDK is up either.

`[SumSub] SDK failed — errorType: ${result.errorType}, errorMsg: ${result.errorMsg}`,
),
);
await sleep(MODAL_HANDOFF_DELAY);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This delay is the only new production behavior in the PR and nothing pins it. The spec mocks sleep to resolve immediately (sumsub.effects.spec.ts:36-39) and the Failed test only asserts that the modal eventually shows (:240), so it would still pass if this await were deleted outright or moved below the dispatch.

Worth asserting that sleep was called with MODAL_HANDOFF_DELAY and that it ordered before the showBottomNotificationModal dispatch — otherwise the next refactor can silently undo the thing this PR set out to fix.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinned — though not via the sleep mock: the factory hands the effect a different module instance than the spec reads, so the call was recorded on an object the assertion never looks at. Dropped that mock; the test now measures the real elapsed time via store.subscribe.
Mutation-checked both ways: deleting the await fails it, moving it below the dispatch fails it. MODAL_HANDOFF_DELAY is exported so the test cannot drift from the source.

@cmgustavo
cmgustavo force-pushed the bug/kyc-modal-verify-identity-01 branch from 979dd72 to cda54a6 Compare September 18, 2026 02:09

@JohnathanWhite JohnathanWhite left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at cda54a63. Both items from the last pass are resolved, and I verified them rather than taking the diff at face value.

1. Double-tap re-entrancy — fixed. state='loading' isn't just cosmetic here: Button.tsx early-returns from _onPress whenever state is truthy, and there's a leading-edge debounce on top of that. All three launch buttons pass the state (VerifyIdentity.tsx:175,197,201), and startKycVerification has exactly one call site, so there's no unguarded entry point left.

2. The sleep before the Failed modal is now covered, and the test is real. I mutation-tested it — deleting the await sleep(MODAL_HANDOFF_DELAY) from the Failed branch makes waits out the SumSub view controller dismissal before the error modal fail, so it's a genuine regression test and not a tautology. Full spec is 27/27 green with the fix in place.

Nothing new found. Things I checked and cleared:

  • The thunk returns a promise, so await dispatch(...) plus the finally in handleResume really does hold the loading state for the whole mint and reset it on rejection too.
  • The no-token path losing its await sleep(MODAL_HANDOFF_DELAY) is fine — no SDK has been launched and the ongoing-process modal is gone, so there's no dismissal to wait out there.
  • The makeLoggedInStore -> makeStore(overrides) refactor spreads whole top-level slices, but sumsub.reducer.ts restores every omitted subfield during init, so partial SUMSUB overrides don't silently drop defaults.
  • No coverage lost in the spec rewrite: the two null-token tests were consolidated with both assertions retained (plus a new startKycAttempt one), and the deleted spinner block covers behavior this PR intentionally removes.

One small caveat, not a blocker: the timing test asserts wall-clock elapsed time, so a long enough CI stall between dispatches could in theory let it pass for the wrong reason. Fine as is — just don't tighten the margin.

LGTM.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants