Skip to content

feat(analytics): fire eea_uplift funnel on modal open, cover all remediation#2332

Open
kushagrasarathe wants to merge 2 commits into
devfrom
analytics/eea-uplift-modal-open
Open

feat(analytics): fire eea_uplift funnel on modal open, cover all remediation#2332
kushagrasarathe wants to merge 2 commits into
devfrom
analytics/eea-uplift-modal-open

Conversation

@kushagrasarathe

Copy link
Copy Markdown
Contributor

Summary

Makes the eea_uplift_started / eea_uplift_completed funnel reliable for watching who attempts the EEA uplift and whether they finish — so session recordings can be filtered by the event. Two gaps fixed:

  1. Fired on the modal CTA, not on open — anyone who opened the uplift modal and abandoned emitted nothing. Now fires at modal-open.
  2. Missed the urgent cohort entirely — post-2026-06-29-cliff, the urgent uplift is no longer a future-dated advisory; the BE reclassifies it to blocking (fixable-rejection → KYC modal), a path that called no tracking. Now covered.

Fires across both remediation paths and both channels (deposit + withdraw):

  • urgent / blockinggate.reason.code starts with eea_uplift (BE sets reason code to the questionnaire cluster).
  • upcoming / advisoryrequirementKey ∈ {sof_individual_primary_purpose, has_foreign_tax_registration, place_of_birth_missing, nationalities} — derived from prod remediation data (172 EEA-affected users).

Event now carries source (advisory|blocking), urgent (blocking = past the cliff = urgent), and effective_date, so urgent vs upcoming (dob-type) split directly in PostHog.

Excluded proof_of_address_document / government_id_expired: they co-occur on the cliff but are separate remediation (cluster null) — keying them to eea would over-fire on ordinary doc rejections for non-EEA users.

Risks

  • Analytics-only; no user-visible behavior change. No BE change.
  • Firing moved from CTA to modal-open ⇒ started counts rise (now includes abandoners) — that's the intent. completed still latches on a recorded start, so completion rate stays meaningful.
  • Slight limitation: a blocking uplift whose reason resolves to document_rejected (freeform classifier) instead of eea_uplift wouldn't be tagged — best signal available FE-side without a BE flag.

QA

…diation

The eea_uplift_started event fired only on the advisory modal's 'Complete
now' CTA, so it missed (a) users who open the uplift modal and abandon, and
(b) the entire urgent post-cliff cohort, whose remediation is now blocking
(fixable-rejection → KYC modal) rather than a future-dated advisory.

Fire at modal-open instead, across both remediation paths and both channels
(deposit + withdraw):
- blocking/urgent: gate.reason.code === 'eea_uplift*' (BE sets it to the
  questionnaire cluster).
- advisory/upcoming: requirementKey in the eea_uplift set (sof_individual_
  primary_purpose, has_foreign_tax_registration, place_of_birth_missing,
  nationalities) — derived from prod remediation data.

Event now carries source + urgent + effective_date so urgent vs upcoming
(dob-type) split in PostHog. PoA / gov-id-expired co-occur on the cliff but
are excluded (separate remediation) to avoid over-firing on generic doc
rejections for non-EEA users.
@vercel

vercel Bot commented Jul 2, 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 2, 2026 4:00pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2fb4c8d4-2313-4e59-b237-2c3eaa2b2def

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

Walkthrough

Adds a shared EEA uplift trigger utility, refactors funnel tracking to use typed trigger snapshots, and updates add-money and withdraw bank pages to derive uplift start events from gate or advisory state while resetting funnel state on modal dismissal paths.

Changes

EEA Uplift Funnel Analytics

Layer / File(s) Summary
Uplift trigger helpers
src/utils/eea-uplift.utils.ts, src/utils/eea-uplift.utils.test.ts
Defines uplift requirement keys, a shared trigger type, and helper functions for blocking and advisory trigger detection, with tests covering matching and non-matching inputs.
Hook event snapshot and tests
src/hooks/useEeaUpliftFunnel.ts, src/hooks/useEeaUpliftFunnel.test.ts
Stores uplift start triggers in the funnel hook, deduplicates start emission, maps start/completed analytics from the same trigger snapshot, and updates tests for the new payload shape and reset behavior.
Add-money bank page wiring
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Derives uplift triggers from gate and advisory state, stops start tracking from the advisory completion path, and clears uplift funnel state on modal close and support handoff.
Withdraw bank page wiring
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Derives uplift triggers from gate and advisory state, stops start tracking from the advisory completion path, and clears uplift funnel state on modal close and support handoff.

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

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: jjramirezn

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: firing the EEA uplift funnel on modal open across remediation paths.
Description check ✅ Passed The description is detailed and directly matches the analytics funnel changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5858.65 → 5864.24 (+5.59)
Findings: 0 net (+29 new, -29 resolved)

🆕 New findings (29)

  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 101, MI 54.64, SLOC 347
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 96, MI 57.54, SLOC 347
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 51 commits, +560/-441 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 47 commits, +489/-222 lines since 6 months ago
  • medium react-long-component — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage is 532 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:50 — OnrampBankPage is 467 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage: MDD 150.4 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:50 — OnrampBankPage: MDD 140.4 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage: DLT 78 (calls 78 distinct functions — high context load)
  • medium high-dlt — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:50 — OnrampBankPage: DLT 68 (calls 68 distinct functions — high context load)
  • medium method-complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:50 — OnrampBankPage CC 29 SLOC 165
  • medium method-complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage CC 28 SLOC 129
  • medium method-complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:214 — CC 19 SLOC 107
  • medium react-effect-derives-state — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:156 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:210 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:347 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:132 — small useEffect that only sets state from deps
  • low high-dlt — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:214 — proceedWithOfframp: DLT 20 (calls 20 distinct functions — high context load)
  • low structural-dup — app/(mobile-ui)/add-money/[country]/bank/page.tsx:134 — 19 duplicate lines / 70 tokens with app/(mobile-ui)/withdraw/[country]/bank/page.tsx:110
  • low structural-dup — app/(mobile-ui)/add-money/[country]/bank/page.tsx:258 — 17 duplicate lines / 61 tokens with app/(mobile-ui)/withdraw/[country]/bank/page.tsx:215

…and 9 more.

✅ Resolved (29)

  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 99, MI 54.88, SLOC 340
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 94, MI 57.82, SLOC 339
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 49 commits, +526/-423 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 45 commits, +451/-202 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage is 515 lines — split it
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage is 452 lines — split it
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage: MDD 149.6 (uses across many lines from declarations)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage: MDD 139.3 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage: DLT 75 (calls 75 distinct functions — high context load)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage: DLT 65 (calls 65 distinct functions — high context load)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage CC 29 SLOC 165
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage CC 28 SLOC 129
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:212 — CC 18 SLOC 103
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:154 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:208 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:337 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:130 — small useEffect that only sets state from deps
  • app/(mobile-ui)/add-money/[country]/bank/page.tsx:133 — 18 duplicate lines / 74 tokens with app/(mobile-ui)/withdraw/[country]/bank/page.tsx:109
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:212 — proceedWithOfframp: DLT 18 (calls 18 distinct functions — high context load)
  • app/(mobile-ui)/add-money/[country]/bank/page.tsx:455 — 16 duplicate lines / 91 tokens with components/Claim/Link/views/BankFlowManager.view.tsx:610

…and 9 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/eea-uplift.utils.ts 0.0 4.4 +4.4

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1656 ran, 0 failed, 0 skipped, 24.9s

📊 Coverage (unit)

metric %
statements 54.7%
branches 37.5%
functions 42.5%
lines 54.6%
⏱ 10 slowest test cases
time test
3.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
0.1s src/services/__tests__/resolveClaimLink.test.ts › restores the pristine password after a redirect mangles the fragment
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 2, 2026

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

🧹 Nitpick comments (2)
src/hooks/useEeaUpliftFunnel.ts (1)

13-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a discriminated union on source for stronger guarantees.

UpliftStartTrigger makes requirementKey/actionKey/effectiveDate optional for both sources, so nothing at compile time stops an advisory trigger from missing effectiveDate (which the PR intends to always carry for the advisory/upcoming path) or catches a caller passing effectiveDate on a blocking trigger where it's meaningless. A discriminated union would let TS enforce the field combinations documented in the JSDoc.

export type UpliftStartTrigger =
    | { source: 'advisory'; requirementKey: string; actionKey: string; effectiveDate: string }
    | { source: 'blocking'; requirementKey?: string }

Not blocking — current shape works fine at runtime given the callers observed in the stack.

🤖 Prompt for 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.

In `@src/hooks/useEeaUpliftFunnel.ts` around lines 13 - 18, The UpliftStartTrigger
shape in useEeaUpliftFunnel should be tightened into a discriminated union keyed
by source so TypeScript enforces the intended field combinations. Update the
UpliftStartTrigger type to separate the advisory and blocking variants, making
requirementKey/actionKey/effectiveDate required for advisory and omitting
effectiveDate for blocking, then adjust any nearby usages in useEeaUpliftFunnel
to match the new source-based narrowing.
src/utils/eea-uplift.utils.ts (1)

31-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type assertion bypasses GateState's discriminated union.

(gate as { reason?: { code?: string } }) sidesteps the fact that several GateState variants (loading, ready, pending, needs-identity, needs-enrollment) don't declare a reason field at all, and it silently accepts any string for code regardless of CapabilityReason's actual shape. A narrowing check preserves the same runtime behavior while keeping type safety.

♻️ Suggested refactor
 export function eeaUpliftReasonCode(gate: GateState): string | undefined {
-    const code = (gate as { reason?: { code?: string } }).reason?.code
-    return code?.startsWith('eea_uplift') ? code : undefined
+    const code = 'reason' in gate ? gate.reason?.code : undefined
+    return code?.startsWith('eea_uplift') ? code : undefined
 }
🤖 Prompt for 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.

In `@src/utils/eea-uplift.utils.ts` around lines 31 - 34, The eeaUpliftReasonCode
helper is bypassing GateState’s discriminated union with an unsafe type
assertion. Update eeaUpliftReasonCode to use proper narrowing on gate instead of
casting to { reason?: { code?: string } }, and only read reason.code after
confirming the variant actually exposes reason. Keep the same runtime behavior
for eea_uplift-prefixed codes, but align the types with GateState and
CapabilityReason so the function remains safe without assertions.
🤖 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.

Nitpick comments:
In `@src/hooks/useEeaUpliftFunnel.ts`:
- Around line 13-18: The UpliftStartTrigger shape in useEeaUpliftFunnel should
be tightened into a discriminated union keyed by source so TypeScript enforces
the intended field combinations. Update the UpliftStartTrigger type to separate
the advisory and blocking variants, making
requirementKey/actionKey/effectiveDate required for advisory and omitting
effectiveDate for blocking, then adjust any nearby usages in useEeaUpliftFunnel
to match the new source-based narrowing.

In `@src/utils/eea-uplift.utils.ts`:
- Around line 31-34: The eeaUpliftReasonCode helper is bypassing GateState’s
discriminated union with an unsafe type assertion. Update eeaUpliftReasonCode to
use proper narrowing on gate instead of casting to { reason?: { code?: string }
}, and only read reason.code after confirming the variant actually exposes
reason. Keep the same runtime behavior for eea_uplift-prefixed codes, but align
the types with GateState and CapabilityReason so the function remains safe
without assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2933f7b-e51a-4255-8ada-2ee82ba42c2e

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7741c and dce0c3b.

📒 Files selected for processing (6)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/hooks/useEeaUpliftFunnel.test.ts
  • src/hooks/useEeaUpliftFunnel.ts
  • src/utils/eea-uplift.utils.test.ts
  • src/utils/eea-uplift.utils.ts

Addresses code-review findings on the eea_uplift funnel:
- phantom completions: the blocking path arms the started latch, but the
  InitiateKycModal onClose/onContactSupport did not reset it (only the
  advisory/Sumsub path did). A later unrelated KYC approval would fire a
  false eea_uplift_completed. Reset the latch on modal dismiss.
- re-fire: trackStarted is now idempotent per armed attempt, so repeat
  Continue clicks in one attempt don't inflate the started count.
- DRY + convention: collapse the four duplicated trigger-construction sites
  into upliftTriggerFrom{Gate,Advisory} helpers and move UpliftStartTrigger
  out of the hook into eea-uplift.utils (per the no-types-in-hooks rule).

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

🧹 Nitpick comments (2)
src/utils/eea-uplift.utils.ts (1)

48-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type cast bypasses GateState discriminated-union narrowing.

(gate as { reason?: { code?: string } }) sidesteps TypeScript's exhaustiveness checking on the GateState union. It works today because JS returns undefined for missing properties, but if CapabilityReason.code or the reason field's shape changes in capability-gate.ts, this cast won't surface a compile error — it'll silently produce undefined and this function will just stop matching.

Consider narrowing with 'reason' in gate instead, which stays type-safe against the actual union:

♻️ Safer narrowing without a blanket cast
 export function upliftTriggerFromGate(gate: GateState): UpliftStartTrigger | null {
-    const code = (gate as { reason?: { code?: string } }).reason?.code
+    const code = 'reason' in gate ? gate.reason?.code : undefined
     if (!code?.startsWith('eea_uplift')) return null
     return { requirementKey: code, source: 'blocking' }
 }
🤖 Prompt for 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.

In `@src/utils/eea-uplift.utils.ts` around lines 48 - 52, The
`upliftTriggerFromGate` helper is bypassing `GateState` narrowing with a blanket
cast, which defeats union safety. Update the logic to narrow the `gate` value
using the discriminant/shape check (for example, `'reason' in gate`) before
reading `reason.code`, so the function stays aligned with `GateState` and
`CapabilityReason` in `capability-gate.ts` without relying on an unsafe cast.
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx (1)

267-270: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider folding the derive+track pattern into the funnel hook.

The const trigger = upliftTriggerFrom*(...); if (trigger) trackUpliftStarted(trigger) snippet is repeated here and again in the withdraw page (4 occurrences total). Extending useEeaUpliftFunnel with e.g. trackStartedFromGate(gate) / trackStartedFromAdvisory(advisory) would let call sites drop the eea-uplift.utils import and null-check entirely, keeping the "when does start fire" logic fully inside the hook (matching the PR's stated goal of consolidating trigger construction into shared helpers).

♻️ Sketch of the hook-side consolidation
 export function useEeaUpliftFunnel(channel: UpliftChannel) {
     const startedRef = useRef<UpliftStartTrigger | null>(null)

     const trackStarted = useCallback(
         (trigger: UpliftStartTrigger) => {
             if (sameTrigger(startedRef.current, trigger)) return
             startedRef.current = trigger
             posthog.capture(ANALYTICS_EVENTS.EEA_UPLIFT_STARTED, upliftEventProps(channel, trigger))
         },
         [channel]
     )
+
+    const trackStartedFromGate = useCallback((gate: GateState) => {
+        const trigger = upliftTriggerFromGate(gate)
+        if (trigger) trackStarted(trigger)
+    }, [trackStarted])
+
+    const trackStartedFromAdvisory = useCallback((advisory: GateAdvisory | undefined) => {
+        const trigger = upliftTriggerFromAdvisory(advisory)
+        if (trigger) trackStarted(trigger)
+    }, [trackStarted])
     ...
-    return { trackStarted, trackCompleted, reset }
+    return { trackStarted, trackStartedFromGate, trackStartedFromAdvisory, trackCompleted, reset }
 }

Also applies to: 280-283

🤖 Prompt for 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.

In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx around lines 267 -
270, Fold the repeated uplift derive+track logic into useEeaUpliftFunnel so call
sites no longer need upliftTriggerFromGate/upliftTriggerFromAdvisory or a
null-check. Add hook methods like trackStartedFromGate(gate) and
trackStartedFromAdvisory(advisory) that internally resolve the trigger and call
trackUpliftStarted, then update the bank page (and the matching withdraw page
usage) to use those methods and remove the direct eea-uplift.utils import.
🤖 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.

Nitpick comments:
In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 267-270: Fold the repeated uplift derive+track logic into
useEeaUpliftFunnel so call sites no longer need
upliftTriggerFromGate/upliftTriggerFromAdvisory or a null-check. Add hook
methods like trackStartedFromGate(gate) and trackStartedFromAdvisory(advisory)
that internally resolve the trigger and call trackUpliftStarted, then update the
bank page (and the matching withdraw page usage) to use those methods and remove
the direct eea-uplift.utils import.

In `@src/utils/eea-uplift.utils.ts`:
- Around line 48-52: The `upliftTriggerFromGate` helper is bypassing `GateState`
narrowing with a blanket cast, which defeats union safety. Update the logic to
narrow the `gate` value using the discriminant/shape check (for example,
`'reason' in gate`) before reading `reason.code`, so the function stays aligned
with `GateState` and `CapabilityReason` in `capability-gate.ts` without relying
on an unsafe cast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e8fd9b8-4a9b-4944-b8ff-7a835f9ade4f

📥 Commits

Reviewing files that changed from the base of the PR and between dce0c3b and 39f20e3.

📒 Files selected for processing (6)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/hooks/useEeaUpliftFunnel.test.ts
  • src/hooks/useEeaUpliftFunnel.ts
  • src/utils/eea-uplift.utils.test.ts
  • src/utils/eea-uplift.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useEeaUpliftFunnel.test.ts

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@kushagrasarathe kushagrasarathe marked this pull request as ready for review July 2, 2026 16:04
@kushagrasarathe kushagrasarathe requested a review from Hugo0 July 2, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant