Skip to content

feat(card): self-serve identity re-upload CTA via Rain card portal (TASK-21687) - #2904

Merged
innolope-dev merged 2 commits into
devfrom
feat/rain-hosted-card-cta
Sep 1, 2026
Merged

innolope-dev merged 2 commits into
devfrom
feat/rain-hosted-card-cta

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Problem

A card application Rain rejected on an identity document (BAD_PROOF_OF_IDENTITY / DOCUMENT_MISSING) showed only the contact-support dead end — no way for the user to fix it themselves.

Change

Pairs with peanut-api-ts #1482, which reclassifies these rails as fixable and emits a new rain-hosted next-action. This PR renders that action as an "Upload identity documents" CTA that opens Rain's card-member portal — where the user re-uploads and Rain re-adjudicates (verified live: the portal mints a fresh Sumsub token for a rejected applicant).

  • Generalize the hosted-verification handoff (it was Bridge-only but the top-level-tab handoff — gesture-bound tab reservation, reverse-tabnabbing guard, native in-app browser — applies to any un-iframeable third-party page):
    • useBridgeHostedVerification() → useHostedVerification(actionKey) (file renamed)
    • startBridgeHostedVerification() → startHostedVerification(key)
  • card page: surface the rain rail's rain-hosted action as onUploadIdentity, opening the portal via the hook.
  • ApplicationStatusScreen: render the identity-upload CTA as primary on the support variants (mirrors the existing proof-of-address CTA), contact-support kept as the fallback.
  • types/i18n: add rain-hosted to NextActionKind; card.uploadIdentityDocuments copy in en / es-419 / pt-BR.

Notes

  • selectBridgeTasks (home carousel) matches only accept-tos/bridge-hosted, so rain-hosted is not swept into the Bridge flow — it surfaces on the card status screen where the user already is.
  • Uses the hand-mirrored src/types/capabilities.ts (the FE source of truth for the capabilities contract), so the generated api.openapi.json/api.generated.ts are untouched and check:api stays green; the generated spec syncs from the API repo separately.

Test

  • ApplicationStatusScreen: new cases for the identity CTA (renders on support/rejected variants, omitted otherwise and on non-support variants).
  • AdditionalVerificationView: updated for the renamed action; still green.
  • tsc clean.

…ASK-21687)

A card application rejected on an identity document showed only the
contact-support dead end. The backend now classifies these as fixable and
emits a `rain-hosted` next-action; render it as an "Upload identity
documents" CTA that opens Rain's card-member portal — the same hosted
handoff as bridge-hosted (Rain runs and re-adjudicates the re-upload).

- Generalize the hosted-verification hook + server action to any hosted
  provider key: useBridgeHostedVerification → useHostedVerification(key),
  startBridgeHostedVerification → startHostedVerification(key).
- card page: surface the rail's rain-hosted action as onUploadIdentity.
- ApplicationStatusScreen: identity upload CTA on support variants.
- add `rain-hosted` to NextActionKind; card.uploadIdentityDocuments copy
  (en/es-419/pt-BR).
@notion-workspace

Copy link
Copy Markdown

@vercel

vercel Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 1, 2026 1:26pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Essentials

Run ID: 33e850fa-9085-4800-8fc5-033a0798c7bf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@innolope-dev innolope-dev self-assigned this Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7151.3 → 7152.14 (+0.84)
Findings: +2 net (+20 new, -18 resolved)

🆕 New findings (20)

  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 142, MI 56.28, SLOC 543
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 50 commits, +1145/-495 lines since 6 months ago
  • high complexity — src/app/actions/sumsub.ts — CC 32, MI 54.69, SLOC 145
  • high complexity — src/hooks/useHostedVerification.ts — CC 30, MI 64.13, SLOC 132
  • high complexity — src/components/Card/ApplicationStatusScreen.tsx — CC 17, MI 49.72, SLOC 47
  • medium high-mdd — src/app/(mobile-ui)/card/page.tsx:58 — CardPage: MDD 182.1 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/card/page.tsx:58 — CardPage: DLT 64 (calls 64 distinct functions — high context load)
  • medium high-mdd — src/hooks/useHostedVerification.ts:25 — useHostedVerification: MDD 38.3 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useHostedVerification.ts:38 — : MDD 26.0 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/card/page.tsx:615 — renderState: MDD 23.9 (uses across many lines from declarations)
  • medium method-complexity — src/app/(mobile-ui)/card/page.tsx:615 — CC 22 SLOC 59
  • medium high-mdd — src/components/Card/ApplicationStatusScreen.tsx:66 — ApplicationStatusScreen: MDD 20.7 (uses across many lines from declarations)
  • medium method-complexity — src/components/Card/ApplicationStatusScreen.tsx:66 — CC 16 SLOC 23
  • medium complexity — src/components/Kyc/AdditionalVerificationView.tsx — CC 9, MI 61.22, SLOC 43
  • medium react-effect-derives-state — src/app/(mobile-ui)/card/page.tsx:290 — small useEffect that only sets state from deps
  • low high-dlt — src/hooks/useHostedVerification.ts:25 — useHostedVerification: DLT 22 (calls 22 distinct functions — high context load)
  • low high-mdd — src/app/(mobile-ui)/card/page.tsx:470 — : MDD 16.2 (uses across many lines from declarations)
  • low high-mdd — src/app/(mobile-ui)/card/page.tsx:300 — : MDD 14.3 (uses across many lines from declarations)
  • low high-mdd — src/hooks/useHostedVerification.ts:144 — : MDD 10.0 (uses across many lines from declarations)
  • low structural-dup — app/actions/sumsub.ts:189 — 9 duplicate lines / 50 tokens with app/actions/sumsub.ts:225

✅ Resolved (18)

  • src/app/(mobile-ui)/card/page.tsx — CC 138, MI 56.06, SLOC 530
  • src/app/(mobile-ui)/card/page.tsx — 49 commits, +1131/-494 lines since 6 months ago
  • src/app/actions/sumsub.ts — CC 32, MI 54.71, SLOC 145
  • src/app/(mobile-ui)/card/page.tsx:57 — CardPage: MDD 178.6 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:57 — CardPage: DLT 62 (calls 62 distinct functions — high context load)
  • src/hooks/useBridgeHostedVerification.ts:23 — useBridgeHostedVerification: MDD 30.4 (uses across many lines from declarations)
  • src/hooks/useBridgeHostedVerification.ts — CC 29, MI 65.2, SLOC 125
  • src/hooks/useBridgeHostedVerification.ts:29 — : MDD 26.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:603 — renderState: MDD 23.5 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:603 — CC 22 SLOC 59
  • src/components/Card/ApplicationStatusScreen.tsx — CC 14, MI 50.58, SLOC 45
  • src/components/Kyc/AdditionalVerificationView.tsx — CC 9, MI 61.25, SLOC 43
  • src/app/(mobile-ui)/card/page.tsx:278 — small useEffect that only sets state from deps
  • src/hooks/useBridgeHostedVerification.ts:23 — useBridgeHostedVerification: DLT 21 (calls 21 distinct functions — high context load)
  • src/components/Card/ApplicationStatusScreen.tsx:62 — ApplicationStatusScreen: MDD 18.6 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:458 — : MDD 16.2 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:288 — : MDD 14.3 (uses across many lines from declarations)
  • src/hooks/useBridgeHostedVerification.ts:129 — : MDD 10.0 (uses across many lines from declarations)

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useHostedVerification.ts 0.0 5.7 +5.7
src/hooks/useBridgeHostedVerification.ts 5.8 0.0 -5.8

@github-actions

github-actions Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 4995 ran, 0 failed, 0 skipped, 1.7m

📊 Coverage (unit)

metric %
statements 73.5%
branches 58.8%
functions 64.7%
lines 74.5%
⏱ 10 slowest test cases
time test
4.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.9s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › prefers the server value over a stale device mirror
1.8s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › the change cooldown shows its date and blocks changing to another country, not re-saving
1.8s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › saves the declared residence, refetches, and closes
1.8s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › swaps from the server value with no device mirror at all
1.6s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › moving to a country in neither slot leaves the second document alone
1.4s src/components/Setup/Views/__tests__/Residence.test.tsx › reveals the second selector via the multi-doc link
1.1s src/components/Setup/Views/__tests__/Residence.test.tsx › shows the partial heads-up for UA (card restriction) and continues on demand
1.1s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › promoting the second document country swaps the pair instead of dropping one
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — no blocking findings — this is not an approval

The Rain-hosted action is correctly gated and routed. One non-blocking launch re-entry race remains on the card CTA.

Findings

  • MINOR · src/app/(mobile-ui)/card/page.tsx:283 · Disable the Rain CTA while its launch is pending
    The card path ignores the hook's isStarting state. On Capacitor, a fast second tap while /users/kyc/start-action is pending invokes start() twice; because start() has no synchronous re-entry guard, both responses can call Browser.open and produce duplicate portal opens. Thread isStarting through as a disabled/loading prop, or guard start() with a ref before opening.

Checked clean

  • Pinned head SHA, exact base SHA, and merge base all matched the supplied values.
  • Capability lookup only exposes the CTA for the Rain rail's blocking rain-hosted action, matching the paired API contract.
  • Hosted URL launch covers popup reservation, same-tab fallback, native browser handoff, friendly errors, reverse-tabnabbing protection, and return refetch behavior.
  • Exact-head CI reported successful format, typecheck, eslint, unit, analyze, deploy-preview, and aggregate ci-success checks.
  • Local targeted Jest execution was unavailable because the detached worktree has no node_modules; the exact-head unit check passed in CI.

Second opinion skipped: openrouter-unparseable-reply.

Exact head: af228731444b · Context: repo, paired-api

Comment thread src/app/(mobile-ui)/card/page.tsx
Chip: a fast second tap re-enters start() before isStarting (React state,
set a tick later) disables anything, reserving/opening a second portal tab.
Guard start() with a ref set in the same tick and reset in finally — covers
every caller, matching the poaStartingRef pattern on the PoA CTA.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — no blocking findings — this is not an approval

The Rain identity re-upload CTA and hosted handoff match the paired API contract. The prior duplicate-launch race is fixed by a synchronous re-entry guard.

Checked clean

  • Pinned head SHA, exact base SHA, and merge base all matched the supplied values.
  • The previous fast-double-tap finding is fixed: startingRef is set synchronously before tab reservation or native launch and reset on every exit path.
  • The Rain rail exposes the CTA only when its blocking action resolves to rain-hosted, and the shared action posts the exact authorized key expected by paired API PR 1482.
  • Hosted launch behavior covers popup reservation, same-tab fallback, native in-app browser handoff, friendly failures, reverse-tabnabbing protection, and return refetches.
  • The Bridge hosted-verification caller was migrated without changing its action key or loading/error behavior, and no old hook/action references remain.
  • All three locale JSON files parse and contain the new card.uploadIdentityDocuments string.
  • Exact-head CI reported successful unit, typecheck, eslint, format, analyze, deploy-preview, and aggregate ci-success checks; ds-shots was still in progress. Local tests were unavailable because the detached worktree has no node_modules.

Second opinion skipped: openrouter-timeout.

Exact head: b6140b10fc06 · Context: repo, paired-api

@innolope-dev
innolope-dev merged commit 156fc14 into dev Sep 1, 2026
23 checks passed

This branch was successfully deployed

1 active deployment
Preview — b6140b10 Deployed Sep 1, 2026 by vercel[bot]
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.

1 participant