Skip to content

feat(support): surface live verification state to Crisp agents - #2487

Open
innolope-dev wants to merge 3 commits into
mainfrom
feat/crisp-support-verification-state
Open

feat(support): surface live verification state to Crisp agents#2487
innolope-dev wants to merge 3 commits into
mainfrom
feat/crisp-support-verification-state

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Problem

Support agents have no visibility into a user's live verification state. In a recent case, three agents took turns guessing what state a user was in because none of it reaches Crisp — today we only send username, email, wallet, Bridge and PostHog links, nothing about where the user is stuck.

Closes #2360.

What this does

Adds a support-facing verification snapshot to the Crisp agent sidebar (session:data). Everything is derived from the two backend read-models already on the /get-user response (capabilities, identityVerification) — no backend change, no new provider-state interpretation on the client. New fields visible to agents:

field source
identity_status identityVerification.status
email_on_file whether an email is on file (provider submission can't run without one)
verification_gates per-op gate kinds via existing deriveGate, e.g. pay:ready deposit:provide-email withdraw:blocked-rejection
failure_reason the stuck rail's id + normalized reason.code + technical details
pending_actions capabilities.nextActions as kind(purpose)

It's threaded through all three Crisp sinks: web widget (setCrispUserData), proxy iframe (useCrispProxyUrlcrisp-proxy), and native Capacitor (SupportDrawer).

Design notes

  • Sidebar only — the user's own message is never modified. An earlier revision also appended a state line to the support message text, but message:text populates the user-visible composer, which would expose internal codes/rail-ids to the user. Dropped in favour of the agent-only sidebar, which already carries the full state.
  • Logic lives in a pure, unit-tested buildSupportVerificationSummary() (reuses deriveGate / railVerdict — no duplicated state-machine logic).

Out of scope (filed separately)

  • The issue's mantecaUserId "while here" needs the backend to expose a provider-account id first (the capability model is deliberately provider-blind) — can't be done FE-only.
  • The longer-term read-only "shadow view" is a genuine feature, not part of this fix.

Testing

  • New support-verification.test.ts (5 cases: gates, stuck-rail failure reason, pending actions, graceful degrade, identity status).
  • tsc --noEmit clean, existing SupportDrawer suite green, prettier clean.

Summary by CodeRabbit

  • New Features
    • Support conversations now include richer live verification context: identity status, whether an email is on file, verification gates/rails, failure details, and pending actions.
    • Enhanced the Crisp support experience so identity/verification metadata is carried through when opening the messenger.
  • Bug Fixes
    • Prevents stale verification data by resetting missing fields and keeping the context in sync as user details change.
  • Tests
    • Added test coverage for verified, blocked/fixable, pending-action, and incomplete verification scenarios.

Support agents had no visibility into where a user was stuck in
verification, so they resorted to guessing. Derive a support-facing
snapshot from the capability + identity read-models already on the
/get-user response and push it into Crisp session:data (agent sidebar):
identity status, email-on-file, per-operation gate kinds, the stuck
rail's failure reason + technical details, and pending next-actions.

Threaded through all Crisp sinks (web widget, proxy iframe, native
Capacitor). The user's own message is never modified.

Closes #2360
@innolope-dev innolope-dev self-assigned this Jul 23, 2026
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 29, 2026 3:28pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02d4840d-cb42-488e-8d6b-eeb8567a2e37

📥 Commits

Reviewing files that changed from the base of the PR and between 480fc8f and 3397091.

📒 Files selected for processing (1)
  • src/hooks/useCrispUserData.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useCrispUserData.ts

📝 Walkthrough

Walkthrough

The PR derives live verification context, propagates it through Crisp user data and proxy URLs, and sends it to web and native Crisp support sessions.

Changes

Crisp verification context

Layer / File(s) Summary
Build verification summary
src/utils/support-verification.ts, src/utils/__tests__/support-verification.test.ts
Derives identity status, email presence, verification gates, failure details, and pending actions, with tests for populated and missing inputs.
Propagate verification data
src/hooks/useCrispUserData.ts, src/hooks/useCrispProxyUrl.ts
Adds the derived fields to Crisp user data and recomputes proxy URLs when they change.
Send Crisp session metadata
src/utils/crisp.ts, src/app/crisp-proxy/page.tsx, src/components/Global/SupportDrawer/index.tsx
Sends verification fields through web session metadata and native Crisp custom data.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UserDataHook
  participant ProxyUrlHook
  participant CrispProxy
  participant CrispMessenger
  UserDataHook->>ProxyUrlHook: verification summary fields
  ProxyUrlHook->>CrispProxy: session_data query parameter
  CrispProxy->>CrispMessenger: session:data metadata
  UserDataHook->>CrispMessenger: native custom data
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: jjramirezn

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 is concise and accurately reflects the main change: exposing live verification state to Crisp agents.
Linked Issues check ✅ Passed The PR adds the requested live verification snapshot to Crisp across web, proxy, and native flows, covering gates, failure reason, email status, identity status, and pending actions.
Out of Scope Changes check ✅ Passed All changes are focused on surfacing verification state to Crisp and related tests; no unrelated scope creep is evident.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/crisp-support-verification-state

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


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

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6306.66 → 6316.31 (+9.65)
Findings: +3 net (+22 new, -19 resolved)

🆕 New findings (22)

  • critical complexity — src/components/Global/SupportDrawer/index.tsx — CC 58, MI 60.81, SLOC 147
  • high complexity — src/app/crisp-proxy/page.tsx — CC 47, MI 60.94, SLOC 120
  • high complexity — src/utils/crisp.ts — CC 34, MI 59.77, SLOC 69
  • high complexity — src/hooks/useCrispProxyUrl.ts — CC 33, MI 55.87, SLOC 35
  • high method-complexity — src/hooks/useCrispProxyUrl.ts:17 — CC 31 SLOC 31
  • medium high-mdd — src/components/Global/SupportDrawer/index.tsx:15 — SupportDrawer: MDD 57.9 (uses across many lines from declarations)
  • medium high-mdd — src/app/crisp-proxy/page.tsx:21 — : MDD 38.0 (uses across many lines from declarations)
  • medium high-mdd — src/app/crisp-proxy/page.tsx:18 — CrispProxyContent: MDD 37.1 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useCrispProxyUrl.ts:16 — useCrispProxyUrl: MDD 34.5 (uses across many lines from declarations)
  • medium high-mdd — src/utils/crisp.ts:15 — setCrispUserData: MDD 28.3 (uses across many lines from declarations)
  • medium method-complexity — src/app/crisp-proxy/page.tsx:64 — CC 22 SLOC 34
  • medium method-complexity — src/utils/crisp.ts:15 — setCrispUserData CC 22 SLOC 28
  • medium method-complexity — src/components/Global/SupportDrawer/index.tsx:65 — CC 18 SLOC 39
  • medium complexity — src/utils/support-verification.ts — CC 15, MI 64.46, SLOC 60
  • medium complexity — src/hooks/useCrispUserData.ts — CC 12, MI 60.7, SLOC 37
  • medium react-effect-derives-state — src/components/Global/SupportDrawer/index.tsx:132 — useEffect with empty deps + setState — derived state anti-pattern
  • low high-mdd — src/hooks/useCrispProxyUrl.ts:17 — : MDD 18.4 (uses across many lines from declarations)
  • low high-mdd — src/hooks/useCrispUserData.ts:31 — useCrispUserData: MDD 13.7 (uses across many lines from declarations)
  • low high-mdd — src/hooks/useCrispUserData.ts:34 — : MDD 13.7 (uses across many lines from declarations)
  • low high-mdd — src/components/Global/SupportDrawer/index.tsx:132 — : MDD 11.5 (uses across many lines from declarations)

…and 2 more.

✅ Resolved (19)

  • src/components/Global/SupportDrawer/index.tsx — CC 51, MI 62.6, SLOC 129
  • src/app/crisp-proxy/page.tsx — CC 41, MI 61.23, SLOC 120
  • src/components/Global/SupportDrawer/index.tsx:15 — SupportDrawer: MDD 52.7 (uses across many lines from declarations)
  • src/app/crisp-proxy/page.tsx:21 — : MDD 40.1 (uses across many lines from declarations)
  • src/app/crisp-proxy/page.tsx:18 — CrispProxyContent: MDD 38.7 (uses across many lines from declarations)
  • src/hooks/useCrispProxyUrl.ts:16 — useCrispProxyUrl: MDD 27.6 (uses across many lines from declarations)
  • src/utils/crisp.ts — CC 27, MI 61.35, SLOC 63
  • src/hooks/useCrispProxyUrl.ts — CC 26, MI 58.52, SLOC 29
  • src/hooks/useCrispProxyUrl.ts:17 — CC 24 SLOC 25
  • src/utils/crisp.ts:15 — setCrispUserData: MDD 22.3 (uses across many lines from declarations)
  • src/app/crisp-proxy/page.tsx:64 — CC 16 SLOC 34
  • src/utils/crisp.ts:15 — setCrispUserData CC 15 SLOC 22
  • src/hooks/useCrispUserData.ts — CC 11, MI 64.14, SLOC 28
  • src/components/Global/SupportDrawer/index.tsx:120 — useEffect with empty deps + setState — derived state anti-pattern
  • src/hooks/useCrispProxyUrl.ts:17 — : MDD 16.0 (uses across many lines from declarations)
  • src/components/Global/SupportDrawer/index.tsx:120 — : MDD 11.5 (uses across many lines from declarations)
  • src/hooks/useCrispUserData.ts:23 — useCrispUserData: MDD 10.3 (uses across many lines from declarations)
  • src/hooks/useCrispUserData.ts:26 — : MDD 10.3 (uses across many lines from declarations)
  • src/app/crisp-proxy/page.tsx:211 — CrispProxyPage: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/support-verification.ts 0.0 5.0 +5.0
src/hooks/useCrispUserData.ts 6.5 7.8 +1.3
src/hooks/useCrispProxyUrl.ts 9.6 10.8 +1.2
src/utils/crisp.ts 7.6 8.3 +0.8
src/components/Global/SupportDrawer/index.tsx 10.3 11.0 +0.7

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2241 ran, 0 failed, 0 skipped, 36.3s

📊 Coverage (unit)

metric %
statements 61.8%
branches 45.2%
functions 51.2%
lines 62.2%
⏱ 10 slowest test cases
time test
3.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.2s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
0.2s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
0.2s src/utils/__tests__/demo-balance.test.ts › starts at the full balance on a fresh install and stamps a timestamp
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Global/SupportDrawer/index.tsx`:
- Around line 84-99: Update the CapacitorCrisp metadata handling in the support
drawer so verification_gates, failure_reason, and pending_actions are always
written, using an empty string when their corresponding userData values are
absent. Preserve the existing values when present and align this native behavior
with the web/proxy paths.

In `@src/utils/support-verification.ts`:
- Around line 41-42: Update the gate summary construction around
SUMMARY_OPERATIONS, deriveGate, and gates to include the rail selected by the
same gate-resolution ordering alongside each operation and kind. Preserve
existing formatting for other fields, ensure pending and waiting-on-provider
gates expose the selected rail even without a failureReason, and add coverage
for this stuck-gate state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b8f84b8-8bb1-4adc-a88a-1653f31c537d

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd5969 and 0b2f673.

📒 Files selected for processing (7)
  • src/app/crisp-proxy/page.tsx
  • src/components/Global/SupportDrawer/index.tsx
  • src/hooks/useCrispProxyUrl.ts
  • src/hooks/useCrispUserData.ts
  • src/utils/__tests__/support-verification.test.ts
  • src/utils/crisp.ts
  • src/utils/support-verification.ts

Comment thread src/components/Global/SupportDrawer/index.tsx Outdated
Comment thread src/utils/support-verification.ts Outdated
…tive fields

Address CodeRabbit review on #2487:
- add verification_rails (each non-enabled rail as id:status(code)) so agents
  can identify WHICH rail is stuck even for pending/waiting gates, where there
  is no failure reason to fall back on.
- native Capacitor path now always writes the verification fields (empty string
  when absent), matching the web/proxy sinks so a prior user's values can't
  linger on the device-local Crisp session.

@kushagrasarathe kushagrasarathe 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.

Reviewed — SAFE. Approving.

No raw PII crosses to Crisp — the payload is status enums, gate tokens, backend-controlled failure/reason strings, and a yes/no email_on_file boolean (not the address itself). The surfaced set is minimized and gated. Gives support the verification state they need without leaking user data.

No blocking issues.

@kushagrasarathe

Copy link
Copy Markdown
Contributor

@innolope-dev some merge conflicts

main rewrote the '@/constants/support' import in place (dropping the
decommissioned Grafana link). This branch's only change there was an
adjacent added line, which git could not merge without a conflict.
Moving it clear of that hunk lets the two merge automatically.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

@Hugo0 — heads-up on a repo-rule interaction, unrelated to this PR's content.

5008b3ef ("Update content submodule to latest main", 2026-07-28) landed on main unsigned — it's the only unsigned commit in the last 60 there. The org-level "All branches" ruleset applies required_signatures to refs/heads/**, and that check re-evaluates a commit as new to the ref when it first enters a branch. So merging main into any feature branch now fails:

remote: error: GH013: Repository rule violations found for refs/heads/<branch>
remote: - Commits must have verified signatures.
remote:   Found 1 violation:
remote:   5008b3efb502f47b6abe5b15e227e8b8dffe27d4

The usual escape hatches are closed too:

  • the same ruleset sets non_fast_forward, so rebase-and-force-push is rejected as well;
  • PUT /repos/.../pulls/{n}/update-branch (the "Update branch" button) returns 422 merge conflict between base and head whenever there's a real conflict, so it can't do it either.

Net effect: while that commit sits on main, no branch can absorb main. I resolved this PR's conflict by restructuring the branch so it stopped conflicting with main at all, rather than merging — but that only works when the conflict is trivially avoidable, which it won't always be.

Two things that would help:

  1. Re-sign or replace 5008b3ef on main.
  2. Check whether the automation that produces these "Update content submodule" commits can sign them — otherwise the next one re-blocks everyone.

Happy to be wrong about the cause if the ruleset is configured differently than it reads from the API.

@kushagrasarathe

Copy link
Copy Markdown
Contributor

PR Review — advisory verdict

Full diff read. Security-focused given this pushes verification state into support tooling.

TIER: T2 (support/PII surface; FE-only, no new endpoint/auth, reuses existing $crisp session:data pattern)
PAIRED PR: none — the only BE-needing item (mantecaUserId) is explicitly deferred; everything else derives from fields already on /get-user.
A. BREAKAGE:  pass — new fields thread identically through all 3 Crisp sinks; gateState shape matches CapabilityState; absent-field writes '' everywhere (guards the documented cross-user-bleed class). typecheck+unit green.
B. PERF:      pass — pure sync derivation in an existing useMemo; NO new network/Sentry/log emission, no external Crisp API call (client-side session:data only).
C. QUALITY:   pass — isolated in unit-tested pure fn (5 cases), reuses deriveGate/railVerdict, no dup state machine. 246 lines, tight scope.
D. SECURITY:  pass — data is the authenticated session user's OWN data (useAuth), not a request-body id; session:data is agent-sidebar-only (the earlier message:text variant that would leak to users was deliberately dropped); new fields are strictly LESS sensitive than the bridge/manteca ids already on that channel.
CI: green on typecheck/unit/format/e2e/CodeRabbit/Vercel; eslint red is pre-existing add-money lint debt in untouched files.
VERDICT: approve-advisory

Non-blocking: confirm the repo's merge-on-green policy tolerates the amber eslint (as prior PRs did), else it may block auto-merge.

🤖 AI advisory review (pr-review rulebook). Task-link gate waived per owner — external-dev series.

@kushagrasarathe kushagrasarathe 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.

Approving — no blocking findings; FE-only, session-user own data, session:data is agent-sidebar-only, new fields less sensitive than existing channel ids.

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.

Support can't see a user's live verification state, so they end up guessing

2 participants