Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2a66155
feat: sumsub sdk types and declrations
kushagrasarathe Feb 13, 2026
b7a4141
feat: initiateSumsubKyc server action
kushagrasarathe Feb 13, 2026
0dd89d3
feat: useSumsubKycFlow hook setup using initiateSumsubKyc server action
kushagrasarathe Feb 13, 2026
2c2bf14
feat: handle websocket event receiving for sumsub kyc
kushagrasarathe Feb 13, 2026
27ea649
feat: SumsubKycWrapper component to handle sumsub web sdk intializati…
kushagrasarathe Feb 13, 2026
1fa243f
feat: SumsubKycFlow entry point for sumsub kyc wrapper and pending st…
kushagrasarathe Feb 13, 2026
61bd07e
fix: update verification view ux copy + add region intent type
kushagrasarathe Feb 13, 2026
2d79044
chore: fix formatting
kushagrasarathe Feb 13, 2026
e3c9bd8
feat: add sumsub provider to kyc types and shared status consts
kushagrasarathe Feb 16, 2026
b2b90e1
feat: unified kyc status hook
kushagrasarathe Feb 16, 2026
a3316cc
fix: sumsub flow, regionIntent and levelName resolution
kushagrasarathe Feb 16, 2026
6b8f838
feat: sumsub region routing
kushagrasarathe Feb 16, 2026
3188926
feat: unified kyc status drawer
kushagrasarathe Feb 16, 2026
4fb61d4
feat: sumsub kyc gate for qr payments
kushagrasarathe Feb 16, 2026
6d390ad
feat: update views for sumsub flow
kushagrasarathe Feb 16, 2026
ec0d1e9
chore: remove dead code
kushagrasarathe Feb 16, 2026
22fa6b7
Merge branch 'peanut-wallet-dev' into feat/kyc2.0
kushagrasarathe Feb 16, 2026
d0b2764
chore: format
kushagrasarathe Feb 16, 2026
e9715af
fix: address cr review comments
kushagrasarathe Feb 17, 2026
7297b9b
fix: remove levelName param and use regionIntent
kushagrasarathe Feb 17, 2026
b564cb9
fix: reuse exiting start kyc modal and fix its copy
kushagrasarathe Feb 17, 2026
9bf32ae
fix: new cr comments
kushagrasarathe Feb 17, 2026
a8ddab2
fix: more cr comments xD
kushagrasarathe Feb 17, 2026
9fbb5d8
feat: add UserRail types, rejectType and metadata to kyc verification…
kushagrasarathe Feb 18, 2026
60e9dc9
feat: add action_required status category and human-readable sumsub r…
kushagrasarathe Feb 18, 2026
fe5f07d
feat: scope region unlocking by verification regionIntent, expose act…
kushagrasarathe Feb 18, 2026
ea64d17
feat: handle sumsub_kyc_status_update websocket messages
kushagrasarathe Feb 18, 2026
7ed541a
feat: add bridge tos acceptance flow: tos step component, reminder ca…
kushagrasarathe Feb 18, 2026
235724f
feat: handle bridge tos acceptance
kushagrasarathe Feb 18, 2026
9e996b0
fix: listen to additional sumsub sdk methods
kushagrasarathe Feb 19, 2026
250cea8
feat: listen to user rail websocket events
kushagrasarathe Feb 19, 2026
40d222f
fix: move PeanutDoesntStoreAnyPersonalInformation out to a separate c…
kushagrasarathe Feb 19, 2026
2f892cb
feat: add useRailStatusTracking hook
kushagrasarathe Feb 19, 2026
f58ad8d
fix: update SumsubKycFlow and KycVerificationInProgressModal to handl…
kushagrasarathe Feb 19, 2026
8c64be9
feat: add isTerminalRejection utility and sumsubRejectType to unified…
kushagrasarathe Feb 20, 2026
302df7b
refactor: extract shared rejectlabelslist component and simplify draw…
kushagrasarathe Feb 20, 2026
8e5ab8b
fix: handle kyc status transitions for non-success terminal states
kushagrasarathe Feb 20, 2026
2bc70ba
feat: add status-aware modals for regions verification page
kushagrasarathe Feb 20, 2026
1170822
fix: re-submit verification flow and stop verification button in kyc …
kushagrasarathe Feb 20, 2026
bebaeef
fix: gate terminal rejection logic by provider and log tos retry failure
kushagrasarathe Feb 23, 2026
05029c0
docs: update kyc 2.0 testing guide with all implemented test cases
kushagrasarathe Feb 23, 2026
3c62aef
fix: remove kyc testing guide
kushagrasarathe Feb 23, 2026
4cda4bb
feat: bridge additional document collection UI
kushagrasarathe Feb 24, 2026
91d7b1f
chore: format
kushagrasarathe Feb 24, 2026
bcb698c
fix: address code review findings
kushagrasarathe Feb 24, 2026
10f5ee5
fix: aggregate additional requirements across all rails, guard termin…
kushagrasarathe Feb 24, 2026
1b4a9e7
fix: label copy
kushagrasarathe Feb 24, 2026
3c929c2
Merge pull request #1689 from peanutprotocol/feat/kyc2.0-provider-res…
kushagrasarathe Feb 24, 2026
908e38d
Merge pull request #1683 from peanutprotocol/feat/kyc2.0-error-retry-ui
kushagrasarathe Feb 24, 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
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 @@ -10,7 +10,7 @@ import { UserHeader } from '@/components/UserHeader'
import { useAuth } from '@/context/authContext'
import { useWallet } from '@/hooks/wallet/useWallet'
import { useUserStore } from '@/redux/hooks'
import { formatExtendedNumber, getUserPreferences, updateUserPreferences, getRedirectUrl } from '@/utils/general.utils'
import { formatExtendedNumber, getUserPreferences, updateUserPreferences } from '@/utils/general.utils'
import { printableUsdc } from '@/utils/balance.utils'
import { useDisconnect } from '@reown/appkit/react'
import Link from 'next/link'
Expand All @@ -24,7 +24,7 @@ import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts'
import { PostSignupActionManager } from '@/components/Global/PostSignupActionManager'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType'
import { useDeviceType } from '@/hooks/useGetDeviceType'
import { useNotifications } from '@/hooks/useNotifications'
import useKycStatus from '@/hooks/useKycStatus'
import { useCardPioneerInfo } from '@/hooks/useCardPioneerInfo'
Expand Down

This file was deleted.

This file was deleted.

54 changes: 1 addition & 53 deletions src/app/(mobile-ui)/profile/identity-verification/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,7 @@
'use client'

import PageContainer from '@/components/0_Bruddle/PageContainer'
import ActionModal from '@/components/Global/ActionModal'
import { useIdentityVerification } from '@/hooks/useIdentityVerification'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'

export default function IdentityVerificationLayout({ children }: { children: React.ReactNode }) {
const [isAlreadyVerifiedModalOpen, setIsAlreadyVerifiedModalOpen] = useState(false)
const router = useRouter()
const { isRegionAlreadyUnlocked, isVerifiedForCountry } = useIdentityVerification()
const params = useParams()
const regionParams = params.region as string
const countryParams = params.country as string

useEffect(() => {
const isAlreadyVerified =
(countryParams && isVerifiedForCountry(countryParams)) ||
(regionParams && isRegionAlreadyUnlocked(regionParams))

if (isAlreadyVerified) {
setIsAlreadyVerifiedModalOpen(true)
}
}, [countryParams, regionParams, isVerifiedForCountry, isRegionAlreadyUnlocked])

return (
<PageContainer>
{children}

<ActionModal
visible={isAlreadyVerifiedModalOpen}
onClose={() => {
setIsAlreadyVerifiedModalOpen(false)
router.push('/profile')
}}
title="You're already verified"
description={
<p>
Your identity has already been successfully verified for this region. You can continue to use
features available in this region. No further action is needed.
</p>
}
icon="shield"
ctas={[
{
text: 'Close',
shadowSize: '4',
className: 'md:py-2',
onClick: () => {
setIsAlreadyVerifiedModalOpen(false)
router.push('/profile')
},
},
]}
/>
</PageContainer>
)
return <PageContainer>{children}</PageContainer>
}
2 changes: 1 addition & 1 deletion src/app/(mobile-ui)/qr-pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useSearchParams, useRouter } from 'next/navigation'
import { useState, useCallback, useMemo, useEffect, useContext, useRef } from 'react'
import { PeanutDoesntStoreAnyPersonalInformation } from '@/components/Kyc/KycVerificationInProgressModal'
import { PeanutDoesntStoreAnyPersonalInformation } from '@/components/Kyc/PeanutDoesntStoreAnyPersonalInformation'
import Card from '@/components/Global/Card'
import { Button } from '@/components/0_Bruddle/Button'
import { Icon } from '@/components/Global/Icons/Icon'
Expand Down
54 changes: 54 additions & 0 deletions src/app/actions/sumsub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use server'

import { type InitiateSumsubKycResponse, type KYCRegionIntent } from './types/sumsub.types'
import { fetchWithSentry } from '@/utils/sentry.utils'
import { PEANUT_API_URL } from '@/constants/general.consts'
import { getJWTCookie } from '@/utils/cookie-migration.utils'

const API_KEY = process.env.PEANUT_API_KEY!

// initiate kyc flow (using sumsub) and get websdk access token
export const initiateSumsubKyc = async (params?: {
regionIntent?: KYCRegionIntent
levelName?: string
}): Promise<{ data?: InitiateSumsubKycResponse; error?: string }> => {
const jwtToken = (await getJWTCookie())?.value

if (!jwtToken) {
return { error: 'Authentication required' }
}

const body: Record<string, string | undefined> = {
regionIntent: params?.regionIntent,
levelName: params?.levelName,
}

try {
const response = await fetchWithSentry(`${PEANUT_API_URL}/users/identity`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
'api-key': API_KEY,
},
body: JSON.stringify(body),
})

const responseJson = await response.json()

if (!response.ok) {
return { error: responseJson.message || responseJson.error || 'Failed to initiate identity verification' }
}

return {
data: {
token: responseJson.token,
applicantId: responseJson.applicantId,
status: responseJson.status,
},
}
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'An unexpected error occurred'
return { error: message }
}
}
9 changes: 9 additions & 0 deletions src/app/actions/types/sumsub.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface InitiateSumsubKycResponse {
token: string | null // null when user is already APPROVED
applicantId: string | null
status: SumsubKycStatus
}

export type SumsubKycStatus = 'NOT_STARTED' | 'PENDING' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ACTION_REQUIRED'

export type KYCRegionIntent = 'STANDARD' | 'LATAM'
44 changes: 44 additions & 0 deletions src/app/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,47 @@ export async function getContacts(params: {
return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}

// fetch bridge ToS acceptance link for users with pending ToS
export const getBridgeTosLink = async (): Promise<{ data?: { tosLink: string }; error?: string }> => {
const jwtToken = (await getJWTCookie())?.value
try {
const response = await fetchWithSentry(`${PEANUT_API_URL}/users/bridge-tos-link`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
'api-key': API_KEY,
},
})
const responseJson = await response.json()
if (!response.ok) {
return { error: responseJson.error || 'Failed to fetch Bridge ToS link' }
}
return { data: responseJson }
} catch (e: unknown) {
return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}

// confirm bridge ToS acceptance after user closes the ToS iframe
export const confirmBridgeTos = async (): Promise<{ data?: { accepted: boolean }; error?: string }> => {
const jwtToken = (await getJWTCookie())?.value
try {
const response = await fetchWithSentry(`${PEANUT_API_URL}/users/bridge-tos-confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
'api-key': API_KEY,
},
})
const responseJson = await response.json()
if (!response.ok) {
return { error: responseJson.error || 'Failed to confirm Bridge ToS' }
}
return { data: responseJson }
} catch (e: unknown) {
return { error: e instanceof Error ? e.message : 'An unexpected error occurred' }
}
}
6 changes: 5 additions & 1 deletion src/components/Global/Badges/StatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const StatusBadge: React.FC<StatusBadgeProps> = ({ status, className, size = 'sm
}

const getStatusText = () => {
// customText overrides the default label for any status type,
// allowing callers to use a specific status style with custom text
if (customText) return customText

switch (status) {
case 'completed':
return 'Completed'
Expand All @@ -59,7 +63,7 @@ const StatusBadge: React.FC<StatusBadgeProps> = ({ status, className, size = 'sm
case 'closed':
return 'Closed'
case 'custom':
return customText
return 'Custom'
default:
return status
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Global/IframeWrapper/StartVerificationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ const StartVerificationView = ({
<h1 className="text-3xl font-extrabold">Secure Verification. Limited Data Use.</h1>
<div>
<p className="mt-2 text-lg font-medium">
The verification is done by Persona, which only shares a yes/no with Peanut.
The verification is done using a trusted provider, which shares your verification status with
Peanut.
</p>
<p className="text-lg font-medium">
Persona is trusted by millions and it operates under strict security and privacy standards.
It operates under industry-standard security and privacy practices.
</p>
<p className="text-lg font-bold">Peanut never sees or stores your verification data.</p>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/components/Home/HomeHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import Card from '../Global/Card'
import { type CardPosition, getCardPosition } from '../Global/Card/card.utils'
import EmptyState from '../Global/EmptyStates/EmptyState'
import { KycStatusItem } from '../Kyc/KycStatusItem'
import { BridgeTosReminder } from '../Kyc/BridgeTosReminder'
import { isKycStatusItem, type KycHistoryEntry } from '@/hooks/useBridgeKycFlow'
import { useBridgeTosStatus } from '@/hooks/useBridgeTosStatus'
import { useWallet } from '@/hooks/wallet/useWallet'
import { BadgeStatusItem } from '@/components/Badges/BadgeStatusItem'
import { isBadgeHistoryItem } from '@/components/Badges/badge.types'
Expand Down Expand Up @@ -43,6 +45,7 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h
const { fetchBalance } = useWallet()
const { triggerHaptic } = useHaptic()
const { fetchUser } = useAuth()
const { needsBridgeTos } = useBridgeTosStatus()

const isViewingOwnHistory = useMemo(
() => (isLoggedIn && !username) || (isLoggedIn && username === user?.user.username),
Expand Down Expand Up @@ -270,6 +273,7 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h
return (
<div className="mx-auto mt-6 w-full space-y-3 md:max-w-2xl">
<h2 className="text-base font-bold">Activity</h2>
{isViewingOwnHistory && needsBridgeTos && <BridgeTosReminder />}
{isViewingOwnHistory &&
((user?.user.bridgeKycStatus && user?.user.bridgeKycStatus !== 'not_started') ||
(user?.user.kycVerifications && user?.user.kycVerifications.length > 0)) && (
Expand Down Expand Up @@ -317,6 +321,9 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h

return (
<div className={twMerge('mx-auto w-full space-y-3 md:max-w-2xl md:space-y-3', isLoggedIn ? 'pb-4' : 'pb-0')}>
{/* bridge ToS reminder for users who haven't accepted yet */}
{isViewingOwnHistory && needsBridgeTos && <BridgeTosReminder />}

{/* link to the full history page */}
{pendingRequests.length > 0 && (
<>
Expand Down
9 changes: 6 additions & 3 deletions src/components/Home/KycCompletedModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,25 @@ const KycCompletedModal = ({ isOpen, onClose }: { isOpen: boolean; onClose: () =
const { user } = useAuth()
const [approvedCountryData, setApprovedCountryData] = useState<CountryData | null>(null)

const { isUserBridgeKycApproved, isUserMantecaKycApproved } = useKycStatus()
const { isUserBridgeKycApproved, isUserMantecaKycApproved, isUserSumsubKycApproved } = useKycStatus()
const { getVerificationUnlockItems } = useIdentityVerification()

const kycApprovalType = useMemo(() => {
// sumsub covers all regions, treat as 'all'
if (isUserSumsubKycApproved) {
return 'all'
}
if (isUserBridgeKycApproved && isUserMantecaKycApproved) {
return 'all'
}

if (isUserBridgeKycApproved) {
return 'bridge'
}
if (isUserMantecaKycApproved) {
return 'manteca'
}
return 'none'
}, [isUserBridgeKycApproved, isUserMantecaKycApproved])
}, [isUserBridgeKycApproved, isUserMantecaKycApproved, isUserSumsubKycApproved])

const items = useMemo(() => {
return getVerificationUnlockItems(approvedCountryData?.title)
Expand Down
Loading
Loading