Skip to content

KYC: Feature - SumSub SDK integration - #2165

Merged
JohnathanWhite merged 1 commit into
bitpay:developfrom
cmgustavo:feat/sumsub-sdk-integration-01
Aug 13, 2026
Merged

KYC: Feature - SumSub SDK integration#2165
JohnathanWhite merged 1 commit into
bitpay:developfrom
cmgustavo:feat/sumsub-sdk-integration-01

Conversation

@cmgustavo

@cmgustavo cmgustavo commented May 11, 2026

Copy link
Copy Markdown
Member

Native identity verification (KYC) using the SumSub React Native MobileSDK (@sumsub/react-native-mobilesdk-module).

The backend exposes two signed /api/v2 RPC methods on the bitpayUser facade (same channel as Device Intelligence's getDeviceAccessToken):

App call (BitPayIdApi.apiCall) Params Returns
getKycAccessToken {} SumSub SDK token, or null when the user is not eligible
getKycStatus {} KycStatusResponse object (path, provider, tier, status, activeAttempt, legacy, applicantId)

The signed token identifies the user server-side — no userId and no levelName are sent; the backend picks the level.

Architecture

Backend object is the single source of truth

getKycStatus returns the whole KYC object and we store it verbatim in SUMSUB.kyc[network]. There is no pre-mapped status field: every UI state is derived from the stored object in sumsub.selectors.ts, so the mapping lives in exactly one place.

Two separate status vocabularies

These are intentionally kept in separate fields because they are different vocabularies:

  • Backend getKycStatus.status (camelCase): notStarted, inProgress, pendingReview, requiresAction, onHold, approved, rejected, suspended → stored in SUMSUB.kyc.
  • SumSub SDK result .status (PascalCase): Initial, Incomplete, Pending, Approved, TemporarilyDeclined, FinallyRejected → stored in SUMSUB.sdkStatus.

sdkStatus is only a fallback signal while the backend lags behind a just-finished session (still reports notStarted until an applicant is submitted / the webhook lands). It is cleared automatically once the backend advances past notStarted.

Derivation (sumsub.selectors.ts)

  • deriveKycUiState(kyc, sdkStatus)KycUiState (notStarted | actionRequired | denied | inReview | success). The backend wins; while it's still notStarted the SDK status fills the gap. Unknown in-flight backend states fall back to inReview (safe default — never a false verified/denied). Terminal SDK outcomes are capped at inReview until the backend confirms.
  • isKycEligibleToStart(kyc, sdkStatus) — the single gate for the "Get Verified" / "Verify Identity" prompt: path === 'sumsub' && provider is null/unset or 'sumsub' && status === 'notStarted' && tier === -1 && no in-progress SDK session. The path/provider check excludes users of the previous (legacy) KYC provider; a fresh eligible user comes back with provider: null (assigned only once they engage SumSub), so null counts as SumSub.

Changes

  • Access token via apiCall('getKycAccessToken', {}). Backend returns null for users with no shopper product / shopper tier 1 / already holding KYC tier ≥1; the app detects this and does not launch the SDK (no error shown). Silent token refresh via onTokenExpired.
  • Status sync via apiCall('getKycStatus', {}), stored verbatim. Dispatched on app init (fetchInitialUserData), post-pairing (startPairAndLoadUser), and when the Verify Identity screen opens — so the status survives app restarts (SUMSUB is persisted) and reflects reviews completed outside the app. After the SDK closes it re-fetches the backend and, if the backend hasn't caught up, remembers the SDK's own status in sdkStatus.
  • Get Verified modal (Home): shown on every launch for an eligible user (isKycEligibleToStart), not once-only. A manual dismiss sticks for the session and the modal returns on the next launch. CTA → VerifyIdentity.
  • Entry points: Home "Get Verified" modal (eligible/not-started users), Home banner KycBannerGate (already-started states), Settings → Security → Verify Identity, and the Profile Settings status pill.
  • Auth flows: login always lands on Home (an eligible user also gets the modal); signup walks an eligible user through VerifyIdentity. SecureAccount takes a context: 'login' | 'signup' param; Login / VerifyEmail route accordingly.
  • VerifyIdentity renders per KycUiState: first-time onboarding (notStarted), Action required + "Resume Application" (actionRequired), In Review, Denied, and Success.
  • Redux slice SUMSUBkyc + sdkStatus per network; reset on logout.
  • Native config: Android SumSub maven repo; iOS Podfile + camera/mic/photo usage strings in Info.plist.

Test Cases

1. Get Verified modal (eligible user)

  • Log in with a TEST account that has a verified email and is KYC-eligible (backend getKycStatus returns path:"sumsub", provider:null, tier:-1, status:"notStarted").
  • On Home the Get Verified modal appears. Dismiss it → gone for the session. Kill & reopen the app → it appears again.
  • Tap Verify My Account → navigates to VerifyIdentity.

2. Other entry points

  • Settings → Security → Verify Identity navigates to the flow.
  • Profile Settings shows a status pill; for an eligible not-started user it reads Verify Identity and navigates to the flow. (An ineligible not-started user gets no identity pill.)

3. SDK launch & permissions (iOS)

  • Start the flow → the SumSub SDK opens as a native overlay.
  • Verify the Camera / Microphone / Photo Library permission prompts appear.

4. Happy path

  • Complete verification with SumSub sandbox test documents.
  • On close the app re-fetches getKycStatus; once the backend confirms, the banner/pill reflect the verified state.

5. Status sync across restart (backend authoritative)

  • Complete or partially complete a flow, then kill and reopen the app.
  • On launch getKycStatus re-fetches the authoritative object → the banner/screen reflect it without reopening the SDK.
  • Change the applicant status in the SumSub dashboard (approve/reject) → reopen the Verify Identity screen → status updates.

6. Cancel mid-flow (backend-lag fallback)

  • Start the flow and cancel partway through (SDK closes with Incomplete).
  • The backend still reports notStarted, but the app remembers the SDK status separately, so:
    • VerifyIdentity shows "Action required on your application" + "Resume Application" (NOT the first-time onboarding).
    • The Home banner shows "Action required on your application".
    • The Get Verified modal does NOT reappear.
  • Kill & reopen → still "Action required" (the SDK status is persisted) until the backend catches up.

7. Not-eligible users (no prompt)

  • Null token: log in as a user who is shopper tier 1 / already KYC-verified → the SDK does not launch, no error modal (logged as "KYC not available").
  • Legacy provider: a user whose path is not sumsub, or whose provider is a different (non-null, non-sumsub) provider, gets no modal/prompt.

8. Token refresh

  • Simulate token expiration (600s TTL) → the SDK silently re-mints via onTokenExpired; the flow continues.

Known / pending

  • Backend statuses onHold and suspended currently fall into the inReview default; their intended UI mapping is TBD (pending product confirmation) and can be given explicit cases later.

@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 3 times, most recently from ccdbe11 to 69c9860 Compare May 12, 2026 20:27
@cmgustavo cmgustavo changed the title SumSub: Feature - first sdk integration KYC: Feature - SumSub SDK integration May 15, 2026
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 5 times, most recently from 9c14d4a to bd34d72 Compare May 21, 2026 13:28
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch from bd34d72 to 9726f26 Compare May 22, 2026 13:09
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 8 times, most recently from 586b5e2 to cdf165a Compare June 5, 2026 14:07
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 7 times, most recently from d785663 to b2556e0 Compare July 23, 2026 18:48
@cmgustavo
cmgustavo marked this pull request as ready for review July 23, 2026 18:52
@cmgustavo
cmgustavo requested a review from JohnathanWhite July 23, 2026 18:53
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 3 times, most recently from fc9a5f3 to c28c63f Compare July 30, 2026 19:12
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 5 times, most recently from 34c2cd4 to 67d59ba Compare August 3, 2026 20:44
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch 7 times, most recently from 63d3000 to 79700d8 Compare August 12, 2026 17:41
@cmgustavo
cmgustavo force-pushed the feat/sumsub-sdk-integration-01 branch from 79700d8 to e9315ba Compare August 12, 2026 18:55
@JohnathanWhite
JohnathanWhite merged commit 9975223 into bitpay:develop Aug 13, 2026
4 checks passed
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