Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
eccdb7d
feat: enable SEPA bank rail for AL/MD/ME/MK/RS + treat Bulgaria as Eu…
abalinda Jul 13, 2026
5f612dc
docs: retitle bank-country map — contents outgrew the 'EAA codes' label
abalinda Jul 13, 2026
997924a
Refactor Macedonian Denar entry formatting
abalinda Jul 13, 2026
976d8f4
style: re-wrap MKD row — prettier print-width forces multi-line, form…
abalinda Jul 13, 2026
8316bdd
Merge pull request #2408 from peanutprotocol/feat/sepa-balkans-bg-eur
Hugo0 Jul 13, 2026
0adb18f
feat(card): /fix-card-signature — guided repair for enable-bricked ke…
jjramirezn Jul 14, 2026
8289fe2
chore: drop eslint-disable for a rule this config doesn't define
jjramirezn Jul 14, 2026
aac3020
review: harden /fix-card-signature per /code-review findings
jjramirezn Jul 14, 2026
d0e51ef
review: widen the repair confirm poll to ~12s (CodeRabbit) — fewer fa…
jjramirezn Jul 14, 2026
a5cc747
Merge pull request #2412 from peanutprotocol/hotfix/fix-card-signatur…
Hugo0 Jul 14, 2026
d550911
fix(deposits): confirm before cancelling + uniform shortened reference
abalinda Jul 14, 2026
6347c3b
fix(support): make SupportDrawer intercept clicks when opened inside …
abalinda Jul 14, 2026
3e1f71e
fix(deposits): guard confirm CTA against double-tap firing the cancel…
abalinda Jul 14, 2026
9b4f6f2
Merge pull request #2416 from peanutprotocol/fix/deposit-cancel-confi…
jjramirezn Jul 14, 2026
2acc377
content: publish card-legal contact hotfix to production
abalinda Jul 15, 2026
7258f72
Merge pull request #2423 from peanutprotocol/content/publish-card-con…
Hugo0 Jul 15, 2026
d64a2a4
content: publish latest to production (src/content → peanut-content@e…
0xkkonrad Jul 15, 2026
3115088
Merge pull request #2425 from peanutprotocol/content/publish-to-main-…
0xkkonrad Jul 15, 2026
0bab866
Merge remote-tracking branch 'origin/main' into backmerge/main-into-dev
kushagrasarathe Jul 16, 2026
c0bee71
Merge remote-tracking branch 'origin/dev' into backmerge/main-into-dev
kushagrasarathe Jul 16, 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
155 changes: 155 additions & 0 deletions src/app/(mobile-ui)/fix-card-signature/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use client'

/**
* Hidden support page: /fix-card-signature
*
* Guided repair for accounts whose card auto-funding approval can never
* validate (nonce-bricked or undeployed kernel — see useCardSignatureRepair).
* Not linked from anywhere; support DMs the URL to affected users. Two passkey
* taps: repair the wallet state, then re-grant auto-funding (the backend
* kicks off a funding run the moment the new approval is stored).
*/

import { useEffect, useState } from 'react'
import { Button } from '@/components/0_Bruddle/Button'
import { Card } from '@/components/0_Bruddle/Card'
import NavHeader from '@/components/Global/NavHeader'
import { findActiveCard } from '@/components/Card/cardState.utils'
import { useRainCardOverview } from '@/hooks/useRainCardOverview'
import { useZeroDev } from '@/hooks/useZeroDev'
import { useCardSignatureRepair } from '@/hooks/wallet/useCardSignatureRepair'
import { useGrantSessionKey } from '@/hooks/wallet/useGrantSessionKey'

export default function FixCardSignaturePage() {
const { address } = useZeroDev()
const { overview, isLoading: isOverviewLoading } = useRainCardOverview()
const { diagnosis, isDiagnosing, isRepairing, error, diagnose, repair } = useCardSignatureRepair()
const { grant, isGranting } = useGrantSessionKey()
const [grantDone, setGrantDone] = useState(false)
const [grantErrorMessage, setGrantErrorMessage] = useState<string | null>(null)

const card = findActiveCard(overview)

// Keyed on address: the zerodev address hydrates asynchronously after the
// layout unblocks, so a mount-only effect would diagnose before it exists
// and never retry — dead page on a cold load from a support DM.
useEffect(() => {
if (address) void diagnose()
}, [address, diagnose])

const needsRepair = diagnosis !== null && diagnosis.state !== 'healthy'
const busy = isDiagnosing || isRepairing || isGranting

const handleRepair = async () => {
setGrantErrorMessage(null)
await repair()
}

const handleGrant = async () => {
setGrantErrorMessage(null)
const result = await grant()
if (result.ok) {
setGrantDone(true)
} else if (result.error.kind !== 'user-cancelled') {
setGrantErrorMessage(
result.error.kind === 'no-card'
? 'No active card found on this account — please contact support.'
: 'Re-enabling did not complete — please try again or contact support.'
)
}
}

return (
<div className="flex min-h-[inherit] flex-col gap-8">
<NavHeader title="Fix card signature" />
<div className="my-auto flex flex-col gap-6">
<p className="text-sm text-grey-1">
This tool repairs a wallet state issue that stops your card from funding itself automatically. It
takes up to two quick passkey confirmations.
</p>

{(isDiagnosing || (!address && !diagnosis)) && <p className="text-sm">Checking your wallet…</p>}

{!isDiagnosing && !diagnosis && error && (
<Button variant="stroke" className="w-full" onClick={() => void diagnose()}>
Check again
</Button>
)}

{diagnosis && (
<Card className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between">
<span className="font-bold">1. Repair wallet state</span>
<span className="text-sm">{needsRepair ? (isRepairing ? '⏳' : '⚠️ needed') : '✅'}</span>
</div>
{needsRepair && (
<>
<p className="text-sm text-grey-1">
{diagnosis.state === 'nonce-bricked'
? 'Your wallet is blocking new card permissions after an earlier security upgrade. One confirmation clears it.'
: 'Your wallet needs a one-time on-chain activation before card permissions can work.'}
</p>
<Button
variant="purple"
shadowSize="4"
className="w-full"
onClick={handleRepair}
disabled={busy}
>
{isRepairing ? 'Repairing…' : 'Repair now'}
</Button>
</>
)}
</Card>
)}

{diagnosis?.state === 'healthy' && (
<Card className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between">
<span className="font-bold">2. Re-enable automatic funding</span>
<span className="text-sm">{grantDone ? '✅' : isGranting ? '⏳' : ''}</span>
</div>
{grantDone ? (
<p className="text-sm text-grey-1">
All set! Automatic funding is back on and a funding run has been started — your card
balance should update within a few minutes.
</p>
) : (
<>
<p className="text-sm text-grey-1">
One more confirmation re-enables automatic card funding with a fresh permission.
</p>
<Button
variant="purple"
shadowSize="4"
className="w-full"
onClick={handleGrant}
disabled={busy || isOverviewLoading || !card}
>
{isGranting
? 'Waiting for confirmation…'
: isOverviewLoading
? 'Loading your card…'
: 'Re-enable funding'}
</Button>
{!isOverviewLoading && !card && (
<p className="text-sm text-grey-1">
No active card found on this account — please contact support.
</p>
)}
</>
)}
</Card>
)}

{(error || grantErrorMessage) && <p className="text-sm text-error">{error ?? grantErrorMessage}</p>}

{diagnosis && diagnosis.state !== 'undeployed' && (
<p className="text-xs text-grey-1">
Diagnostics: nonce {diagnosis.currentNonce} / floor {diagnosis.validNonceFrom}
</p>
)}
</div>
</div>
)
}
10 changes: 5 additions & 5 deletions src/components/AddMoney/components/AddMoneyBankDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useRouter, useParams } from 'next/navigation'
import { useCallback, useEffect, useMemo } from 'react'
import { countryData } from '@/components/AddMoney/consts'
import { formatCurrencyAmount } from '@/utils/currency'
import { formatBankAccountDisplay } from '@/utils/format.utils'
import { formatBankAccountDisplay, shortDepositReference } from '@/utils/format.utils'
import { applyBridgeCrossCurrencyFee, getCurrencyConfig, getCurrencySymbol } from '@/utils/bridge.utils'
import { RequestFulfillmentBankFlowStep, useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext'
import { formatAmount } from '@/utils/general.utils'
Expand Down Expand Up @@ -236,7 +236,7 @@ ${routingLabel}: ${routingValue}`
}

bankDetails += `
Deposit Reference: ${onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'}
Deposit Reference: ${shortDepositReference(onrampData?.depositInstructions?.depositMessage) || 'Loading...'}

Please use these details to complete your bank transfer.`

Expand Down Expand Up @@ -274,11 +274,11 @@ Please use these details to complete your bank transfer.`
<p className="text-xs font-normal text-gray-1">Deposit reference</p>
<div className="flex items-baseline gap-2">
<p className="text-xl font-extrabold text-black md:text-4xl">
{onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'}
{shortDepositReference(onrampData?.depositInstructions?.depositMessage) || 'Loading...'}
</p>
{onrampData?.depositInstructions?.depositMessage && (
<CopyToClipboard
textToCopy={onrampData.depositInstructions.depositMessage?.slice(0, 10)}
textToCopy={shortDepositReference(onrampData.depositInstructions.depositMessage)}
fill="black"
iconSize="4"
/>
Expand Down Expand Up @@ -416,7 +416,7 @@ Please use these details to complete your bank transfer.`
title="Double check in your bank before sending:"
items={[
`Amount: ${formattedCurrencyAmount} (exact)`,
`Reference: ${onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'} (included)`,
`Reference: ${shortDepositReference(onrampData?.depositInstructions?.depositMessage) || 'Loading...'} (included)`,
]}
/>

Expand Down
10 changes: 8 additions & 2 deletions src/components/AddMoney/consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export const countryData: CountryData[] = [
id: 'BGR',
type: 'country',
title: 'Bulgaria',
currency: 'BGN',
currency: 'EUR',
path: 'bulgaria',
iso2: 'BG',
iso3: 'BGR',
Expand Down Expand Up @@ -2597,10 +2597,12 @@ export const countryData: CountryData[] = [

export const COUNTRY_SPECIFIC_METHODS: Record<string, CountrySpecificMethods> = {}

// bridge EAA country codes, source: https://apidocs.bridge.xyz/docs/sepa-euro-transactions
// countries enabled for Bridge bank transfers — SEPA zone (source: https://apidocs.bridge.xyz/docs/sepa-euro-transactions,
// incl. the 2025/26 SEPA joiners AL/MD/ME/MK/RS) plus US
// note: this is a map of 3-letter country codes to 2-letter country codes, for flags to work, bridge expects 3 letter codes
export const BRIDGE_ALPHA3_TO_ALPHA2: { [key: string]: string } = {
ALA: 'AX',
ALB: 'AL',
AND: 'AD',
AUT: 'AT',
BEL: 'BE',
Expand All @@ -2627,13 +2629,17 @@ export const BRIDGE_ALPHA3_TO_ALPHA2: { [key: string]: string } = {
MLT: 'MT',
MTQ: 'MQ',
MYT: 'YT',
MDA: 'MD',
MNE: 'ME',
NLD: 'NL',
MKD: 'MK',
NOR: 'NO',
POL: 'PL',
PRT: 'PT',
REU: 'RE',
ROU: 'RO',
MAF: 'MF',
SRB: 'RS',
SVK: 'SK',
SVN: 'SI',
ESP: 'ES',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,31 @@ describe('SupportDrawer — Crisp load-failure fallback', () => {
})
})

describe('SupportDrawer — pointer-events when opened inside a vaul drawer', () => {
beforeEach(() => {
mockUseCrispUserData.mockReset().mockReturnValue({ userId: undefined, email: undefined })
mockUseCrispTokenId.mockReset().mockReturnValue(undefined)
mockIsCapacitor.mockReset().mockReturnValue(false)
})

it('backdrop and panel explicitly re-enable pointer events while open', () => {
// vaul sets pointer-events:none on <body> while the transaction drawer is
// open. Without an explicit pointer-events-auto, the support overlay
// inherits none and becomes click-transparent — taps fall through to the
// receipt underneath (one landed on "Cancel deposit" and cancelled a
// user's funded bank deposit).
const { container } = render(<SupportDrawer />)

const backdrop = container.querySelector('[aria-hidden="true"]')
const panel = screen.getByRole('dialog', { name: 'Support' })

expect(backdrop?.className).toContain('pointer-events-auto')
expect(panel.className).toContain('pointer-events-auto')
expect(backdrop?.className).not.toContain('pointer-events-none')
expect(panel.className).not.toContain('pointer-events-none')
})
})

describe('SupportDrawer Crisp session gate — native (Capacitor)', () => {
beforeEach(() => {
mockUseCrispUserData.mockReset()
Expand Down
10 changes: 8 additions & 2 deletions src/components/Global/SupportDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,15 @@ const SupportDrawer = () => {
return (
<>
{/* backdrop */}
{/* pointer-events-auto is load-bearing on BOTH divs: when this drawer is
opened from inside a vaul drawer (transaction receipt), vaul sets
pointer-events:none on <body> and these divs inherit it — the whole
support overlay becomes click-transparent, and taps fall through to
the receipt underneath (a fall-through tap on "Cancel deposit"
cancelled a user's funded bank deposit). */}
<div
className={`fixed inset-0 z-[999998] bg-black/80 transition-opacity duration-300 ${
isSupportModalOpen ? 'opacity-100' : 'pointer-events-none opacity-0'
isSupportModalOpen ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
}`}
onClick={() => setIsSupportModalOpen(false)}
aria-hidden="true"
Expand All @@ -162,7 +168,7 @@ const SupportDrawer = () => {
aria-label="Support"
aria-modal={isSupportModalOpen}
className={`fixed inset-x-0 bottom-0 z-[999999] flex max-h-[85vh] flex-col rounded-t-[10px] border bg-background pt-4 ${
isSupportModalOpen ? 'translate-y-0' : 'pointer-events-none translate-y-full'
isSupportModalOpen ? 'pointer-events-auto translate-y-0' : 'pointer-events-none translate-y-full'
}`}
style={{
transform: isSupportModalOpen ? `translateY(${dragOffset}px)` : 'translateY(100%)',
Expand Down
Loading
Loading