Skip to content

feat(react-native): identity verification component and takeover flag - #2019

Merged
AngelPaella merged 7 commits into
mainfrom
angel/react-native-identity-verification
Aug 13, 2026
Merged

feat(react-native): identity verification component and takeover flag#2019
AngelPaella merged 7 commits into
mainfrom
angel/react-native-identity-verification

Conversation

@AngelPaella

@AngelPaella AngelPaella commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Ships CrossmintIdentityVerification for React Native, plus the identityVerificationHandling="external" takeover flag, so an RN merchant can render Crossmint's hosted verification step in their own screen instead of inside embedded checkout.

The hosted page at /sdk/unstable/identity-verification already detects window.ReactNativeWebView and speaks the RN bridge protocol, so this is client-only: no crossbit-main change, no new wire contract, no new dependency.

What's here

CrossmintIdentityVerification hosts that page in RNWebView, builds its URL through the existing createIdentityVerificationService(...).iframe.getUrl(props), and subscribes the five shipped events (ui:height.changed, kyc:ready, kyc:completed, kyc:cancelled, kyc:error) through WebViewParent, surfacing them as the same props the web component takes.

Details worth review:

  • A failed load reports through onError. Without it a 500 or an offline device produces no kyc:ready and no kyc:error, and the view stays 0px tall, so the merchant cannot distinguish a dead page from a loading one. Both onError and onHttpError map to the shipped contract's non-retriable widget-unavailable. No main-frame guard is needed, unlike Flutter's equivalent: RNCWebViewClient overrides only the legacy 4-arg onReceivedError (main-frame only by Android contract, never the modern WebResourceRequest overload) and wraps onReceivedHttpError in if (request.isForMainFrame()), so a Persona sub-frame hiccup cannot reach these. retriable: false matches Flutter and is honest: the component does not retry on its own.
  • Listener cleanup uses the ids client.on() returns, not the event name. off(name) removes nothing, which is the leak EmbeddedCheckoutV3WebView.tsx has today. The unmount test covers it.
  • Callbacks are read off a ref, so listeners subscribe once and a late event still calls the current render's props. This is the component's subtlest mechanism and it now has a dedicated test: removing the ref leaves every other test green, because they each render once.
  • domStorageEnabled is set explicitly. Android's WebSettings default is off and react-native-webview applies no default of its own: the codegen spec declares domStorageEnabled?: boolean with no WithDefault, and the JS component has no defaultProps, so an unset prop means the native setter is never called. (The prop's own JSDoc claiming "The default value is true" is stale.) The hosted Persona widget touching localStorage would then fail on Android only. Checkout's WebView and ExportPrivateKeyButton both set it for the same reason. Not copying the rest of checkout's Android block: mixedContentMode="always", allowFileAccess and allowUniversalAccessFromFileURLs are security loosenings with no identified need here.
  • WebViewParent receives webViewRef itself rather than a { current: node } snapshot, so the transport reads the live node. Inert today (identityVerificationOutgoingEvents is {} so nothing sends, and no recovery is configured so nothing reloads), but it is also less code than the pattern it was copied from.

height starting at 0 is deliberate, matching both in-repo precedents: the hosted page reports ~660 off a fixed-pixel Persona container regardless of the WebView's own height, and a 0-height RN WebView still loads and runs JS.

getIdentityVerificationCredentials is re-exported from client-sdk-base. It is platform-neutral and already tested there, so this is a re-export rather than an implementation, and the build is what proves the symbol exists.

identityVerificationHandling moves from CrossmintEmbeddedCheckoutV3WebProps onto the shared common props. RN builds its checkout URL with the same getUrl(props) and appendObjectToQueryParams, so moving the field is the entire change: no forwarding code. CrossmintEmbeddedCheckoutV3WebProps stays as a deprecated alias because deleting an exported type is breaking.

Test runner

The package had a vitest.config.ts and a turbo.json declaring test:vitest, but no script, no test deps and no test files, so CI ran nothing here. Added the script and deps; this is the package's first suite, 10 tests.

Two test-environment notes:

  • react-native-get-random-values is stubbed via a vitest.config.ts alias rather than vi.mock. rn-window's transport imports that polyfill, which requires Flow-typed react-native and is unparseable by vitest, and rn-ui and rn-window resolve different installed copies (2.0.0 vs 1.11.0), so a vi.mock in the test file never intercepts the copy rn-window's dist actually imports. The alias works regardless of importer and covers future tests in this package.
  • The react-native-webview mock forwards ref. React 19 does not assign refs to function components, and without it webViewRef.current stays null, the client is never constructed, and every event assertion passes vacuously.

Known and accepted

An event arriving before the post-mount effect constructs the client is dropped, since onMessage forwards through client?.handleMessage. The window is one render against a full page load, and checkout has the same shape. onMessage itself stays a defined function across that window on purpose: react-native-webview derives messagingEnabled from typeof onMessage === "function", so passing client?.handleMessage directly would create the native view with messaging off and no window.ReactNativeWebView for the hosted page to detect.

Not here

Camera permission code (mediaCapturePermissionGrantType on iOS, a CAMERA declaration in withCrossmintUI) is gated on Leg 1 of the device-test protocol. The documentation is not: RNCWebChromeClient.onPermissionRequest maps RESOURCE_VIDEO_CAPTURE to Manifest.permission.CAMERA and calls Activity.requestPermissions, which cannot succeed unless the host app declares it, and that holds regardless of what Leg 1 finds. So the reference snippet now carries a note naming both the Android manifest entry and NSCameraUsageDescription. Run A may show react-native-webview@13.15.0 already grants capture, in which case the deliverable is a docs note. If it shows Android denies it, the fix belongs in EmbeddedCheckoutV3WebView.tsx too, since the shipped in-checkout KYC step has the same gap. Writing it before the test means guessing.

Manual end-to-end verification of the flag is also not possible yet: no RN app with embedded checkout exists (the repo's only Expo app is wallets-only, and embedded-checkout-playground is Next.js).

Release note

The flag is a footgun while the component is unpublished: a merchant setting "external" with nothing to mount leaves the buyer unable to finish. Both changesets are in this PR so the component and the flag go out in the same release.

Separately, published client-sdk-base@2.7.0 still builds the dead /sdk/unstable/kyc-verification route. The fix is already on main; it is waiting in CI's release PR #2013.

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bc17d69

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@crossmint/client-sdk-base Minor
@crossmint/client-sdk-react-native-ui Minor
@crossmint/client-sdk-react-ui Minor
@crossmint/client-sdk-nextjs-starter Patch
@crossmint/client-sdk-auth Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-verifiable-credentials Patch
@crossmint/client-sdk-smart-wallet Patch
@crossmint/common-sdk-auth Patch
@crossmint/wallets-playground-expo Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/wallets-playground-react Patch
@crossmint/server-sdk Patch
@crossmint/wallets-sdk Patch
crossmint-auth-node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(checkout): accept identityVerificat..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "refactor(react-native): drop speculative..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Reviews (3): Last reviewed commit: "fix(react-native): enable DOM storage fo..." | Re-trigger Greptile

@AngelPaella
AngelPaella requested a review from luans-qa August 12, 2026 14:07
Comment thread packages/client/ui/react-native/scripts/generate-reference.mjs
Comment thread packages/client/ui/react-native/examples.json Outdated
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Reviews (4): Last reviewed commit: "fix(react-native): report failed identit..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Reviews (5): Last reviewed commit: "feat(checkout): add useIdentityVerificat..." | Re-trigger Greptile

@luans-qa luans-qa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

@AngelPaella
AngelPaella merged commit a8b6b60 into main Aug 13, 2026
6 checks passed
@AngelPaella
AngelPaella deleted the angel/react-native-identity-verification branch August 13, 2026 21:16
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