Conversation
JohnathanWhite
left a comment
There was a problem hiding this comment.
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:136makes 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:147handleResumeis a plain button press on a full screen, not a sheet dismissal, andGetVerifiedModalonly navigates fromonModalHide. No competing dismissal at the moment the no-token modal fires. - The import removal is clean. No remaining
ongoingProcessManagerreference in the file;sleepandMODAL_HANDOFF_DELAYare still used by both the Failed and exception paths. - No coverage lost in the refactor. Deleting the spinner
describematches the production deletion, and the two merged not-eligible tests kept their assertions.
| } finally { | ||
| ongoingProcessManager.hide(); | ||
| } | ||
| const accessToken = await getAccessToken(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
979dd72 to
cda54a6
Compare
JohnathanWhite
left a comment
There was a problem hiding this comment.
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 thefinallyinhandleResumereally 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, butsumsub.reducer.tsrestores every omitted subfield during init, so partialSUMSUBoverrides 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
startKycAttemptone), 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.
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.