Skip to content

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

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

feat(support): surface live verification state to Crisp agents#2487
innolope-dev wants to merge 4 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 Aug 17, 2026 6:44pm

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: 7096.03 → 7104.02 (+7.99)
Findings: +3 net (+13 new, -10 resolved)

🆕 New findings (13)

  • critical complexity — src/components/Global/SupportDrawer/index.tsx — CC 65, MI 59.49, SLOC 191
  • high complexity — src/utils/crisp.ts — CC 34, MI 59.77, SLOC 69
  • medium high-mdd — src/components/Global/SupportDrawer/index.tsx:26 — SupportDrawer: MDD 95.2 (uses across many lines from declarations)
  • medium high-mdd — src/utils/crisp.ts:17 — setCrispUserData: MDD 25.2 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/SupportDrawer/index.tsx:189 — : MDD 22.5 (uses across many lines from declarations)
  • medium method-complexity — src/utils/crisp.ts:17 — setCrispUserData CC 22 SLOC 28
  • medium method-complexity — src/components/Global/SupportDrawer/index.tsx:120 — 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:189 — useEffect with empty deps + setState — derived state anti-pattern
  • 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/utils/support-verification.ts:31 — buildSupportVerificationSummary: MDD 10.9 (uses across many lines from declarations)

✅ Resolved (10)

  • src/components/Global/SupportDrawer/index.tsx — CC 58, MI 60.86, SLOC 173
  • src/components/Global/SupportDrawer/index.tsx:26 — SupportDrawer: MDD 89.4 (uses across many lines from declarations)
  • src/utils/crisp.ts — CC 27, MI 61.36, SLOC 63
  • src/components/Global/SupportDrawer/index.tsx:177 — : MDD 22.5 (uses across many lines from declarations)
  • src/utils/crisp.ts:17 — setCrispUserData: MDD 20.5 (uses across many lines from declarations)
  • src/utils/crisp.ts:17 — setCrispUserData CC 15 SLOC 22
  • src/hooks/useCrispUserData.ts — CC 11, MI 64.14, SLOC 28
  • src/components/Global/SupportDrawer/index.tsx:177 — useEffect with empty deps + setState — derived state anti-pattern
  • 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)

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/support-verification.ts 0.0 5.0 +5.0
src/hooks/useCrispUserData.ts 7.0 8.2 +1.2
src/utils/crisp.ts 7.4 8.1 +0.7
src/components/Global/SupportDrawer/index.tsx 12.3 12.9 +0.7

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2940 ran, 0 failed, 0 skipped, 52.1s

📊 Coverage (unit)

metric %
statements 66.3%
branches 51.3%
functions 56.5%
lines 67.1%
⏱ 10 slowest test cases
time test
4.0s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s 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.test.ts › should include Content-Type in validateInviteCode
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/utils/__tests__/auth-token.test.ts › authReady does not park — hydrates the plain token without an unlock
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
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/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
📍 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.

main moved the Crisp proxy off the URL transport onto a postMessage
handshake (PR #2669, useCrispProxyUrl deleted) and routes the iframe
through setCrispUserData, so the verification fields this branch added
to crisp-proxy/page.tsx and useCrispProxyUrl are now duplicated by the
utils/crisp.ts mapping, which survives the merge unchanged.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

@kushagrasarathe heads up — main moved under this PR and it went conflicted, so I've updated the branch (c3db567). That dismissed your approval; would appreciate a re-review when you have a moment.

What changed: #2669 moved the Crisp proxy off the URL transport onto the postMessage handshake — it deleted useCrispProxyUrl.ts and rewrote crisp-proxy/page.tsx to route identity through setCrispUserData. This branch had been adding the six verification fields in three places (utils/crisp.ts, crisp-proxy/page.tsx, useCrispProxyUrl.ts), and the latter two are exactly the duplication #2669 removed — so the new commit reverts them to their pre-branch state. The single mapping in utils/crisp.ts survives and now feeds both the proxy iframe and the main-window widget: agents still get every field, via one code path instead of three.

Nothing of main's was touched, and the review feedback from the earlier rounds is untouched.

Checks: typecheck, unit, format, e2e and both preview deploys are green on the merge ref. Three reds, none from this change:

  • eslint — fails on the push run, passes on the pull_request run at the same SHA. Those are branch-tip-alone errors (no-explicit-any in utils/crisp.ts, the @/interfaces barrel import in useCrispUserData.ts) that main has already fixed, so the merged state is clean.
  • Analyze (actions) — CodeQL died on a GitHub outage (Encountered an error while trying to determine feature enablement: HttpError: No server is currently available), not on code. It's default-setup so it refuses a manual rerun; the next push will re-trigger it.
  • approve-and-merge — the dismissed approval above.

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