Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
9c094bc
feat(waitlist): "who invited you" — drop suffix from invite-code UX
0xkkonrad May 28, 2026
c697b5f
ux(waitlist): drop jail framing — "Peanut is invite-only" instead
0xkkonrad May 28, 2026
d832e5a
chore(assets): consolidate peanut mascot assets into one canonical home
0xkkonrad Jun 2, 2026
cdbe7ae
chore(assets): optimize mascot GIFs to animated WebP (-67%)
0xkkonrad Jun 2, 2026
dba77b2
chore(assets): restore the 2 pruned mascot poses to complete the set
0xkkonrad Jun 2, 2026
fb5e5b4
chore(assets): bring in the 2nd animation batch (waving/walking/point…
0xkkonrad Jun 2, 2026
3e0a8c3
chore(assets): optimize peanut-club still to WebP (-74%)
0xkkonrad Jun 2, 2026
3b79345
chore(assets): optimize repo-wide raster assets (-80%, ~9.4MB)
0xkkonrad Jun 2, 2026
465e1a4
fix(mascot): address CodeRabbit review on #2164
0xkkonrad Jun 2, 2026
15e02b4
chore(mascot): rename 3 exports + retune per-screen mascot casting
0xkkonrad Jun 3, 2026
0c4bc2f
Merge dev into consolidation branch; resolve badge.utils.ts
0xkkonrad Jun 3, 2026
a20674c
chore(assets): further-optimize 16 rasters + recrisp github-white icon
0xkkonrad Jun 3, 2026
b6618d4
chore(mascot): give PeanutWhistling a home on landing hero + setup intro
0xkkonrad Jun 3, 2026
1441f7f
ux(waitlist): clearer invite-not-found error — point users at the inv…
0xkkonrad Jun 3, 2026
98c1101
fix(drawer): give drag handle its own top spacing in shared primitive
0xkkonrad Jun 3, 2026
526f9cd
feat(invite): unify all invite links on bare ?code=<username>
0xkkonrad Jun 3, 2026
ae86add
chore(assets): fix stale mascot barrel comments + re-point qr-pay tes…
0xkkonrad Jun 3, 2026
545392f
feat(card): use PeanutWalking on physical-card waitlist, relieving ov…
0xkkonrad Jun 3, 2026
b47f053
chore(assets): remove 7 orphaned image assets
0xkkonrad Jun 3, 2026
8ae0f35
Show actionable Pix-minimum error on below-minimum charges
abalinda Jun 4, 2026
58d2716
Match real PIX_MIN_AMOUNT error code from backend
abalinda Jun 4, 2026
b3d0d0c
feat(mascot): use PeanutWhistling for chill 'you're in' wins, reserve…
0xkkonrad Jun 4, 2026
74f2344
Merge pull request #2190 from peanutprotocol/chore/sync-main-dev-0606
Hugo0 Jun 7, 2026
912b69a
fix(card): surface real readiness errors + show actually-recovered am…
Hugo0 Jun 7, 2026
329c968
Merge pull request #2195 from peanutprotocol/fix/card-readiness-and-r…
Hugo0 Jun 7, 2026
c3acbfb
feat(shhhhh): polish card share asset for Twitter — peanut blue, hero…
Hugo0 Jun 7, 2026
06ba76e
test(invites): guard campaign codes resolve to real BADGES entries
Hugo0 Jun 1, 2026
7a8c786
Merge remote-tracking branch 'origin/main' into chore/sync-main-into-…
Hugo0 Jun 7, 2026
019e7df
fix(card): retire the stuck "You skipped the line" history row
Hugo0 Jun 7, 2026
66dc977
chore(card): rename useCardPioneerInfo → useCardInfo (Pioneer is gone)
Hugo0 Jun 7, 2026
49a4fcd
feat(shhhhh): bare door joins the waitlist; ?campaign=skip awards Ski…
Hugo0 Jun 7, 2026
82e6db4
fix(shhhhh): read campaign param client-side, not useSearchParams (pr…
Hugo0 Jun 7, 2026
0ac14ab
Merge pull request #2197 from peanutprotocol/chore/preserve-badge-gua…
Hugo0 Jun 7, 2026
d756fe7
refactor(shhhhh): read ?campaign=skip at click time, drop needless state
Hugo0 Jun 7, 2026
785deba
Merge pull request #2198 from peanutprotocol/chore/sync-main-into-dev…
Hugo0 Jun 7, 2026
7c0aeea
fix(history): consistent sign + FX rate for card transactions
Hugo0 Jun 8, 2026
1d7d2e5
Merge pull request #2164 from peanutprotocol/chore/consolidate-mascot…
Hugo0 Jun 8, 2026
383b94e
fix(receipt): restore pre-decomplexify receipt links (?t= back-compat)
Hugo0 Jun 8, 2026
3a1e206
refactor(history): collapse per-type switches into config maps
Hugo0 Jun 8, 2026
5b40b53
test(receipt): lock OFFRAMP FX-row generalization
Hugo0 Jun 8, 2026
e58de7f
fix(receipt): normalize array searchParams in resolveReceiptKind
Hugo0 Jun 8, 2026
5838dcc
fix(receipt): drop dead SimpleFi legacy mapping + phantom route param
Hugo0 Jun 10, 2026
8b2479e
fix(shhhhh+card): audit fixes — door opens for access holders, histor…
Hugo0 Jun 10, 2026
75a759f
docs(predicates): pin why isFxBearingFlow's card arm is block-based
Hugo0 Jun 10, 2026
b22704d
Merge remote-tracking branch 'origin/main' into chore/sync-main-into-…
Hugo0 Jun 10, 2026
b75b1b2
refactor(history): one anchor per fact — kind sets, sign record, shar…
Hugo0 Jun 10, 2026
d3031d5
Merge pull request #2205 from peanutprotocol/chore/sync-main-into-dev…
Hugo0 Jun 10, 2026
e569f65
fix(kyc): stop FE second-guessing cross-region provider — close the R…
Hugo0 Jun 10, 2026
b3981de
chore(profile): remove dead KycVerifiedOrReviewModal
Hugo0 Jun 10, 2026
ef17391
Merge origin/dev into feat/shhhhh-visual-polish
Hugo0 Jun 10, 2026
5539733
fix(limits): scope the locked-region pending badge to the rail's own …
Hugo0 Jun 10, 2026
a8624cf
Merge origin/dev — resolve mascot asset consolidation
Hugo0 Jun 10, 2026
c2e337a
fix(claim): carry the regional claim method through the auth redirect…
Hugo0 Jun 10, 2026
c4a4a37
Merge remote-tracking branch 'origin/dev' into fix/kyc-loop-door-cleanup
Hugo0 Jun 10, 2026
8ceb81f
Merge pull request #2200 from peanutprotocol/fix/card-txn-sign-fx
Hugo0 Jun 10, 2026
5105a01
chore(types): regen api.openapi.json + api.generated.ts against api#9…
Hugo0 Jun 10, 2026
8bcd81b
Merge pull request #2196 from peanutprotocol/feat/shhhhh-visual-polish
Hugo0 Jun 10, 2026
d17ae80
fix: drop accidentally-committed public/flags symlink
Hugo0 Jun 10, 2026
700139d
Merge pull request #2206 from peanutprotocol/fix/kyc-loop-door-cleanup
Hugo0 Jun 10, 2026
d2d9514
Merge remote-tracking branch 'origin/dev' into feat/waitlist-who-invi…
Hugo0 Jun 10, 2026
b08abc9
fix(waitlist): tolerate hand-typed inviter usernames + unify not-foun…
Hugo0 Jun 10, 2026
2e0fd15
Merge pull request #2127 from peanutprotocol/feat/waitlist-who-invite…
Hugo0 Jun 10, 2026
e5bf5ac
fix(card): map rail failure statuses to real screens, not add-card
Hugo0 Jun 10, 2026
c99c2e3
review hardening: default-deny unknown railStatus + wait for capabili…
Hugo0 Jun 10, 2026
ce0d26b
feat: advertise ETH on the crypto deposit screen (EVM only)
Hugo0 Jun 10, 2026
5c5d507
chore: annotate getSupportedTokens return type
Hugo0 Jun 10, 2026
03d8b6d
Merge pull request #2207 from peanutprotocol/fix/card-rail-failure-st…
Hugo0 Jun 10, 2026
f95b4c1
fix(crisp): isolate support chat per account, stop cross-user history…
Hugo0 Jun 10, 2026
9882c42
review: derive RHINO_SUPPORTED_TOKENS from the per-network union; mir…
Hugo0 Jun 10, 2026
1c472cf
fix: actionable UX + telemetry for WebAuthn NotAllowedError during si…
Hugo0 Jun 10, 2026
42858db
docs(crisp): correct stale iframe-mount comment after token gate
Hugo0 Jun 10, 2026
57de610
fix(rain-cooldown): mount intro modal globally, not just in (mobile-ui)
Hugo0 Jun 10, 2026
376e8ec
feat(analytics): rain_cooldown_hit event when qr-pay swallows a cooldown
Hugo0 Jun 10, 2026
4d6e905
fix(chunks): inline auto-reload recovery for uncaught chunk-load fail…
Hugo0 Jun 10, 2026
9b1cf88
fix(chunks): recover from chunk failures that error boundaries catch
Hugo0 Jun 10, 2026
6908ccc
Merge pull request #2208 from peanutprotocol/feat/eth-deposit-token
Hugo0 Jun 10, 2026
7db50c5
review: hoist WebAuthn error-name set, document text-vs-name matching…
Hugo0 Jun 10, 2026
634af22
Merge pull request #2209 from peanutprotocol/fix/crisp-session-cross-…
Hugo0 Jun 10, 2026
3c1ec97
fix(crisp): gate native Crisp open on token too, closing the same ble…
Hugo0 Jun 10, 2026
bbf60e5
Merge pull request #2210 from peanutprotocol/fix/passkey-sign-notallowed
Hugo0 Jun 10, 2026
b32d9ef
fix(chunks): use a raw inline script, not next/script beforeInteractive
Hugo0 Jun 10, 2026
519a844
review: capture rain_cooldown_hit in rainRequest, not per-callsite
Hugo0 Jun 10, 2026
a61a209
Merge pull request #2213 from peanutprotocol/fix/crisp-native-token-gate
Hugo0 Jun 10, 2026
c5c4c9b
Merge pull request #2212 from peanutprotocol/fix/chunk-error-recovery
Hugo0 Jun 11, 2026
4a30022
Merge remote-tracking branch 'origin/dev' into fix/rain-cooldown-glob…
Hugo0 Jun 11, 2026
41aacd6
Merge pull request #2211 from peanutprotocol/fix/rain-cooldown-global…
Hugo0 Jun 11, 2026
f04b8a5
Fail fast on PIX_MIN_AMOUNT payment-lock errors
Hugo0 Jun 11, 2026
3b7843c
polish(share-asset): drop the tick-mark sparkle deco + bring back the…
Hugo0 Jun 11, 2026
a050caf
Cover the open-amount Pix path with the same minimum-amount error
Hugo0 Jun 11, 2026
17def36
Merge pull request #2217 from peanutprotocol/polish/share-asset-tick-…
Hugo0 Jun 11, 2026
766aa05
Merge pull request #2186 from peanutprotocol/fix/qr-pay-pix-minimum-e…
Hugo0 Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "src/assets/animations"]
path = src/assets/animations
url = https://github.com/peanutprotocol/peanut-animations.git
[submodule "src/content"]
path = src/content
url = https://github.com/peanutprotocol/peanut-content.git
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
"node_modules/(?!(@wagmi|wagmi|viem|@viem|@walletconnect|@justaname\\.id|@zerodev|permissionless)/)"
],
"moduleNameMapper": {
"\\.(svg|png|jpg|jpeg|gif)$": "jest-transform-stub",
"\\.(svg|png|jpg|jpeg|gif|webp)$": "jest-transform-stub",
"^@/config/wagmi\\.config$": "<rootDir>/src/utils/__mocks__/wagmi-config.ts",
"^wagmi/chains$": "<rootDir>/src/utils/__mocks__/wagmi.ts",
"^@justaname\\.id/react$": "<rootDir>/src/utils/__mocks__/justaname.ts",
Expand Down
Binary file removed public/badges/_archive/founder_house.png
Binary file not shown.
Binary file modified public/badges/peanut-pioneer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/claim-metadata-img.jpg
Binary file not shown.
Binary file removed public/easter-eggs/antarctica.png
Binary file not shown.
Binary file added public/easter-eggs/antarctica.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/bouvet.png
Binary file not shown.
Binary file added public/easter-eggs/bouvet.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/christmas.png
Binary file not shown.
Binary file added public/easter-eggs/christmas.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/cocos.png
Binary file not shown.
Binary file added public/easter-eggs/cocos.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/heard.png
Binary file not shown.
Binary file added public/easter-eggs/heard.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/pitcairn.png
Binary file not shown.
Binary file added public/easter-eggs/pitcairn.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/southgeorgia.png
Binary file not shown.
Binary file added public/easter-eggs/southgeorgia.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/easter-eggs/tokelau.png
Binary file not shown.
Binary file added public/easter-eggs/tokelau.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/email/peanut-jail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/email/peanut-wave.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/game/1x-cloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/game/1x-horizon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/game/1x-obstacle-large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/game/1x-obstacle-small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/game/1x-restart.png
Binary file modified public/game/1x-trex.png
Binary file modified public/game/2x-cloud.png
Binary file modified public/game/2x-horizon.png
Binary file modified public/game/2x-obstacle-large.png
Binary file modified public/game/2x-obstacle-small.png
Binary file modified public/game/2x-restart.png
Binary file modified public/game/2x-text.png
Binary file modified public/game/2x-trex.png
Binary file modified public/icons/apple-touch-icon-152x152-beta.png
Binary file modified public/icons/apple-touch-icon-beta.png
Binary file modified public/icons/icon-192x192-beta.png
Binary file modified public/icons/icon-192x192-maskable.png
Binary file modified public/icons/icon-512x512-beta.png
Binary file modified public/icons/icon-512x512-maskable.png
Binary file modified public/logo-favicon.png
Binary file modified public/merchants/badigitalnomads/coworking.jpg
Binary file modified public/merchants/stain/profile.jpg
Binary file modified public/merchants/stain/tripadvisor-2.jpg
Binary file modified public/metadata-img.png
Binary file removed public/preview-bg.png
Diff not rendered.
Binary file removed public/raffle-metadata-img.png
Diff not rendered.
Binary file removed public/redpacket-img.png
Diff not rendered.
Binary file modified public/social-preview-bg.png
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ jest.mock('@/constants/rhino.consts', () => ({
getSupportedTokens: jest.fn(() => [
{ name: 'USDC', logoUrl: '/usdc.png' },
{ name: 'USDT', logoUrl: '/usdt.png' },
{ name: 'ETH', logoUrl: '/eth-token.png' },
]),
TOKEN_LOGOS: { USDC: '/usdc.png', USDT: '/usdt.png' },
TOKEN_LOGOS: { USDC: '/usdc.png', USDT: '/usdt.png', ETH: '/eth-token.png' },
}))

jest.mock('@/constants/zerodev.consts', () => ({
Expand Down
15 changes: 11 additions & 4 deletions src/app/(mobile-ui)/card-recovery/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client'

import { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import type { Address, Hex } from 'viem'
import { Button } from '@/components/0_Bruddle/Button'
import { Card } from '@/components/0_Bruddle/Card'
import ErrorAlert from '@/components/Global/ErrorAlert'
import NavHeader from '@/components/Global/NavHeader'
import PeanutLoading from '@/components/Global/PeanutLoading'
import { useKernelClient } from '@/context/kernelClient.context'
import { useSafeBack } from '@/hooks/useSafeBack'
import {
RAIN_WITHDRAW_EIP712_DOMAIN_NAME,
RAIN_WITHDRAW_EIP712_DOMAIN_VERSION,
Expand Down Expand Up @@ -40,13 +40,17 @@ type Step = 'preview' | 'confirm' | 'signing' | 'submitting' | 'done'
* requires the user's passkey.
*/
export default function CardRecoveryPage() {
const router = useRouter()
const onBack = useSafeBack('/home')
const { getClientForChain } = useKernelClient()

const [step, setStep] = useState<Step>('preview')
const [preview, setPreview] = useState<RecoverFundsPreviewResponse | null>(null)
const [error, setError] = useState<string | null>(null)
const [txHash, setTxHash] = useState<Hex | null>(null)
// The amount actually prepared + signed + submitted. The mount-time `preview`
// can be stale by the time the user confirms (collateral can change), so the
// completion screen must report what was really recovered, not the preview.
const [recoveredCents, setRecoveredCents] = useState<string | null>(null)

useEffect(() => {
let cancelled = false
Expand All @@ -71,6 +75,8 @@ export default function CardRecoveryPage() {
// page were tampered with at runtime, the backend signs over the
// values it computed itself.
const prep = await rainApi.prepareRecoverFunds()
// Lock in the real prepared amount for the completion screen.
setRecoveredCents(prep.amountCents)

const chainIdStr = String(PEANUT_WALLET_CHAIN.id)
const chainIdNum = Number(prep.chainId)
Expand Down Expand Up @@ -120,15 +126,16 @@ export default function CardRecoveryPage() {

return (
<div className="flex min-h-[inherit] flex-col gap-8">
<NavHeader title="Recover card funds" onPrev={() => router.push('/home')} />
<NavHeader title="Recover card funds" onPrev={onBack} />
<div className="my-auto flex flex-col gap-6">
{error && <ErrorAlert description={error} />}

{step === 'done' && txHash ? (
<Card className="flex flex-col gap-3 p-6">
<h2 className="text-h7 font-bold">Funds sent to your wallet.</h2>
<p className="text-sm text-grey-1">
${formatCents(preview!.amountCents)} USDC has been returned to your peanut wallet.
${formatCents(recoveredCents ?? preview!.amountCents)} USDC has been returned to your peanut
wallet.
</p>
<a
className="text-black underline"
Expand Down
103 changes: 51 additions & 52 deletions src/app/(mobile-ui)/card/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import PageContainer from '@/components/0_Bruddle/PageContainer'
import { SumsubKycWrapper } from '@/components/Kyc/SumsubKycWrapper'
import { rainApi, type ApplyForCardResponse } from '@/services/rain'
import { useGrantSessionKey } from '@/hooks/wallet/useGrantSessionKey'
import { useCapabilities } from '@/hooks/useCapabilities'
import { useModalsContext } from '@/context/ModalsContext'
import { useSafeBack } from '@/hooks/useSafeBack'

// localStorage key for the one-time celebration gate. Phase 5 will swap
// this for a BE-persisted `cardWaitlistSkipCelebrationSeenAt` lookup.
// localStorage key for the one-time celebration gate (per-device by design:
// re-doing the funnel re-celebrates, see the eligibility-check effect below).
// v2 (2026-05-25): celebration now fires for ALL hasCardAccess users, not
// just skip-badge holders. v1's stale `true` values from earlier QA runs
// would silently skip the celebration — bumping the key invalidates them.
Expand Down Expand Up @@ -68,6 +69,7 @@ const CardPage: FC = () => {

const { overview, isLoading: overviewLoading, error: overviewError } = useRainCardOverview()
const { serializeGrant } = useGrantSessionKey()
const { railsForProvider, isLoading: capabilitiesLoading } = useCapabilities()
const { setIsSupportModalOpen } = useModalsContext()
const onBack = useSafeBack('/home')

Expand All @@ -84,8 +86,8 @@ const CardPage: FC = () => {
const [isIssuing, setIsIssuing] = useState(false)

// Track whether the user has acknowledged the skip-badge celebration.
// localStorage for M2; Phase 5 will read this from BE's
// cardWaitlistSkipCelebrationSeenAt column.
// localStorage on purpose (per-device, replayable via the eligibility
// re-hold below) — the celebration is a moment, not durable state.
const [skipCelebrationSeen, setSkipCelebrationSeen] = useState<boolean>(() => getSkipCelebrationSeen())

// Press-and-hold "see if you qualify" gate. Resets per mount: as long
Expand All @@ -96,66 +98,29 @@ const CardPage: FC = () => {
// exists (see cardState.utils.ts — active-card wins first).
const [eligibilityCheckDone, setEligibilityCheckDone] = useState<boolean>(false)

// `?press_door=1` arrives from /shhhhh → /setup → here. It means: the
// user clicked "press the door" on /shhhhh while signed out, just
// completed signup, and now expects to enter the card flow. We
// auto-stamp `flowEarlyAccess` on their behalf so they don't have to
// re-press the door. Initial value is read synchronously to gate the
// outer-gate redirect on first paint; the param is cleared once the
// stamp lands.
const [pressDoorMode, setPressDoorMode] = useState<boolean>(() => {
if (typeof window === 'undefined') return false
return new URL(window.location.href).searchParams.get('press_door') === '1'
})
const pressDoorFiredRef = useRef(false)
useEffect(() => {
if (!pressDoorMode) return
if (pioneerLoading || !cardInfo) return
if (pressDoorFiredRef.current) return
pressDoorFiredRef.current = true
const finish = (): void => {
const url = new URL(window.location.href)
url.searchParams.delete('press_door')
window.history.replaceState(window.history.state, '', url.toString())
setPressDoorMode(false)
}
if (cardInfo.flowEarlyAccess) {
// Already stamped (e.g. quick refresh after stamp landed) — just clean up.
finish()
return
}
void (async () => {
try {
await cardApi.grantFlowEarlyAccess()
posthog.capture(ANALYTICS_EVENTS.CARD_FLOW_EARLY_ACCESS_GRANTED)
await queryClient.invalidateQueries({ queryKey: ['card-info'] })
} catch (err) {
console.error('[card] press_door auto-stamp failed:', err)
} finally {
finish()
}
})()
}, [pressDoorMode, pioneerLoading, cardInfo, queryClient])
// The old `?press_door=1` auto-stamp was removed alongside the /shhhhh
// door rework: the bare door joins the waitlist and grants nothing, so a
// shareable URL that silently stamps flowEarlyAccess would have been the
// exact bypass the rework forbids. BE now also reports flowEarlyAccess
// true whenever hasCardAccess is (inner gate implies outer).

// Outer gate: pre-public-launch, the card campaign isn't fully online
// yet. Users without flow early access get a 404 — the page behaves as
// if it doesn't exist. The only ways in are (a) already holding a card
// / being mid-application, or (b) entering through the special /shhhhh
// page, which stamps `flowEarlyAccess` via ?press_door=1 before landing
// here. BE returns `flowEarlyAccess: false` for everyone else.
// / being mid-application, or (b) holding card access (skip badge /
// admin grant — BE reports flowEarlyAccess true whenever hasCardAccess
// is). Everyone else belongs on /shhhhh, which joins the waitlist
// inline and never routes here.
//
// IMPORTANT: skip the 404 if the user already has a non-canceled card.
// Legacy Pioneers + admin-granted users issued cards before /shhhhh
// existed and have no flowEarlyAccess stamp — they must still reach
// YourCardScreen. The computeCardState() precedence below mirrors this
// rule (active-card before no-flow-access). Also skip while pressDoorMode
// is in flight: the stamp is about to land any tick now, and 404-ing
// mid-stamp would wrongly bounce a legit /shhhhh entrant.
// rule (active-card before no-flow-access).
//
// notFound() thrown synchronously inside the effect bubbles to Next's
// not-found boundary just like a render-time call.
useEffect(() => {
if (pressDoorMode) return
if (pioneerLoading || pioneerError) return
if (!cardInfo) return
if (cardInfo.flowEarlyAccess) return
Expand All @@ -167,7 +132,7 @@ const CardPage: FC = () => {
if (hasIssuedCard) return
posthog.capture(ANALYTICS_EVENTS.CARD_FLOW_GATED)
notFound()
}, [pioneerLoading, pioneerError, cardInfo, overview, overviewLoading, pressDoorMode])
}, [pioneerLoading, pioneerError, cardInfo, overview, overviewLoading])

const state = computeCardState({
overview,
Expand Down Expand Up @@ -546,6 +511,40 @@ const CardPage: FC = () => {
return <ApplicationStatusScreen variant="pending" onPrev={onBack} />
case 'manual-review':
return <ApplicationStatusScreen variant="manual-review" onPrev={onBack} />
case 'requires-info': {
// Surface the structured remediation reason from the
// capabilities read-model — `rail.reason.userMessage` is
// display-ready and provider-neutral by contract. The card
// provider serves exactly one rail, so [0] is the card rail.
// Overview and capabilities load independently — wait for
// capabilities so the screen never flashes without its reason.
if (capabilitiesLoading) {
return (
<div className="flex min-h-[inherit] w-full items-center justify-center">
<Loading />
</div>
)
}
const cardRailReason = railsForProvider('rain')[0]?.reason?.userMessage
return (
<ApplicationStatusScreen
variant="requires-info"
reasonMessage={cardRailReason}
onContactSupport={() => setIsSupportModalOpen(true)}
onPrev={onBack}
/>
)
}
case 'requires-support':
// Pipeline-side failure — nothing the user can re-submit.
// Same support deep-link as 'rejected' below.
return (
<ApplicationStatusScreen
variant="requires-support"
onContactSupport={() => setIsSupportModalOpen(true)}
onPrev={onBack}
/>
)
case 'rejected':
// No retry CTA: Rain denials are terminal on our side. The
// only path forward is support reviewing the case manually
Expand Down
3 changes: 0 additions & 3 deletions src/app/(mobile-ui)/dev/components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -991,9 +991,6 @@ export default function ComponentsPage() {
<p>
<span className="font-bold text-n-1">GuestLoginModal</span> — guest auth flow
</p>
<p>
<span className="font-bold text-n-1">KycVerifiedOrReviewModal</span> — KYC status
</p>
<p>
<span className="font-bold text-n-1">BalanceWarningModal</span> — low balance warning
</p>
Expand Down
1 change: 0 additions & 1 deletion src/app/(mobile-ui)/dev/ds/patterns/modal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ export default function ModalPage() {
['InviteFriendsModal', 'Share referral link with copy + social buttons'],
['ConfirmInviteModal', 'Confirm invitation before sending'],
['GuestLoginModal', 'Prompt guest users to log in or register'],
['KycVerifiedOrReviewModal', 'KYC verification status feedback'],
['BalanceWarningModal', 'Warn about insufficient balance'],
['TokenAndNetworkConfirmationModal', 'Confirm token + chain before transfer'],
['TokenSelectorModal', 'Pick token from a list'],
Expand Down
31 changes: 29 additions & 2 deletions src/app/(mobile-ui)/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { buildKycHistoryEntry } from '@/utils/kyc-grouping.utils'
import { useAuth } from '@/context/authContext'
import { BadgeStatusItem } from '@/components/Badges/BadgeStatusItem'
import { isBadgeHistoryItem } from '@/components/Badges/badge.types'
import CardUnlockHistoryItem from '@/components/Card/CardUnlockHistoryItem'
import { deriveCardUnlockEntry, isCardUnlockHistoryItem } from '@/components/Card/cardUnlock.types'
import { useCardInfo } from '@/hooks/useCardInfo'
import { useRainCardOverview } from '@/hooks/useRainCardOverview'
import React, { useMemo } from 'react'
import { useQueryClient, type InfiniteData } from '@tanstack/react-query'
import { useWebSocket } from '@/hooks/useWebSocket'
Expand All @@ -35,6 +39,9 @@ const HistoryPage = () => {
const { user } = useUserStore()
const queryClient = useQueryClient()
const { fetchUser } = useAuth()
// Synthetic card-unlock row inputs — same cached queries HomeHistory uses.
const { cardInfo } = useCardInfo()
const { overview: rainOverview } = useRainCardOverview()

const {
data: historyData,
Expand Down Expand Up @@ -176,22 +183,35 @@ const HistoryPage = () => {
if (kycEntry) entries.push(kycEntry)
}

// add the card-unlock milestone row, placed chronologically. Unlike
// the home top-5 (where it ages out), the full page always carries it.
if (cardInfo) {
const unlock = deriveCardUnlockEntry({
hasIssuedCard: (rainOverview?.cards.length ?? 0) > 0,
hasCardAccess: cardInfo.hasCardAccess,
cardAccessGrantedAt: cardInfo.waitlistReleasedAt,
skipBadges: cardInfo.skipBadges,
userBadges: user?.user?.badges,
})
if (unlock) entries.push(unlock)
}

entries.sort((a, b) => {
const dateA = new Date(a.timestamp || 0).getTime()
const dateB = new Date(b.timestamp || 0).getTime()
return dateB - dateA
})

return entries
}, [allEntries, user, isLoading])
}, [allEntries, user, isLoading, cardInfo, rainOverview])

// Memoize per-row drawer projection so the .map() below doesn't recompute
// mapTransactionDataForDrawer per row on every parent rerender (websocket
// tick, infinite-scroll fetch). One Map<uuid, mapped> per visible page.
const drawerByUuid = useMemo(() => {
const m = new Map<string, ReturnType<typeof mapTransactionDataForDrawer>>()
for (const item of combinedAndSortedEntries) {
if (isKycStatusItem(item) || isBadgeHistoryItem(item)) continue
if (isKycStatusItem(item) || isBadgeHistoryItem(item) || isCardUnlockHistoryItem(item)) continue
if (!m.has(item.uuid)) m.set(item.uuid, mapTransactionDataForDrawer(item))
}
return m
Expand Down Expand Up @@ -265,6 +285,13 @@ const HistoryPage = () => {
<KycStatusItem position={position} />
) : isBadgeHistoryItem(item) ? (
<BadgeStatusItem position={position} entry={item} />
) : isCardUnlockHistoryItem(item) ? (
<CardUnlockHistoryItem
entry={item}
position={position}
username={user?.user?.username ?? undefined}
badges={user?.user?.badges}
/>
) : (
(() => {
const { transactionDetails, transactionCardType } =
Expand Down
4 changes: 2 additions & 2 deletions src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { useNotifications } from '@/hooks/useNotifications'
import { useCapabilities } from '@/hooks/useCapabilities'
import { useCardPioneerInfo } from '@/hooks/useCardPioneerInfo'
import { useCardInfo } from '@/hooks/useCardInfo'
import HomeCarouselCTA from '@/components/Home/HomeCarouselCTA'
import EnableAutoBalanceBanner from '@/components/Home/EnableAutoBalanceBanner'
import InvitesIcon from '@/components/Home/InvitesIcon'
Expand Down Expand Up @@ -70,7 +70,7 @@ export default function Home() {
const { isActivated, activationStep, dismissCardStep } = useActivationStatus()
// Fire-and-forget: warms the card-info cache so /card mounts fast.
// Return values intentionally unused — only the fetch side effect matters.
useCardPioneerInfo()
useCardInfo()
const username = user?.user.username

const [showBalanceWarningModal, setShowBalanceWarningModal] = useState(false)
Expand Down
3 changes: 0 additions & 3 deletions src/app/(mobile-ui)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import '../../styles/globals.css'
import QRScannerOverlay from '@/components/Global/QRScannerOverlay'
import SecurityVerificationOverlay from '@/components/Global/SecurityVerificationOverlay'
import SupportDrawer from '@/components/Global/SupportDrawer'
import RainCooldownIntroModal from '@/components/Global/RainCooldown/IntroModal'
import JoinWaitlistPage from '@/components/Invites/JoinWaitlistPage'
import { useRouter } from 'next/navigation'
import { Banner } from '@/components/Global/Banner'
Expand Down Expand Up @@ -226,8 +225,6 @@ const Layout = ({ children }: { children: React.ReactNode }) => {

<SupportDrawer />

<RainCooldownIntroModal />

<QRScannerOverlay />

<SecurityVerificationOverlay />
Expand Down
Loading
Loading