Skip to content

Conversation

@dieguezguille
Copy link
Member

@dieguezguille dieguezguille commented Jan 24, 2026

closes #640


Open with Devin

Summary by CodeRabbit

  • New Features

    • Integrated Persona SDK to enable web and native identity verification flows.
  • Refactor

    • Unified KYC flow with improved lifecycle handling for more reliable completion and cancellation.
  • Bug Fixes

    • Prevents overlapping or orphaned KYC sessions when navigating or mounting components.
  • Chores

    • Added Persona runtime dependency and changeset for a patch release.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 24, 2026

🦋 Changeset detected

Latest commit: 8780581

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

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

@sentry
Copy link

sentry bot commented Jan 24, 2026

Codecov Report

❌ Patch coverage is 4.16667% with 69 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.07%. Comparing base (c1fb4c2) to head (8780581).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/utils/persona.ts 2.85% 68 Missing ⚠️
...rc/components/home/card-upgrade/VerifyIdentity.tsx 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #683      +/-   ##
==========================================
- Coverage   61.48%   61.07%   -0.42%     
==========================================
  Files         169      169              
  Lines        5188     5236      +48     
  Branches     1457     1461       +4     
==========================================
+ Hits         3190     3198       +8     
- Misses       1836     1876      +40     
  Partials      162      162              
Flag Coverage Δ
e2e 43.11% <4.16%> (-0.31%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist
Copy link

Summary of Changes

Hello @dieguezguille, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the Know Your Customer (KYC) verification process by integrating the Persona Web SDK. The primary goal is to provide a more seamless and embedded experience for web users, moving away from external redirects. The changes involve a clear separation of concerns for web and native platforms within the KYC initiation logic, ensuring that each environment leverages its optimal Persona SDK implementation.

Highlights

  • Persona Web SDK Integration: The Persona Web SDK has been integrated to handle Know Your Customer (KYC) verification directly within the web application, replacing the previous method of redirecting to a one-time link.
  • Platform-Specific KYC Implementation: The startKYC function has been refactored to provide distinct implementations for web and native platforms, ensuring that web clients use the new Persona Web SDK while native clients continue to use react-native-persona.
  • Enhanced Web KYC Flow: The web KYC process now utilizes the Persona client's lifecycle callbacks (onReady, onComplete, onCancel, onError) for a more controlled and integrated user experience, including automatic client destruction and state management.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Walkthrough

Adds persona dependency and refactors KYC initialization in src/utils/persona.ts to a per-call cancellable startKYC with AbortController, adds cancelKYC, updates components to cancel on unmount, and adds a changeset for a patch release. No other behavioral changes.

Changes

Cohort / File(s) Summary
Dependency Addition
package.json
Added runtime dependency persona at version ^5.5.0.
KYC utility
src/utils/persona.ts
Replaced previous startKYC with a function returning a per-call Promise; adds centralized in-flight operation state, AbortController-based cancellation, cancelKYC() export, lazy web/native SDK loading, enhanced redirect URI logic, and shared handlers for complete/cancel/error/cache invalidation.
Components — cancel on lifecycle
src/components/card/Card.tsx, src/components/home/card-upgrade/VerifyIdentity.tsx, src/utils/useBeginKYC.ts
Import and wire cancelKYC via useEffect cleanup to abort any in-flight KYC on mount/unmount; minor import adjustments.
Changeset
.changeset/spicy-poems-brush.md
New changeset entry for patch release: "✨ implement persona web sdk".

Sequence Diagram(s)

sequenceDiagram
    rect rgba(30,144,255,0.5)
    actor App
    end
    rect rgba(255,165,0,0.5)
    participant TokenSvc as Token Service
    end
    rect rgba(34,139,34,0.5)
    participant Persona as Persona SDK (Web / Native)
    end
    rect rgba(220,20,60,0.5)
    participant Nav as App Navigation / Callbacks
    end

    App->>TokenSvc: request KYC tokens (clientToken, sessionId)
    TokenSvc-->>App: return tokens
    App->>Persona: initialize client with clientToken & config
    Persona-->>App: onReady
    App->>Persona: start/open flow
    Persona-->>Nav: onComplete / onCancel / onError
    Nav-->>App: navigate / handle result
    App->>Persona: destroy/cleanup active client (on finish or abort)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing Persona Web SDK integration with clear, specific terminology.
Linked Issues check ✅ Passed The PR successfully implements Persona Web SDK integration closing #640, adding startKYC/cancelKYC functions, persona dependency, and KYC cancellation hooks across components.
Out of Scope Changes check ✅ Passed All changes are tightly focused on Persona Web SDK implementation including dependency addition, utility function refactoring, and component integration without extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch persona

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements the Persona Web SDK for the KYC process, which is a great addition. The implementation correctly handles platform-specific logic for web and native environments. No specific code review comments are being provided at this time, as the suggested refactoring for startKYC might be considered a premature abstraction according to repository guidelines.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View issue and 4 additional flags in Devin Review.

Open in Devin Review

Comment on lines 18 to 98
export const startKYC = (
Platform.OS === "web"
? () => {
let activeClient: InstanceType<typeof PersonaWeb.Client> | undefined;

const { Inquiry } = await import("react-native-persona");
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1);
router.replace("/(main)/(home)");
})
.onError((error) => reportError(error))
.build()
.start();
}
return async () => {
const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
import("persona"),
getKYCTokens("basic", await getRedirectURI()),
]);

activeClient?.destroy();

activeClient = new Client({
inquiryId,
sessionToken,
environment: environment as "production" | "sandbox", // TODO deprecated - use environmentId instead
// environmentId: "",
onReady: () => activeClient?.open(),
onComplete: () => {
activeClient?.destroy();
activeClient = undefined;
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1); // TODO probably not needed unless the user is upgrading their card
router.replace("/(main)/(home)");
},
onCancel: () => {
activeClient?.destroy();
activeClient = undefined;
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
},
onError: (error) => {
activeClient?.destroy();
activeClient = undefined;
reportError(error);
},
});
};

Choose a reason for hiding this comment

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

🔴 KYC flow broken for Farcaster mini apps and embedded web contexts

The refactored startKYC function removes special handling for mini apps and embedded contexts that was present in the old implementation.

Click to expand

Previous Behavior

The old implementation at src/utils/persona.ts had explicit handling for different web contexts:

if (Platform.OS === "web") {
  if (await sdk.isInMiniApp()) {
    await sdk.actions.openUrl(oneTimeLink);  // Mini app specific handling
    return;
  }
  const embeddingContext = queryClient.getQueryData<EmbeddingContext>(["embedding-context"]);
  if (embeddingContext && !embeddingContext.endsWith("-web")) {
    window.location.replace(oneTimeLink);  // Embedded context handling
    return;
  }
  window.open(oneTimeLink, "_blank", "noopener,noreferrer");
}

New Behavior

The new implementation uses the Persona Web SDK's Client directly for all web contexts without checking if the user is in a mini app or embedded context.

Impact

  1. Farcaster mini apps: The Persona Web SDK likely cannot open its modal/iframe properly in mini app environments where popup/iframe capabilities are restricted. The old code used sdk.actions.openUrl() to properly navigate within the mini app context.

  2. Non-web embedded contexts (e.g., farcaster, base, metamask, phantom): These contexts previously used window.location.replace() to navigate to the one-time link, which is now not handled at all.

Users in these contexts will likely experience a broken KYC flow where the Persona modal fails to open or displays incorrectly.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 95: The Persona v5 client is being initialized with the deprecated
environment field; update the Persona client construction in
src/utils/persona.ts so that new Persona.Client(...) is passed environmentId
instead of environment. Locate the function or constant that calls new
Persona.Client (look for symbols like PersonaClient, createPersonaClient, or the
file-exported client instance), replace the environment:
process.env.PERSONA_ENVIRONMENT (or similar) with environmentId:
process.env.PERSONA_ENVIRONMENT_ID (or the appropriate env var containing the
env_... token), and remove the deprecated environment property so the client
uses the required environmentId parameter.

In `@src/utils/persona.ts`:
- Around line 37-72: The shared startKYC utility currently mutates
queryClient.setQueryData(["card-upgrade"], 1) in both web and native branches
(see the onComplete handlers in persona.ts), which wrongly affects all callers;
remove those setQueryData calls from persona.ts and instead apply the mutation
only in the card-upgrade flow by adding the call inside the
card-upgrade-specific caller (VerifyIdentity component) after the startKYC
promise resolves (or alternatively add an explicit boolean parameter to startKYC
like applyCardUpgrade that defaults to false and only setQueryData when true),
ensuring other callers (useBeginKYC, Card) are unaffected.
♻️ Duplicate comments (1)
src/utils/persona.ts (1)

14-57: Web KYC flow still lacks miniapp/embedded fallback.

The web branch now always instantiates the Persona Web Client; in Farcaster mini apps or embedded contexts this can fail to open due to iframe/popup restrictions. Consider restoring context checks and fallback navigation (e.g., miniapp openUrl or full redirect).

Persona Web SDK support for embedded webviews / Farcaster mini apps and recommended integration when popups/iframes are restricted

package.json Outdated
"i18n-iso-countries": "^7.14.0",
"i18next": "^25.7.4",
"moti": "^0.30.0",
"persona": "^5.0.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Persona Web SDK npm v5 Expo React Native compatibility environment vs environmentId init options

💡 Result:

  • persona (Web/Embedded Flow) v5 (npm i persona): use environmentId (an env_... token) when constructing new Persona.Client(...). This is the current, non-deprecated way to choose the Persona API environment. Example shown in the Embedded Flow quickstart. [1]
  • environment option: this is the deprecated legacy field; it used string values like "sandbox" / "production" and is documented as “Deprecated. Use environmentId instead.” [2]

Expo / React Native compatibility

  • The Web SDK is meant for web (DOM) environments, not a native React Native runtime. For a mobile app, Persona recommends using their native mobile SDKs / React Native SDK instead. [3][4]
  • If you must run Persona in an in-app browser, use a WebView/hosted flow and include is-webview=true in the URL (required for WebView flow). [4]

References
[1] Persona Embedded Flow Quickstart (NPM) – environmentId in Persona.Client init: (docs.withpersona.com)
[2] Persona “Parameters” docs – environment deprecated, use environmentId: (docs.withpersona.com)
[3] Persona React Native Integration Guide (native SDK usage): (docs.withpersona.com)
[4] Persona WebView integration – native SDKs recommended; is-webview=true required: (docs.withpersona.com)

Citations:


🏁 Script executed:

# Search for Persona SDK usage in the codebase
rg -A 5 -B 5 "persona" package.json | head -50

Repository: exactly/exa

Length of output: 768


🏁 Script executed:

# Look for Persona initialization code
rg -A 10 "new Persona|Persona\(" --type ts --type tsx --type js --type jsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

# Check for any persona API version constants or configuration
rg "PERSONA_API_VERSION|Persona.*init|persona.*init" -A 5 --type ts --type tsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

# Search for Persona initialization/client instantiation
rg -i "new Persona|Persona\.Client|import.*persona" -A 3 | head -100

Repository: exactly/exa

Length of output: 4274


🏁 Script executed:

# Look for any persona config or environment setup
rg -i "environmentId|environment.*persona|PERSONA" -B 2 -A 2 | head -100

Repository: exactly/exa

Length of output: 6081


🏁 Script executed:

# Check what files import persona package (non react-native)
rg "from ['\"]persona['\"]|import.*['\"]persona['\"]" -l

Repository: exactly/exa

Length of output: 75


Update Persona v5 initialization: use environmentId instead of deprecated environment field.

The codebase imports Persona v5 but initializes it using the deprecated environment field ("sandbox"/"production"). Persona v5 requires environmentId (an env_... token). Update src/utils/persona.ts to pass environmentId when constructing new Persona.Client(...) to avoid compatibility failures with v5.

🤖 Prompt for AI Agents
In `@package.json` at line 95, The Persona v5 client is being initialized with the
deprecated environment field; update the Persona client construction in
src/utils/persona.ts so that new Persona.Client(...) is passed environmentId
instead of environment. Locate the function or constant that calls new
Persona.Client (look for symbols like PersonaClient, createPersonaClient, or the
file-exported client instance), replace the environment:
process.env.PERSONA_ENVIRONMENT (or similar) with environmentId:
process.env.PERSONA_ENVIRONMENT_ID (or the appropriate env var containing the
env_... token), and remove the deprecated environment property so the client
uses the required environmentId parameter.

Comment on lines 37 to 72
onComplete: () => {
activeClient?.destroy();
activeClient = undefined;
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1); // TODO probably not needed unless the user is upgrading their card
router.replace("/(main)/(home)");
},
onCancel: () => {
activeClient?.destroy();
activeClient = undefined;
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
},
onError: (error) => {
activeClient?.destroy();
activeClient = undefined;
reportError(error);
},
});
};
}
: () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
return async () => {
const { inquiryId, sessionToken } = await getKYCTokens("basic", await getRedirectURI());
const { Inquiry } = await import("react-native-persona");
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1);
router.replace("/(main)/(home)");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n --type=ts --type=tsx 'startKYC\s*\(' -A 3 -B 3

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

rg -n 'startKYC\s*\(' -A 3 -B 3

Repository: exactly/exa

Length of output: 1494


🏁 Script executed:

cat src/utils/persona.ts

Repository: exactly/exa

Length of output: 3642


🏁 Script executed:

cat src/utils/useBeginKYC.ts

Repository: exactly/exa

Length of output: 1258


🏁 Script executed:

cat src/components/card/Card.tsx | head -210 | tail -30

Repository: exactly/exa

Length of output: 848


🏁 Script executed:

head -50 src/components/card/Card.tsx

Repository: exactly/exa

Length of output: 2010


🏁 Script executed:

rg -n "card|Card" src/components/card/Card.tsx | head -5

Repository: exactly/exa

Length of output: 351


🏁 Script executed:

rg -n "^export\s+(default\s+)?.*Card|^function Card|^const Card" src/components/card/Card.tsx

Repository: exactly/exa

Length of output: 90


🏁 Script executed:

rg -n "from.*card/Card|import.*Card.*from.*card/Card" --type js --type ts

Repository: exactly/exa

Length of output: 144


🏁 Script executed:

rg -n "GettingStarted|getting-started" --max-count 20

Repository: exactly/exa

Length of output: 973


🏁 Script executed:

cat src/components/home/card-upgrade/VerifyIdentity.tsx

Repository: exactly/exa

Length of output: 3084


Move setQueryData(["card-upgrade"], 1) calls out of the shared startKYC utility.

The shared utility unconditionally sets ["card-upgrade"] on completion in both web (line 41) and native (line 71) branches, affecting all callers: useBeginKYC (onboarding), Card (general card KYC), and VerifyIdentity (card-upgrade). This mutation should only apply to card-upgrade flows. The existing TODO comment ("probably not needed unless the user is upgrading their card") reflects this concern. Move these setQueryData calls into the card-upgrade-specific caller (VerifyIdentity.tsx) or accept the mutation through a parameter/context to explicitly gate it to relevant flows.

🤖 Prompt for AI Agents
In `@src/utils/persona.ts` around lines 37 - 72, The shared startKYC utility
currently mutates queryClient.setQueryData(["card-upgrade"], 1) in both web and
native branches (see the onComplete handlers in persona.ts), which wrongly
affects all callers; remove those setQueryData calls from persona.ts and instead
apply the mutation only in the card-upgrade flow by adding the call inside the
card-upgrade-specific caller (VerifyIdentity component) after the startKYC
promise resolves (or alternatively add an explicit boolean parameter to startKYC
like applyCardUpgrade that defaults to false and only setQueryData when true),
ensuring other callers (useBeginKYC, Card) are unaffected.

Comment on lines 18 to 28
export const startKYC = (
Platform.OS === "web"
? () => {
let activeClient: InstanceType<typeof PersonaWeb.Client> | undefined;

const { Inquiry } = await import("react-native-persona");
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1);
router.replace("/(main)/(home)");
})
.onError((error) => reportError(error))
.build()
.start();
}
return async () => {
const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
import("persona"),
getKYCTokens("basic", await getRedirectURI()),
]);

This comment was marked as outdated.

Comment on lines 50 to 60
onError: (error) => {
activeClient?.destroy();
activeClient = undefined;
reportError(error);

This comment was marked as outdated.

@dieguezguille dieguezguille changed the base branch from lifi to main January 26, 2026 20:54
@dieguezguille
Copy link
Member Author

@coderabbitai review the latest force-pushed code

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

@dieguezguille: I'll review the latest force-pushed code for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@dieguezguille dieguezguille changed the title ✨ app: implement persona web sdk ✨ implement persona web sdk Jan 26, 2026
Comment on lines 41 to 51
activeClient?.destroy();
activeClient = undefined;
handleCancel();
},
onError: (error) => {
activeClient?.destroy();
activeClient = undefined;
reportError(error);
},
});
};

This comment was marked as outdated.

@cruzdanilo
Copy link
Member

@coderabbitai what can we deprecate/drop in the server after this change?

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@cruzdanilo: Let me investigate the server codebase to identify what can be deprecated after this client-side Persona Web SDK implementation.


🏁 Script executed:

#!/bin/bash
# Find server-related files and directories
fd -t d "server|api|backend" --max-depth 3

# Search for Persona-related code in the repository
rg -n "persona" --type-add 'server:*.{ts,js,tsx,jsx}' -g '!node_modules' -g '!*.md' -g '!package*.json' -C3

# Search for KYC-related endpoints or functions
rg -n "kyc|KYC" --type-add 'server:*.{ts,js,tsx,jsx}' -g '!node_modules' -g '!*.md' -C3

# Look for API route files
fd -e ts -e js "route|api|endpoint" --max-depth 5 -g '!node_modules'

Length of output: 165158


Let me search for the specific OTL (One-Time Link) and redirect flow logic to identify what's deprecated:


🏁 Script executed:

#!/bin/bash
# Search for OTL (One-Time Link) generation and usage
rg -n "generateOTL|one-time-link|OTL" --type-add 'code:*.{ts,tsx}' -g '!node_modules' -g '!*.test.ts' -C2

# Search for redirect URI handling
rg -n "redirectURI|redirect.*uri" --type-add 'code:*.{ts,tsx}' -g '!node_modules' -g '!*.test.ts' -i -C2

# Look at the POST endpoint in kyc.ts to understand the flow
cat server/api/kyc.ts | sed -n '123,180p'

Length of output: 11449


@cruzdanilo: Based on my analysis of the server code, here's what can be deprecated/dropped after implementing the Persona Web SDK:

Can Be Deprecated

1. OTL (One-Time Link) generation for basic KYC flow

In server/api/kyc.ts:

  • Line 214: Remove the generateOTL() call from generateInquiryTokens()
  • Lines 158-159, 220: Remove otl and legacy fields from the POST response

The web client now uses the embedded Persona Client SDK which doesn't require one-time links or redirects. It manages the flow inline with callbacks (onComplete, onCancel, etc.).

2. Response simplification

The POST /kyc endpoint currently returns:

{ inquiryId, otl, sessionToken, legacy }

Can be simplified to:

{ inquiryId, sessionToken }

Both web (Persona SDK) and native (react-native-persona) only need these two values.

3. Potentially: redirectURI parameter

In server/api/kyc.ts (line 130, 140, 156):

  • The redirectURI parameter might be unnecessary now since:
    • Web uses SDK callbacks instead of redirects
    • Native uses SDK callbacks instead of redirects

However, verify this doesn't break native mobile before removing.

Must Keep

Cannot deprecate these:

  1. Webhook handling (server/hooks/persona.ts) - Still needed to process inquiry completion callbacks from Persona
  2. All inquiry management functions - createInquiry(), resumeInquiry(), getInquiry(), etc.
  3. sessionToken generation - Required by both web and native SDKs
  4. Manteca-specific OTL flow - resumeOrCreateMantecaInquiryOTL() is used for additional Manteca ramp KYC and should remain
  5. Account and template evaluation logic - Still needed for determining KYC requirements

Recommendation

Start by removing the OTL-related code from generateInquiryTokens() and simplifying the response. There's even a TODO comment at line 157 suggesting this:

// TODO use a query param to select otl or sessionToken

This cleanup will simplify the codebase and remove the now-obsolete web redirect flow.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/persona.ts`:
- Around line 18-24: The startKYC factory uses an IIFE and multi-line arrow
functions; refactor it into a named function declaration that returns an async
function and convert the onComplete/onCancel/onError handlers into method
shorthand or separate function declarations (locate PersonaWeb.Client usage and
the returned async factory in startKYC); also remove the unconditional
queryClient.setQueryData(["card-upgrade"], 1) from handleComplete and instead
perform that mutation only in the VerifyIdentity flow (where card-upgrade is
relevant), ensuring other callers like Card.tsx and useBeginKYC no longer
trigger the card-upgrade update.

Comment on lines 18 to 24
export const startKYC = (
Platform.OS === "web"
? () => {
let activeClient: InstanceType<typeof PersonaWeb.Client> | undefined;

const { Inquiry } = await import("react-native-persona");
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
router.replace("/(main)/(home)");
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(reportError);
queryClient.setQueryData(["card-upgrade"], 1);
router.replace("/(main)/(home)");
})
.onError((error) => reportError(error))
.build()
.start();
}
return async () => {
const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
Copy link

@coderabbitai coderabbitai bot Jan 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/utils/persona.ts

Repository: exactly/exa

Length of output: 3714


🏁 Script executed:

#!/bin/bash
# Find and display eslint rules for function style and object shorthand
find . -name '.eslintrc*' -o -name 'eslint.config.*' | head -20 | while read file; do
  echo "=== File: $file ==="
  cat "$file"
  echo ""
done

Repository: exactly/exa

Length of output: 3944


🏁 Script executed:

#!/bin/bash
# Search for func-style and object-shorthand rules in eslint configs
rg "func-style|object-shorthand" -A 2 -B 2 --type-list | head -100

Repository: exactly/exa

Length of output: 444


🏁 Script executed:

find . -path "*/exactly/common/eslint/base.mjs" -o -path "*/common/eslint/base.mjs" | head -5

Repository: exactly/exa

Length of output: 79


🏁 Script executed:

cat -n common/eslint/base.mjs | head -100

Repository: exactly/exa

Length of output: 5554


🏁 Script executed:

rg "func-style|object-shorthand" common/eslint/ -A 2 -B 2

Repository: exactly/exa

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Find usages of startKYC to understand flow
rg "startKYC" -B 2 -A 2 --type ts --type tsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Find where card-upgrade query data is used/set
rg "card-upgrade" -B 2 -A 2 --type ts --type tsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Check unicorn eslint plugin documentation for func-style and object-shorthand
npm list eslint-plugin-unicorn 2>/dev/null | head -20

Repository: exactly/exa

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Find usages of startKYC
rg "startKYC" -B 2 -A 2

Repository: exactly/exa

Length of output: 2412


🏁 Script executed:

#!/bin/bash
# Find where card-upgrade is used/set
rg "card-upgrade" -B 2 -A 2

Repository: exactly/exa

Length of output: 7111


🏁 Script executed:

#!/bin/bash
# Check VerifyIdentity.tsx mentioned in learnings to understand flow-specific behavior
cat -n src/components/home/card-upgrade/VerifyIdentity.tsx | head -80

Repository: exactly/exa

Length of output: 3229


Refactor to function declarations and method shorthand; move card-upgrade mutation to flow-specific context.

The IIFE with nested arrow functions (lines 18–54) and multi-line arrow function properties (onComplete, onCancel, onError) violate the coding guideline requiring function declarations for multi-line functions and object method shorthand. Refactor the factory and handlers to use proper function declarations and method shorthand syntax.

Additionally, line 72 unconditionally sets queryClient.setQueryData(["card-upgrade"], 1) in handleComplete(), but this should be flow-specific. The card-upgrade context in VerifyIdentity.tsx (line 28) already handles this mutation, and other KYC flows using startKYC (e.g., Card.tsx, useBeginKYC) should not trigger card-upgrade state changes. Move this mutation out of the general utility.

♻️ Proposed refactor
-export const startKYC = (
-  Platform.OS === "web"
-    ? () => {
-        let activeClient: InstanceType<typeof PersonaWeb.Client> | undefined;
-
-        return async () => {
-          const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
-            import("persona"),
-            getKYCTokens("basic", await getRedirectURI()),
-          ]);
-
-          activeClient?.destroy();
-          activeClient = new Client({
-            inquiryId,
-            sessionToken,
-            environment: environment as "production" | "sandbox", // TODO implement environmentId
-            onReady: () => activeClient?.open(),
-            onComplete: () => {
-              activeClient?.destroy();
-              activeClient = undefined;
-              handleComplete();
-            },
-            onCancel: () => {
-              activeClient?.destroy();
-              activeClient = undefined;
-              handleCancel();
-            },
-            onError: (error) => {
-              activeClient?.destroy();
-              activeClient = undefined;
-              reportError(error);
-            },
-          });
-        };
-      }
-    : () => startNativeKYC
-)();
+function createStartKYC() {
+  if (Platform.OS === "web") {
+    let activeClient: InstanceType<typeof PersonaWeb.Client> | undefined;
+
+    async function startWebKYC() {
+      const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
+        import("persona"),
+        getKYCTokens("basic", await getRedirectURI()),
+      ]);
+
+      activeClient?.destroy();
+      activeClient = new Client({
+        inquiryId,
+        sessionToken,
+        environment: environment as "production" | "sandbox", // TODO implement environmentId
+        onReady: () => activeClient?.open(),
+        onComplete() {
+          activeClient?.destroy();
+          activeClient = undefined;
+          handleComplete();
+        },
+        onCancel() {
+          activeClient?.destroy();
+          activeClient = undefined;
+          handleCancel();
+        },
+        onError(error) {
+          activeClient?.destroy();
+          activeClient = undefined;
+          reportError(error);
+        },
+      });
+    }
+
+    return startWebKYC;
+  }
+
+  return startNativeKYC;
+}
+
+export const startKYC = createStartKYC();
🤖 Prompt for AI Agents
In `@src/utils/persona.ts` around lines 18 - 24, The startKYC factory uses an IIFE
and multi-line arrow functions; refactor it into a named function declaration
that returns an async function and convert the onComplete/onCancel/onError
handlers into method shorthand or separate function declarations (locate
PersonaWeb.Client usage and the returned async factory in startKYC); also remove
the unconditional queryClient.setQueryData(["card-upgrade"], 1) from
handleComplete and instead perform that mutation only in the VerifyIdentity flow
(where card-upgrade is relevant), ensuring other callers like Card.tsx and
useBeginKYC no longer trigger the card-upgrade update.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai is this fixed in the latest force-pushed code?

Copy link

Choose a reason for hiding this comment

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

@dieguezguille No, the issues have not been addressed yet. The current code still has the same three concerns:

  1. IIFE pattern (lines 18-55): The startKYC export still uses an IIFE with nested arrow functions instead of a named function declaration.

  2. Arrow function properties (lines 33, 34-39, 40-45, 46-51): The onReady, onComplete, onCancel, and onError handlers are still multi-line arrow functions rather than method shorthand.

  3. Card-upgrade mutation (line 72): The handleComplete() function still unconditionally calls queryClient.setQueryData(["card-upgrade"], 1), which should only happen in the card-upgrade flow (e.g., VerifyIdentity.tsx), not in all KYC flows like Card.tsx or useBeginKYC.

The refactor I suggested would address all three issues by:

  • Converting the IIFE to a named createStartKYC() function
  • Using method shorthand syntax for the event handlers
  • Removing the card-upgrade mutation from the shared handleComplete() utility

Would you like to apply these changes?


🧠 Learnings used
Learnt from: cruzdanilo
Repo: exactly/exa PR: 653
File: src/utils/persona.ts:42-47
Timestamp: 2026-01-22T19:39:06.502Z
Learning: In `src/utils/persona.ts`, when using `Inquiry.fromInquiry(inquiryId)` to resume a server-created inquiry, the `.environment()` call is not needed because the environment was already set when the inquiry was created on the server. The `.environment()` call is only necessary when creating a new inquiry client-side using methods like `.fromTemplate()`.

Learnt from: franm91
Repo: exactly/exa PR: 653
File: src/components/home/card-upgrade/VerifyIdentity.tsx:27-29
Timestamp: 2026-01-22T19:01:57.860Z
Learning: In `src/components/home/card-upgrade/VerifyIdentity.tsx`, the KYC mutation logic intentionally differs from the `GettingStarted.tsx` variants. The differences include calling `queryClient.setQueryData(["card-upgrade"], 1)` on success status and showing a toast via `onError` callback. These are intentional flow-specific behaviors for the card upgrade context.

Learnt from: franm91
Repo: exactly/exa PR: 653
File: src/components/getting-started/GettingStarted.tsx:132-133
Timestamp: 2026-01-22T18:44:56.353Z
Learning: In the KYC flow refactor, the `{ inquiryId, sessionToken }` response from the KYC API is a legacy format that was only returned when a `templateId` parameter was provided. This legacy format is being deprecated. The new flow in `src/components/getting-started/GettingStarted.tsx` and related files calls `getKYCStatus()` without the `templateId` parameter, so the legacy response shape will never be returned, and the `"code" in status` type guard is sufficient.
<!--

Learnt from: mainqueg
Repo: exactly/exa PR: 637
File: server/utils/persona.ts:155-160
Timestamp: 2026-01-15T19:10:12.433Z
Learning: In server/utils/persona.ts, the PERSONA_API_VERSION constant is intentionally set to "2023-01-05" to match the API version configured for the production API key in Persona's admin dashboard. This ensures consistent response behavior across different environments and should not be updated without also updating the production API key configuration.

Comment on lines 30 to 40
return new Promise<void>((resolve, reject) => {
activeClient = new Client({
inquiryId,
sessionToken,
environment: environment as "production" | "sandbox", // TODO implement environmentId
onReady: () => activeClient?.open(),
onComplete: () => {
activeClient?.destroy();
activeClient = undefined;
handleComplete();
resolve();

This comment was marked as outdated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/utils/persona.ts`:
- Line 34: Update the trailing TODO on the environment assignment in
src/utils/persona.ts (the line containing environment: environment as
"production" | "sandbox", // TODO implement environmentId) by either replacing
the TODO with a linked issue reference (e.g. // TODO implement environmentId -
ISSUE-1234) or removing the TODO entirely if no ticket exists; ensure the
comment follows project guidelines and references the issue key in the same
comment when keeping the TODO.
- Around line 18-55: startKYC can leave a pending Promise when a previous
Persona web Client is destroyed (Client.destroy) because destroy doesn't call
onCancel/onComplete; modify startKYC to track the current promise's
resolve/reject alongside activeClient and, before creating a new Client (or when
calling activeClient?.destroy()), explicitly settle the prior promise (call its
resolve or reject with an appropriate Error) so awaiting callers don't hang;
update references in startKYC to use these stored settle functions and ensure
all paths that null out activeClient (onComplete, onCancel, onError, and the
pre-create destroy) also clear and settle the stored promise, keeping functions
like handleComplete, handleCancel, reportError, and the new settle logic
consistent.

.start();
}
return () => {
if (pending) return pending;

This comment was marked as outdated.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai Persona SDK handles onCancel. is this a valid concern?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Persona web SDK onCancel callback behavior navigation

💡 Result:

In Persona’s Embedded Flow (web SDK), onCancel is only meant to fire when the user explicitly cancels/exits the flow UI before completion (or when your code calls client.cancel(...)). It is not a reliable signal for browser/navigation events like route changes, refresh, tab close, or the user hitting the browser back button—because the page/iframe can unload before the SDK can run callbacks, and Persona explicitly cautions not to rely on callbacks for critical logic. [1]

Key details that matter for “navigation” cases:

  • What triggers onCancel: user cancels the inquiry flow before finishing; the callback receives { inquiryId, sessionToken }, which can be undefined if cancellation happens before an inquiry is created. [1]
  • Cancel vs. “done/close”: onCancel is distinct from completion; Persona even renamed onExitonCancel to clarify it’s not the “close after completed” path. [2]
  • Programmatic cancel: if you want consistent “user is leaving this screen” behavior in your app’s navigation, you can call client.cancel(false) (show confirmation) or client.cancel(true) (immediately hide). [3]
  • If you need truth about inquiry state: use webhooks / server-side inquiry status, not client callbacks. [1]

Sources:
[1] Persona Docs – Client Callbacks (onCancel + callback reliability note) (docs.withpersona.com)
[2] Persona Docs – Embedded Flow Changelog (rename onExitonCancel) (docs.withpersona.com)
[3] Persona Docs – Client Methods (client.cancel(force)) (docs.withpersona.com)

Citations:


@dieguezguille Unfortunately, this is a valid concern. According to Persona's documentation, onCancel only fires when the user explicitly cancels through Persona's UI—it does not fire for browser/app navigation events like:

  • Browser back button
  • Tab close
  • Page refresh
  • Route changes via Expo Router

Persona explicitly notes that "the page/iframe can unload before the SDK can run callbacks" and cautions against relying on client callbacks for critical logic.

The problem: If a user navigates away using any of these methods, none of the callbacks (onComplete/onCancel/onError) will fire, leaving pending stuck and blocking all future KYC attempts until app restart.

Suggested fix: Add cleanup logic where startKYC is called. For example, in the calling component:

useEffect(() => {
  return () => {
    // On component unmount, programmatically cancel the Persona flow
    // This will trigger onCancel and clear the pending state
    activeClient?.cancel?.(true); // force cancel without confirmation
  };
}, []);

However, since activeClient is in closure scope, you'd need to expose a cleanup function from startKYC, or track the client in a ref that can be cleaned up externally.

Alternative approach: Add a timeout or expose a reset() function that clears the pending state if needed.


🧠 Learnings used
Learnt from: cruzdanilo
Repo: exactly/exa PR: 653
File: src/utils/persona.ts:42-47
Timestamp: 2026-01-22T19:39:06.502Z
Learning: In `src/utils/persona.ts`, when using `Inquiry.fromInquiry(inquiryId)` to resume a server-created inquiry, the `.environment()` call is not needed because the environment was already set when the inquiry was created on the server. The `.environment()` call is only necessary when creating a new inquiry client-side using methods like `.fromTemplate()`.

Learnt from: franm91
Repo: exactly/exa PR: 653
File: src/components/home/card-upgrade/VerifyIdentity.tsx:27-29
Timestamp: 2026-01-22T19:01:57.860Z
Learning: In `src/components/home/card-upgrade/VerifyIdentity.tsx`, the KYC mutation logic intentionally differs from the `GettingStarted.tsx` variants. The differences include calling `queryClient.setQueryData(["card-upgrade"], 1)` on success status and showing a toast via `onError` callback. These are intentional flow-specific behaviors for the card upgrade context.

Learnt from: franm91
Repo: exactly/exa PR: 653
File: src/components/getting-started/GettingStarted.tsx:132-133
Timestamp: 2026-01-22T18:44:56.353Z
Learning: In the KYC flow refactor, the `{ inquiryId, sessionToken }` response from the KYC API is a legacy format that was only returned when a `templateId` parameter was provided. This legacy format is being deprecated. The new flow in `src/components/getting-started/GettingStarted.tsx` and related files calls `getKYCStatus()` without the `templateId` parameter, so the legacy response shape will never be returned, and the `"code" in status` type guard is sufficient.
<!--

Learnt from: mainqueg
Repo: exactly/exa PR: 637
File: server/utils/persona.ts:155-160
Timestamp: 2026-01-15T19:10:12.433Z
Learning: In server/utils/persona.ts, the PERSONA_API_VERSION constant is intentionally set to "2023-01-05" to match the API version configured for the production API key in Persona's admin dashboard. This ensures consistent response behavior across different environments and should not be updated without also updating the production API key configuration.

Learnt from: mainqueg
Repo: exactly/exa PR: 637
File: server/hooks/persona.ts:186-195
Timestamp: 2026-01-15T19:06:45.697Z
Learning: In server/hooks/persona.ts, the webhook handler has intentionally different error handling for Manteca vs Panda flows: the Manteca path (payload.template === "manteca") lets addDocument errors propagate to enable webhook retries, while the Panda path uses .catch() to swallow addDocument errors and prevent retries since the user creation is already committed.

@dieguezguille dieguezguille force-pushed the persona branch 2 times, most recently from bb45d9b to 45ed32a Compare January 26, 2026 23:16
Comment on lines 87 to 93
reportError(error);
reject(error);
})
.build()
.start();

This comment was marked as outdated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/persona.ts`:
- Around line 71-92: The abort listener attached to signal may remain if the
inquiry finishes first; change the listener to a named function (e.g., onAbort)
so you can remove it when the inquiry finishes or errors. In the Promise where
Inquiry.fromInquiry(inquiryId)... is built, register the listener with a named
callback, then call signal.removeEventListener("abort", onAbort) inside the
.onCanceled, .onComplete, and .onError handlers (before resolve/reject) to
ensure the abort listener is cleaned up; reference symbols: Inquiry.fromInquiry,
signal.addEventListener / removeEventListener, onCanceled, onComplete, onError,
handleCancel, handleComplete, reportError.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/persona.ts`:
- Around line 105-108: Remove the double type cast on the sdk.context call and
use real runtime checks with the actual returned value: replace the `(await
sdk.context) as unknown as undefined | { client: { appUrl?: string } }` pattern
by awaiting sdk.context into a local variable (e.g., miniappContext or ctx) and
then check `miniappContext` and `miniappContext.client?.appUrl` at runtime;
update the Platform.OS === "web" branch to return miniappContext.client.appUrl
only if the runtime check passes, matching how other files use await sdk.context
without type assertions.

@cruzdanilo cruzdanilo merged commit 8780581 into main Jan 27, 2026
13 of 15 checks passed
@cruzdanilo cruzdanilo deleted the persona branch January 27, 2026 14:24
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.

app: debug kyc inside base app

3 participants