Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
// Local UI state (not URL-appropriate - transient)
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
const [showKycModal, setShowKycModal] = useState<boolean>(false)
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)

Check failure on line 65 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

'isRiskAccepted' is assigned a value but never used. Allowed unused elements of array destructuring must match /^_/u

Check failure on line 65 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

'isRiskAccepted' is assigned a value but never used. Allowed unused elements of array destructuring must match /^_/u
const { setError, error, setOnrampData, onrampData } = useOnrampFlow()

const { balance } = useWallet()
Expand Down Expand Up @@ -116,7 +116,7 @@

useEffect(() => {
fetchUser()
}, [])

Check warning on line 119 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

React Hook useEffect has a missing dependency: 'fetchUser'. Either include it or remove the dependency array

Check warning on line 119 in src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

View workflow job for this annotation

GitHub Actions / eslint

React Hook useEffect has a missing dependency: 'fetchUser'. Either include it or remove the dependency array

const peanutWalletBalance = useMemo(() => {
return balance !== undefined ? formatAmount(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) : ''
Expand Down Expand Up @@ -213,9 +213,11 @@
if (!validateAmount(rawTokenAmount)) return

if (gate.kind !== 'ready') {
// capabilities still loading — silently no-op instead of flashing a
// needs_kyc modal on top of state we don't know yet.
if (gate.kind === 'loading') return
// capabilities still loading OR provider doing internal review —
// silently no-op instead of flashing a misleading needs_kyc modal.
// `waiting-on-provider` means the user has nothing to do; opening
// a KYC modal would imply otherwise.
if (gate.kind === 'loading' || gate.kind === 'waiting-on-provider') return
if (gate.kind === 'accept-tos') {
guardWithTos()
} else {
Expand Down Expand Up @@ -437,6 +439,7 @@
handleWarningConfirm()
}}
onSkip={hideTos}
reasonCode={gate.kind === 'accept-tos' ? gate.reason?.code : undefined}
/>
</div>
)
Expand Down
5 changes: 4 additions & 1 deletion src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ export default function WithdrawBankPage() {

const handleCreateAndInitiateOfframp = async () => {
if (gate.kind !== 'ready') {
if (gate.kind === 'loading') return
// Loading and waiting-on-provider both mean "user has no action to
// take" — silently no-op instead of bouncing them through Sumsub.
if (gate.kind === 'loading' || gate.kind === 'waiting-on-provider') return
if (gate.kind === 'accept-tos') {
guardWithTos()
} else {
Expand Down Expand Up @@ -469,6 +471,7 @@ export default function WithdrawBankPage() {
handleCreateAndInitiateOfframp()
}}
onSkip={hideTos}
reasonCode={gate.kind === 'accept-tos' ? gate.reason?.code : undefined}
/>

<InitiateKycModal
Expand Down
13 changes: 13 additions & 0 deletions src/app/(mobile-ui)/withdraw/manteca/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,19 @@ export default function MantecaWithdrawFlow() {
/**
* Detect Manteca onboarding-incomplete errors and redirect user to complete their profile.
* Returns true if the error was handled (caller should return early).
*
* INTENTIONAL FALLBACK — NOT a primary code path. The KYC 2.0 architecture
* (engineering/projects/kyc-2.0/final-plan.md) centralizes all data
* collection in Sumsub and submits to Manteca via the API (`submitToManteca`
* in peanut-api-ts). The Manteca hosted onboarding widget is dead-by-design
* — but we keep this last-resort redirect for the long tail of users who
* land in an incomplete Manteca state (partial provisioning, undelivered
* initial-onboarding API call). Without this escape hatch they'd be stuck
* at withdraw time with no actionable error.
*
* Right fix: root-cause why `submitToManteca` sometimes leaves users
* half-onboarded, fix that, delete this fallback + `/manteca/initiate-onboarding`
* route + `mantecaApi.initiateOnboarding` client. Tracked separately.
*/
const handleOnboardingError = useCallback(async (error: string): Promise<boolean> => {
const onboardingErrorPatterns = ['fund origin', 'profile incomplete', 'onboarding required']
Expand Down
20 changes: 13 additions & 7 deletions src/components/AddWithdraw/AddWithdrawCountriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,12 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
const checkBridgeGate = useCallback(
(onAfterTos?: () => void): boolean => {
if (gate.kind !== 'ready') {
// capabilities still loading — caller should wait, NOT open a KYC modal
// on top of state we don't yet know (would falsely "needs_kyc" an approved
// user mid-load). Returning true keeps the caller's early-return path.
if (gate.kind === 'loading') return true
// capabilities still loading OR provider doing internal review —
// caller should wait, NOT open a KYC modal. For `loading` we
// don't yet know if the user is approved. For `waiting-on-provider`
// (Bridge KYC review, post_processing) there's no user action to
// take; opening the modal would imply otherwise.
if (gate.kind === 'loading' || gate.kind === 'waiting-on-provider') return true
if (gate.kind === 'accept-tos') {
pendingAfterTosRef.current = onAfterTos ?? null
guardWithTos()
Expand Down Expand Up @@ -142,9 +144,12 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
// unified bridge gate: tos → fixable rejection → blocked → enrollment
// return a non-visible error to prevent the form from treating this as success
if (gate.kind !== 'ready') {
// capabilities still loading — silently no-op (don't show a KYC modal on
// top of state we don't yet know).
if (gate.kind === 'loading') return { error: 'gate_blocked', silent: true }
// capabilities still loading OR provider doing internal review —
// silently no-op (don't show a KYC modal). `waiting-on-provider`
// means no user action available.
if (gate.kind === 'loading' || gate.kind === 'waiting-on-provider') {
return { error: 'gate_blocked', silent: true }
}
if (gate.kind === 'accept-tos') {
guardWithTos()
} else {
Expand Down Expand Up @@ -338,6 +343,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
else formRef.current?.handleSubmit()
}}
onSkip={hideTos}
reasonCode={gate.kind === 'accept-tos' ? gate.reason?.code : undefined}
/>
<SumsubKycModals flow={sumsubFlow} />
</>
Expand Down
11 changes: 8 additions & 3 deletions src/components/Claim/Link/views/BankFlowManager.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,13 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
// for logged-in users, check bank-rail readiness before proceeding
const isGuestFlow = bankClaimType === BankClaimType.GuestBankClaim
if (!isGuestFlow && gate.kind !== 'ready') {
// capabilities still loading — silently return; the CTA that triggered
// this should be disabled too, but defend against double-click races.
if (gate.kind === 'loading') return
// capabilities still loading OR provider doing internal review —
// silently return; the CTA that triggered this should be disabled
// too, but defend against double-click races. `waiting-on-provider`
// means there's no user action to take (Bridge KYC review,
// post_processing), so opening the KYC modal would falsely imply
// the user has something to do.
if (gate.kind === 'loading' || gate.kind === 'waiting-on-provider') return
if (gate.kind === 'accept-tos') {
guardWithTos()
} else {
Expand Down Expand Up @@ -523,6 +527,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
handleCreateOfframpAndClaim(localBankDetails)
}}
onSkip={hideTos}
reasonCode={gate.kind === 'accept-tos' ? gate.reason?.code : undefined}
/>
<InitiateKycModal
visible={showKycModal}
Expand Down
37 changes: 32 additions & 5 deletions src/components/Kyc/BridgeTosStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,38 @@ interface BridgeTosStepProps {
visible: boolean
onComplete: () => void
onSkip: () => void
/**
* BE-emitted reason code from the rail capability (`reason.code`). Used
* solely to vary copy between Bridge's base ToS (`bridge_tos_required`,
* US/ACH/Wire) and the SEPA v2 ToS (`bridge_tos_v2_required`, EUR + GBP
* inherited). The Bridge `tos_acceptance_link` endpoint is opaque to
* endorsement — Bridge serves the correct ToS based on the customer's
* pending requirements — so this prop ONLY affects user-facing copy, not
* the endpoint we call. Defaults to base copy if absent.
*/
reasonCode?: string
}

// Capability reason codes emitted by the BE resolver for Bridge ToS rails.
// Pinned as `const` so the comparison below catches typos at compile time —
// the upstream `CapabilityReason.code` is a free-form string by contract.
const BRIDGE_TOS_V2_REQUIRED = 'bridge_tos_v2_required' as const

const TOS_COPY = {
base: {
title: 'Accept Terms of Service',
description: "To enable bank transfers, you need to accept our payment partner's Terms of Service.",
},
sepa: {
title: 'Accept SEPA Terms of Service',
description:
"To enable EUR (SEPA) and GBP (Faster Payments) bank transfers, you need to accept our payment partner's updated Terms of Service.",
},
} as const

// shown immediately after sumsub kyc approval when bridge rails need ToS acceptance.
// displays a prompt, then opens the bridge ToS iframe.
export const BridgeTosStep = ({ visible, onComplete, onSkip }: BridgeTosStepProps) => {
export const BridgeTosStep = ({ visible, onComplete, onSkip, reasonCode }: BridgeTosStepProps) => {
const { fetchUser } = useAuth()
const [showIframe, setShowIframe] = useState(false)
const [tosLink, setTosLink] = useState<string | null>(null)
Expand Down Expand Up @@ -80,17 +107,17 @@ export const BridgeTosStep = ({ visible, onComplete, onSkip }: BridgeTosStepProp

if (!visible) return null

const copy = reasonCode === BRIDGE_TOS_V2_REQUIRED ? TOS_COPY.sepa : TOS_COPY.base

return (
<>
{/* confirmation modal — hidden when iframe is open or ToS is being confirmed */}
<ActionModal
visible={visible && !showIframe && !isConfirming}
onClose={onSkip}
icon={error ? ('alert' as IconName) : ('badge' as IconName)}
title={error ? 'Could not load terms' : 'Accept Terms of Service'}
description={
error || "To enable bank transfers, you need to accept our payment partner's Terms of Service."
}
title={error ? 'Could not load terms' : copy.title}
description={error || copy.description}
ctas={[
{
text: isLoading ? 'Loading...' : error ? 'Try again' : 'Accept Terms',
Expand Down
Loading
Loading