diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx
index bdcba1dc2..5a5330ce2 100644
--- a/src/app/(mobile-ui)/home/page.tsx
+++ b/src/app/(mobile-ui)/home/page.tsx
@@ -20,7 +20,6 @@ import { twMerge } from 'tailwind-merge'
import { useAccount } from 'wagmi'
// import ReferralCampaignModal from '@/components/Home/ReferralCampaignModal'
// import FloatingReferralButton from '@/components/Home/FloatingReferralButton'
-import { AccountType } from '@/interfaces'
import { formatUnits } from 'viem'
import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants'
import { PostSignupActionManager } from '@/components/Global/PostSignupActionManager'
@@ -51,7 +50,7 @@ const BALANCE_WARNING_EXPIRY = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_
export default function Home() {
const { showPermissionModal } = useNotifications()
- const { balance, address, isFetchingBalance } = useWallet()
+ const { balance, isFetchingBalance } = useWallet()
const { resetFlow: resetClaimBankFlow } = useClaimBankFlow()
const { resetWithdrawFlow } = useWithdrawFlow()
const { deviceType } = useDeviceType()
@@ -64,7 +63,7 @@ export default function Home() {
const { disconnect: disconnectWagmi } = useDisconnect()
const { triggerHaptic } = useHaptic()
- const { isFetchingUser, addAccount, fetchUser } = useAuth()
+ const { isFetchingUser, fetchUser } = useAuth()
const { isUserKycApproved } = useKycStatus()
const username = user?.user.username
@@ -107,19 +106,6 @@ export default function Home() {
resetWithdrawFlow()
}, [resetClaimBankFlow, resetWithdrawFlow])
- useEffect(() => {
- if (isFetchingUser) return
- // We have some users that didn't have the peanut wallet created
- // correctly, so we need to create it
- if (address && user && !user.accounts.some((a) => a.type === AccountType.PEANUT_WALLET)) {
- addAccount({
- accountIdentifier: address,
- accountType: 'peanut-wallet',
- userId: user.user.userId,
- })
- }
- }, [user, address, isFetchingUser])
-
// always reset external wallet connection on home page
useEffect(() => {
if (isWagmiConnected) {
diff --git a/src/app/(mobile-ui)/layout.tsx b/src/app/(mobile-ui)/layout.tsx
index b80369424..8c71adeca 100644
--- a/src/app/(mobile-ui)/layout.tsx
+++ b/src/app/(mobile-ui)/layout.tsx
@@ -22,6 +22,7 @@ import ForceIOSPWAInstall from '@/components/ForceIOSPWAInstall'
import { PUBLIC_ROUTES_REGEX } from '@/constants/routes'
import { usePullToRefresh } from '@/hooks/usePullToRefresh'
import { useNetworkStatus } from '@/hooks/useNetworkStatus'
+import { useAccountSetupRedirect } from '@/hooks/useAccountSetupRedirect'
const Layout = ({ children }: { children: React.ReactNode }) => {
const pathName = usePathname()
@@ -80,6 +81,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
}
}, [user, isFetchingUser, isReady, isPublicPath, router])
+ // redirect logged-in users without peanut wallet account to complete setup
+ const { needsRedirect, isCheckingAccount } = useAccountSetupRedirect()
+
// show full-page offline screen when user is offline
// only show after initialization to prevent flash on initial load
// when connection is restored, page auto-reloads (no "back online" screen)
@@ -97,8 +101,8 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
)
}
} else {
- // For protected paths, wait for user auth
- if (!isReady || isFetchingUser || !hasToken || !user) {
+ // for protected paths, wait for user auth and account setup check
+ if (!isReady || isFetchingUser || !hasToken || !user || needsRedirect || isCheckingAccount) {
return (
diff --git a/src/app/(setup)/setup/finish/page.tsx b/src/app/(setup)/setup/finish/page.tsx
new file mode 100644
index 000000000..fe07da29e
--- /dev/null
+++ b/src/app/(setup)/setup/finish/page.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import { Suspense } from 'react'
+import PeanutLoading from '@/components/Global/PeanutLoading'
+import { SetupWrapper } from '@/components/Setup/components/SetupWrapper'
+import SignTestTransaction from '@/components/Setup/Views/SignTestTransaction'
+import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif'
+
+/**
+ * finish setup page for users who logged in but haven't completed account setup
+ * shows test transaction step to verify passkey works before finalizing
+ */
+function FinishSetupPageContent() {
+ return (
+
+
+
+ )
+}
+
+export default function FinishSetupPage() {
+ return (
+
}>
+
+
+ )
+}
diff --git a/src/components/Global/GuestLoginCta/index.tsx b/src/components/Global/GuestLoginCta/index.tsx
deleted file mode 100644
index d7dc57aa3..000000000
--- a/src/components/Global/GuestLoginCta/index.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Button } from '@/components/0_Bruddle'
-import Divider from '@/components/0_Bruddle/Divider'
-import { useToast } from '@/components/0_Bruddle/Toast'
-import { useZeroDev } from '@/hooks/useZeroDev'
-import { sanitizeRedirectURL, saveRedirectUrl } from '@/utils'
-import { initializeAppKit } from '@/config/wagmi.config'
-import { useAppKit } from '@reown/appkit/react'
-import * as Sentry from '@sentry/nextjs'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { useEffect } from 'react'
-
-interface GuestLoginCtaProps {
- hideConnectWallet?: boolean
- view?: 'CLAIM' | 'REQUEST'
-}
-
-const GuestLoginCta = ({ hideConnectWallet = false, view }: GuestLoginCtaProps) => {
- const { handleLogin, isLoggingIn, address: passkeyAddress } = useZeroDev()
- const toast = useToast()
- const router = useRouter()
- const { open: openReownModal } = useAppKit()
- const searchParams = useSearchParams()
-
- // If user already has a passkey address, auto-redirect to avoid double prompting
- useEffect(() => {
- if (passkeyAddress && !isLoggingIn) {
- console.log('User already has passkey wallet:', passkeyAddress)
- }
- }, [passkeyAddress, isLoggingIn])
-
- const handleSignUp = () => {
- saveRedirectUrl()
- router.push('/setup')
- }
-
- const handleLoginClick = async () => {
- // Prevent double login attempts
- if (isLoggingIn || passkeyAddress) {
- return
- }
-
- try {
- await handleLogin()
- const redirect_uri = searchParams.get('redirect_uri')
- if (redirect_uri) {
- const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri)
- // Only redirect if the URL is safe (same-origin)
- if (sanitizedRedirectUrl) {
- router.push(sanitizedRedirectUrl)
- } else {
- // If redirect_uri was invalid, stay on current page
- Sentry.captureException(`Invalid redirect URL ${redirect_uri}`)
- }
- }
- } catch (e) {
- toast.error('Error logging in')
- Sentry.captureException(e)
- }
- }
-
- return (
-
- {/* Primary Sign Up Button */}
-
-
- {/* Secondary Log In Button */}
-
-
- {/* "or" divider and External Wallet Button */}
- {!hideConnectWallet && (
- <>
-
-
- >
- )}
-
- )
-}
-
-export default GuestLoginCta
diff --git a/src/components/Setup/Setup.consts.tsx b/src/components/Setup/Setup.consts.tsx
index d941513cb..c3e3a46d4 100644
--- a/src/components/Setup/Setup.consts.tsx
+++ b/src/components/Setup/Setup.consts.tsx
@@ -1,11 +1,9 @@
import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif'
-import peanutWithGlassesAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_06.gif'
import happyPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_03.gif'
import { PEANUTMAN_MOBILE, ThinkingPeanut } from '@/assets'
import type { ISetupStep } from '@/components/Setup/Setup.types'
-import { InstallPWA, SetupPasskey, SignupStep, JoinBetaStep, CollectEmail, LandingStep } from '@/components/Setup/Views'
+import { InstallPWA, SetupPasskey, SignupStep, LandingStep, SignTestTransaction } from '@/components/Setup/Views'
import JoinWaitlist from './Views/JoinWaitlist'
-import ForceIOSPWAInstall from '../ForceIOSPWAInstall'
export const setupSteps: ISetupStep[] = [
{
@@ -89,12 +87,12 @@ export const setupSteps: ISetupStep[] = [
contentClassName: 'flex flex-col items-end pt-8 justify-center gap-5',
},
{
- screenId: 'collect-email',
+ screenId: 'sign-test-transaction',
layoutType: 'signup',
- title: 'Stay in the loop',
- description: 'Enter your email to finish setup. We’ll send you an update as soon as you get access.',
+ title: 'Sign a test transaction',
+ description: "Let's make sure your passkey is working and you have everything set up correctly.",
image: chillPeanutAnim.src,
- component: CollectEmail,
+ component: SignTestTransaction,
showBackButton: false,
showSkipButton: false,
contentClassName: 'flex flex-col items-center justify-center gap-5',
diff --git a/src/components/Setup/Setup.types.ts b/src/components/Setup/Setup.types.ts
index f42079faf..edd15da32 100644
--- a/src/components/Setup/Setup.types.ts
+++ b/src/components/Setup/Setup.types.ts
@@ -11,7 +11,7 @@ export type ScreenId =
| 'success'
| 'unsupported-browser'
| 'join-beta'
- | 'collect-email'
+ | 'sign-test-transaction'
export type LayoutType = 'signup' | 'standard' | 'android-initial-pwa-install'
@@ -31,7 +31,7 @@ export type ScreenProps = {
'android-initial-pwa-install': undefined
'unsupported-browser': undefined
'join-beta': undefined
- 'collect-email': undefined
+ 'sign-test-transaction': undefined
}
export interface StepComponentProps {
diff --git a/src/components/Setup/Views/CollectEmail.tsx b/src/components/Setup/Views/CollectEmail.tsx
deleted file mode 100644
index e1d7f3c28..000000000
--- a/src/components/Setup/Views/CollectEmail.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-'use client'
-
-import { updateUserById } from '@/app/actions/users'
-import { Button } from '@/components/0_Bruddle'
-import ErrorAlert from '@/components/Global/ErrorAlert'
-import ValidatedInput from '@/components/Global/ValidatedInput'
-import { useAuth } from '@/context/authContext'
-import { useRouter } from 'next/navigation'
-import React, { useState } from 'react'
-import { twMerge } from 'tailwind-merge'
-
-const CollectEmail = () => {
- const [email, setEmail] = useState('')
- const [isValid, setIsValid] = useState(false)
- const [isChanging, setIsChanging] = useState(false)
- const [isLoading, setisLoading] = useState(false)
- const [error, setError] = useState('')
- const { user } = useAuth()
- const router = useRouter()
-
- const validateEmail = async (email: string) => {
- const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
- return emailRegex.test(email)
- }
-
- const handleFinishSetup = async () => {
- try {
- setError('')
- setisLoading(true)
- const { error } = await updateUserById({
- userId: user?.user.userId,
- email,
- })
-
- if (error && error.includes('Unique constraint failed on the fields: (`email`)')) {
- setError('Email already in use.')
- return
- }
-
- if (error) {
- setError('Something went wrong. Please try again or contact support')
- return
- }
- router.push('/home')
- } catch {
- setError('Something went wrong. Please try again or contact support')
- } finally {
- setisLoading(false)
- }
- }
-
- return (
-
- {
- setIsValid(isValid)
- setIsChanging(isChanging)
- setEmail(value)
- }}
- isSetupFlow
- isInputChanging={isChanging}
- className={twMerge(
- !isValid && !isChanging && !!email && 'border-error dark:border-error',
- isValid && !isChanging && !!email && 'border-secondary-8 dark:border-secondary-8',
- 'rounded-sm'
- )}
- />
-
-
-
- {(!!error || (!isValid && !isChanging && !!email)) && }
-
- )
-}
-
-export default CollectEmail
diff --git a/src/components/Setup/Views/SetupPasskey.tsx b/src/components/Setup/Views/SetupPasskey.tsx
index 781fcb559..f43122fef 100644
--- a/src/components/Setup/Views/SetupPasskey.tsx
+++ b/src/components/Setup/Views/SetupPasskey.tsx
@@ -6,115 +6,46 @@ import { useSetupFlow } from '@/hooks/useSetupFlow'
import { useAuth } from '@/context/authContext'
import { useDeviceType } from '@/hooks/useGetDeviceType'
import { useEffect, useState } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
import * as Sentry from '@sentry/nextjs'
-import { WalletProviderType, AccountType } from '@/interfaces'
import { WebAuthnError } from '@simplewebauthn/browser'
import Link from 'next/link'
-import {
- getFromCookie,
- getRedirectUrl,
- getValidRedirectUrl,
- clearRedirectUrl,
- clearAuthState,
- withWebAuthnRetry,
- getWebAuthnErrorMessage,
-} from '@/utils'
-import { POST_SIGNUP_ACTIONS } from '@/components/Global/PostSignupActionManager/post-signup-action.consts'
+import { clearAuthState, withWebAuthnRetry, getWebAuthnErrorMessage } from '@/utils'
const SetupPasskey = () => {
const dispatch = useAppDispatch()
- const { username, telegramHandle, inviteCode } = useSetupStore()
+ const { username } = useSetupStore()
const { isLoading, handleNext } = useSetupFlow()
const { handleRegister, address } = useZeroDev()
- const { user, isFetchingUser } = useAuth()
- const { addAccount } = useAuth()
+ const { user } = useAuth()
const { deviceType } = useDeviceType()
const [error, setError] = useState
(null)
- const router = useRouter()
- const searchParams = useSearchParams()
+ // once passkey is registered successfully, move to test transaction step
useEffect(() => {
- // Dont try to double add the account
- if (isFetchingUser || user?.accounts.some((a) => a.type === AccountType.PEANUT_WALLET)) return
- if (address && user) {
- addAccount({
- accountIdentifier: address,
- accountType: WalletProviderType.PEANUT,
- userId: user?.user.userId as string,
- telegramHandle: telegramHandle.length > 0 ? telegramHandle : undefined,
- })
- .then(() => {
- const inviteCodeFromCookie = getFromCookie('inviteCode')
-
- const userInviteCode = inviteCode || inviteCodeFromCookie
-
- // if no invite code, go to collect email step
- if (!userInviteCode) {
- handleNext()
- return
- }
-
- const redirect_uri = searchParams.get('redirect_uri')
- if (redirect_uri) {
- const validRedirectUrl = getValidRedirectUrl(redirect_uri, '/home')
- // Only redirect if the URL is safe (same-origin)
- router.push(validRedirectUrl)
- return
- // If redirect_uri was invalid, fall through to other redirect logic
- }
-
- const localStorageRedirect = getRedirectUrl()
- // redirect based on post signup action config
- if (localStorageRedirect) {
- const matchedAction = POST_SIGNUP_ACTIONS.find((action) =>
- action.pathPattern.test(localStorageRedirect)
- )
- if (matchedAction) {
- router.push('/home')
- } else {
- clearRedirectUrl()
- const validRedirectUrl = getValidRedirectUrl(localStorageRedirect, '/home')
- router.push(validRedirectUrl)
- }
- } else {
- router.push('/home')
- }
- })
- .catch((e) => {
- Sentry.captureException(e)
- console.error('Error adding account', e)
- setError('Error adding account. Please try refreshing the page.')
-
- // CRITICAL FIX: Clear auth state if account creation fails
- // This prevents the user from getting stuck in an unrecoverable state
- clearAuthState(user?.user.userId)
- })
- .finally(() => {
- dispatch(setupActions.setLoading(false))
- })
+ if (address) {
+ handleNext()
}
- }, [address, user, isFetchingUser])
+ }, [address, handleNext])
return (
{/* amount and status on the right side */}
-
-
-
{displayAmount}
- {currencyDisplayAmount && (
-
{currencyDisplayAmount}
- )}
+ {isTestTransaction ? (
+
+ ) : (
+
+
+ {displayAmount}
+ {currencyDisplayAmount && (
+ {currencyDisplayAmount}
+ )}
+
-
+ )}
diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
index 1abb54d6e..f32c19cd2 100644
--- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
+++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
@@ -13,6 +13,7 @@ import { VerifiedUserLabel } from '../UserHeader'
import ProgressBar from '../Global/ProgressBar'
import { useRouter } from 'next/navigation'
import { twMerge } from 'tailwind-merge'
+import { PEANUTMAN_LOGO } from '@/assets'
export type TransactionDirection =
| 'send'
@@ -78,6 +79,11 @@ const getTitle = (
} else {
const isAddress = isWalletAddress(userName)
const displayName = isAddress ? printableAddress(userName) : userName
+
+ // check if this is a test transaction (setup confirmation)
+ // note: bad check, but its a quick fix for now - kush (18 nov 2025), to be handled in the backend post devconnect.
+ const isTestTransaction = displayName === 'Enjoy Peanut!'
+
switch (direction) {
case 'send':
if (status === 'pending' || status === 'cancelled') {
@@ -112,7 +118,11 @@ const getTitle = (
break
case 'add':
case 'bank_deposit':
- titleText = `${status === 'completed' ? 'Added' : 'Adding'} from ${displayName}`
+ if (isTestTransaction) {
+ titleText = 'Enjoy Peanut!'
+ } else {
+ titleText = `${status === 'completed' ? 'Added' : 'Adding'} from ${displayName}`
+ }
break
case 'claim_external':
if (status === 'completed') {
@@ -141,8 +151,12 @@ const getTitle = (
return titleText
}
-const getIcon = (direction: TransactionDirection, isLinkTransaction?: boolean): IconName | undefined => {
- if (isLinkTransaction) {
+const getIcon = (
+ direction: TransactionDirection,
+ isLinkTransaction?: boolean,
+ isTestTransaction?: boolean
+): IconName | undefined => {
+ if (isLinkTransaction || isTestTransaction) {
return undefined
}
@@ -195,7 +209,9 @@ export const TransactionDetailsHeaderCard: React.FC
{
if (isAvatarClickable) {
@@ -207,65 +223,77 @@ export const TransactionDetailsHeaderCard: React.FC
-
-
- {avatarUrl ? (
-
-
-
- ) : (
-
- )}
+ {isTestTransaction ? (
+
+
+
+
+
+
Enjoy Peanut!
+
-
-
- {icon && }
-
-
-
- {status && }
-
-
-
+
+ {avatarUrl ? (
+
+
+
+ ) : (
+
)}
- >
- {amountDisplay}
-
+
+
+
+ {icon && }
+
- {convertedAmount && ≈ {convertedAmount}
}
+
+ {status && }
+
+
+
+ {amountDisplay}
+
- {isNoGoalSet &&
No goal set
}
+ {convertedAmount &&
≈ {convertedAmount}
}
+
+ {isNoGoalSet &&
No goal set
}
+
-
+ )}
+
{!isNoGoalSet && showProgessBar && goal !== undefined && progress !== undefined && (
diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx
index fdfd53c27..8e912bf35 100644
--- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx
+++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx
@@ -61,9 +61,9 @@ import {
import { mantecaApi } from '@/services/manteca'
import { getReceiptUrl } from '@/utils/history.utils'
import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'
-import TransactionCard from './TransactionCard'
import ContributorCard from '../Global/Contributors/ContributorCard'
import { requestsApi } from '@/services/requests'
+import { PasskeyDocsLink } from '../Setup/Views/SignTestTransaction'
export const TransactionDetailsReceipt = ({
transaction,
@@ -1387,14 +1387,18 @@ export const TransactionDetailsReceipt = ({
)}
- {/* support link section */}
-
setIsSupportModalOpen(true)}
- className="flex w-full items-center justify-center gap-2 text-sm font-medium text-grey-1 underline transition-colors hover:text-black"
- >
-
- Issues with this transaction?
-
+ {/* support link section or passkey docs for test transactions */}
+ {transaction.userName === 'Enjoy Peanut!' ? (
+
+ ) : (
+
setIsSupportModalOpen(true)}
+ className="flex w-full items-center justify-center gap-2 text-sm font-medium text-grey-1 underline transition-colors hover:text-black"
+ >
+
+ Issues with this transaction?
+
+ )}
{/* Cancel Link Modal */}
diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts
index 214702933..0a271bdc1 100644
--- a/src/components/TransactionDetails/transactionTransformer.ts
+++ b/src/components/TransactionDetails/transactionTransformer.ts
@@ -327,12 +327,19 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact
nameForDetails = 'Bank Account'
isPeerActuallyUser = false
break
- case EHistoryEntryType.DEPOSIT:
+ case EHistoryEntryType.DEPOSIT: {
direction = 'add'
transactionCardType = 'add'
- nameForDetails = entry.senderAccount?.identifier || 'Deposit Source'
+ // check if this is a test transaction (0 amount deposit during account setup), ideally this should be handled in the backend, but for now we'll handle it here cuz its a quick fix, and in promisland of post devconnect this should be handled in the backend.
+ const isTestTransaction = String(entry.amount) === '0' || entry.extraData?.usdAmount === '0'
+ if (isTestTransaction) {
+ nameForDetails = 'Enjoy Peanut!'
+ } else {
+ nameForDetails = entry.senderAccount?.identifier || 'Deposit Source'
+ }
isPeerActuallyUser = false
break
+ }
case EHistoryEntryType.MANTECA_QR_PAYMENT:
direction = 'qr_payment'
transactionCardType = 'pay'
@@ -502,6 +509,10 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact
// if showFullName is false or undefined, use username; otherwise use fullName
const nameForInitials = showFullName && fullName ? fullName : nameForDetails
+ // check if this is a test transaction for adding a memo
+ const isTestDeposit =
+ entry.type === EHistoryEntryType.DEPOSIT && (String(entry.amount) === '0' || entry.extraData?.usdAmount === '0')
+
// build the final transactiondetails object for the ui
const transactionDetails: TransactionDetails = {
id: entry.uuid,
@@ -519,7 +530,7 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact
// only show verification badge if the other person is a peanut user
date: new Date(entry.timestamp),
fee: undefined,
- memo: entry.memo?.trim(),
+ memo: isTestDeposit ? 'Your peanut wallet is ready to use!' : entry.memo?.trim(),
attachmentUrl: entry.attachmentUrl,
cancelledDate: entry.cancelledAt,
txHash: entry.txHash,
diff --git a/src/context/authContext.tsx b/src/context/authContext.tsx
index 165bcd4d7..213f4a1ec 100644
--- a/src/context/authContext.tsx
+++ b/src/context/authContext.tsx
@@ -2,8 +2,9 @@
import { useToast } from '@/components/0_Bruddle/Toast'
import { useUserQuery } from '@/hooks/query/user'
import * as interfaces from '@/interfaces'
-import { useAppDispatch, useUserStore } from '@/redux/hooks'
+import { useAppDispatch } from '@/redux/hooks'
import { setupActions } from '@/redux/slices/setup-slice'
+import { zerodevActions } from '@/redux/slices/zerodev-slice'
import {
fetchWithSentry,
removeFromCookie,
@@ -13,7 +14,7 @@ import {
} from '@/utils'
import { resetCrispProxySessions } from '@/utils/crisp'
import { useQueryClient } from '@tanstack/react-query'
-import { useRouter, usePathname } from 'next/navigation'
+import { useRouter } from 'next/navigation'
import { createContext, type ReactNode, useContext, useState, useEffect, useMemo, useCallback } from 'react'
import { captureException } from '@sentry/nextjs'
// import { PUBLIC_ROUTES_REGEX } from '@/constants/routes'
@@ -137,13 +138,23 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
})
if (response.ok) {
+ // clear user preferences (webauthn key in localStorage)
updateUserPreferences(user?.user.userId, { webAuthnKey: undefined })
+
+ // clear cookies
removeFromCookie(WEB_AUTHN_COOKIE_KEY)
+ document.cookie = 'jwt-token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
+
+ // clear redirect url
clearRedirectUrl()
+
+ // invalidate all queries
queryClient.invalidateQueries()
- // clear JWT cookie by setting it to expire
- document.cookie = 'jwt-token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
+ // reset redux state (setup and zerodev)
+ dispatch(setupActions.resetSetup())
+ dispatch(zerodevActions.resetZeroDevState())
+ console.log('[Logout] Cleared redux state (setup and zerodev)')
// Clear service worker caches to prevent user data leakage
// When User A logs out and User B logs in on the same device, cached API responses
@@ -177,8 +188,10 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
resetCrispProxySessions()
}
+ // fetch user (should return null after logout)
await fetchUser()
- dispatch(setupActions.resetSetup())
+
+ // redirect to setup page
router.replace('/setup')
toast.success('Logged out successfully')
diff --git a/src/context/kernelClient.context.tsx b/src/context/kernelClient.context.tsx
index e6d75d4a6..8d2597ef2 100644
--- a/src/context/kernelClient.context.tsx
+++ b/src/context/kernelClient.context.tsx
@@ -163,7 +163,14 @@ export const KernelClientProvider = ({ children }: { children: ReactNode }) => {
// lifecycle hooks
useEffect(() => {
- if (!user?.user.userId) return
+ if (!user?.user.userId) {
+ // clear webauthn key and clients when user logs out
+ console.log('[KernelClient] No user found, clearing webAuthnKey and clients')
+ setWebAuthnKey(undefined)
+ setClientsByChain({})
+ return
+ }
+
const userPreferences = getUserPreferences(user.user.userId)
const storedWebAuthnKey = userPreferences?.webAuthnKey ?? getFromCookie(WEB_AUTHN_COOKIE_KEY)
if (storedWebAuthnKey) {
diff --git a/src/hooks/useAccountSetup.ts b/src/hooks/useAccountSetup.ts
new file mode 100644
index 000000000..5594a0001
--- /dev/null
+++ b/src/hooks/useAccountSetup.ts
@@ -0,0 +1,95 @@
+import { useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import * as Sentry from '@sentry/nextjs'
+import { useAuth } from '@/context/authContext'
+import { WalletProviderType } from '@/interfaces'
+import { getRedirectUrl, getValidRedirectUrl, clearRedirectUrl, clearAuthState } from '@/utils'
+import { POST_SIGNUP_ACTIONS } from '@/components/Global/PostSignupActionManager/post-signup-action.consts'
+import { useSetupStore } from '@/redux/hooks'
+
+/**
+ * shared hook for finalizing account setup after test transaction succeeds
+ * handles adding account to db and navigation logic
+ */
+export const useAccountSetup = () => {
+ const { user } = useAuth()
+ const { addAccount } = useAuth()
+ const { telegramHandle } = useSetupStore()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const [error, setError] = useState
(null)
+ const [isProcessing, setIsProcessing] = useState(false)
+
+ /**
+ * finalize account setup by adding account to db and navigating
+ */
+ const finalizeAccountSetup = async (address: string) => {
+ console.log('[useAccountSetup] Starting account finalization', { address, userId: user?.user.userId })
+
+ if (!user) {
+ console.error('[useAccountSetup] No user found')
+ setError('User not found. Please refresh the page.')
+ return false
+ }
+
+ setIsProcessing(true)
+ setError(null)
+
+ try {
+ console.log('[useAccountSetup] Adding account to database')
+ await addAccount({
+ accountIdentifier: address,
+ accountType: WalletProviderType.PEANUT,
+ userId: user.user.userId as string,
+ telegramHandle: telegramHandle.length > 0 ? telegramHandle : undefined,
+ })
+ console.log('[useAccountSetup] Account added successfully')
+
+ const redirect_uri = searchParams.get('redirect_uri')
+ if (redirect_uri) {
+ const validRedirectUrl = getValidRedirectUrl(redirect_uri, '/home')
+ console.log('[useAccountSetup] Redirecting to redirect_uri:', validRedirectUrl)
+ router.push(validRedirectUrl)
+ return true
+ }
+
+ const localStorageRedirect = getRedirectUrl()
+ if (localStorageRedirect) {
+ const matchedAction = POST_SIGNUP_ACTIONS.find((action) =>
+ action.pathPattern.test(localStorageRedirect)
+ )
+ if (matchedAction) {
+ console.log('[useAccountSetup] Matched post-signup action, redirecting to /home')
+ router.push('/home')
+ } else {
+ clearRedirectUrl()
+ const validRedirectUrl = getValidRedirectUrl(localStorageRedirect, '/home')
+ console.log('[useAccountSetup] Redirecting to localStorage redirect:', validRedirectUrl)
+ router.push(validRedirectUrl)
+ }
+ } else {
+ console.log('[useAccountSetup] No redirect found, going to /home')
+ router.push('/home')
+ }
+
+ return true
+ } catch (e) {
+ Sentry.captureException(e)
+ console.error('[useAccountSetup] Error adding account:', e)
+ setError('Error adding account. Please try refreshing the page.')
+
+ // clear auth state if account creation fails
+ clearAuthState(user?.user.userId)
+ return false
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ return {
+ finalizeAccountSetup,
+ isProcessing,
+ error,
+ setError,
+ }
+}
diff --git a/src/hooks/useAccountSetupRedirect.ts b/src/hooks/useAccountSetupRedirect.ts
new file mode 100644
index 000000000..88c3908ce
--- /dev/null
+++ b/src/hooks/useAccountSetupRedirect.ts
@@ -0,0 +1,35 @@
+import { useEffect, useMemo } from 'react'
+import { useRouter, usePathname } from 'next/navigation'
+import { useAuth } from '@/context/authContext'
+import { AccountType } from '@/interfaces'
+
+/**
+ * hook to check if logged-in user needs to complete account setup
+ * returns whether user should be redirected and handles the redirect
+ * should be used in layouts or pages that require a complete account
+ */
+export const useAccountSetupRedirect = () => {
+ const { user, isFetchingUser } = useAuth()
+ const router = useRouter()
+ const pathName = usePathname()
+
+ // synchronously check if user needs redirect (runs during render, not after)
+ const needsRedirect = useMemo(() => {
+ if (!user || isFetchingUser || pathName === '/setup/finish') return false
+
+ const hasPeanutWalletAccount = user.accounts.some((a) => a.type === AccountType.PEANUT_WALLET)
+ return !hasPeanutWalletAccount
+ }, [user, isFetchingUser, pathName])
+
+ // perform redirect in effect
+ useEffect(() => {
+ if (needsRedirect) {
+ console.log(
+ '[useAccountSetupRedirect] User logged in without peanut wallet account, redirecting to /setup/finish'
+ )
+ router.push('/setup/finish')
+ }
+ }, [needsRedirect, router])
+
+ return { needsRedirect, isCheckingAccount: isFetchingUser }
+}
diff --git a/src/hooks/useLogin.tsx b/src/hooks/useLogin.tsx
index fc988c5e0..1c4c66183 100644
--- a/src/hooks/useLogin.tsx
+++ b/src/hooks/useLogin.tsx
@@ -13,6 +13,8 @@ import { useRouter, useSearchParams } from 'next/navigation'
* 2. Saved redirect URL from localStorage (if present and safe)
* 3. '/home' as fallback
*
+ * Note: The mobile-ui layout handles redirecting users without PEANUT_WALLET accounts to /setup/finish
+ *
* All redirects are sanitized to prevent external URL redirection attacks.
*
* @returns {Object} Login handlers and state
@@ -27,10 +29,11 @@ export const useLogin = () => {
const router = useRouter()
const [isloginClicked, setIsloginClicked] = useState(false)
- // Wait for user to be fetched, then redirect
+ // wait for user to be fetched, then redirect
useEffect(() => {
- // Run only if login button is clicked to provide un-intentional redirects.
+ // run only if login button is clicked to prevent un-intentional redirects
if (isloginClicked && user) {
+ // redirect based on query params or saved redirect url
const localStorageRedirect = getRedirectUrl()
const redirect_uri = searchParams.get('redirect_uri')
if (redirect_uri) {
diff --git a/src/utils/index.ts b/src/utils/index.ts
index da1813df8..ba54273d9 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -8,6 +8,7 @@ export * from './ens.utils'
export * from './history.utils'
export * from './auth.utils'
export * from './webauthn.utils'
+export * from './passkeyDebug'
// Bridge utils - explicit exports to avoid naming conflicts
export {
diff --git a/src/utils/passkeyDebug.ts b/src/utils/passkeyDebug.ts
new file mode 100644
index 000000000..c9c022247
--- /dev/null
+++ b/src/utils/passkeyDebug.ts
@@ -0,0 +1,64 @@
+import * as Sentry from '@sentry/nextjs'
+
+/**
+ * captures debug information about passkey and device capabilities
+ * useful for troubleshooting passkey setup and transaction signing issues
+ */
+export const capturePasskeyDebugInfo = async (context: string) => {
+ try {
+ const debugInfo: Record = {
+ context,
+ timestamp: new Date().toISOString(),
+ // basic environment checks
+ isSecureContext: window.isSecureContext,
+ cookieEnabled: navigator.cookieEnabled,
+ userAgent: navigator.userAgent,
+ platform: navigator.platform,
+ // credentials api availability
+ credentialsApiAvailable: 'credentials' in navigator,
+ publicKeyCredentialAvailable: typeof window.PublicKeyCredential !== 'undefined',
+ }
+
+ // check conditional mediation support
+ if (window.PublicKeyCredential) {
+ try {
+ debugInfo.conditionalMediationAvailable =
+ await window.PublicKeyCredential.isConditionalMediationAvailable()
+ } catch (e) {
+ debugInfo.conditionalMediationError = (e as Error).message
+ }
+
+ // check user verifying platform authenticator availability
+ try {
+ debugInfo.platformAuthenticatorAvailable =
+ await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
+ } catch (e) {
+ debugInfo.platformAuthenticatorError = (e as Error).message
+ }
+ }
+
+ // check for stored credentials (non-intrusive check)
+ if (navigator.credentials) {
+ try {
+ console.log('navigator.credentials object:', navigator.credentials)
+ } catch (e) {
+ debugInfo.credentialsObjectError = (e as Error).message
+ }
+ }
+
+ // log to sentry with all collected info
+ Sentry.captureMessage(`Passkey Debug Info: ${context}`, {
+ level: 'info',
+ extra: debugInfo,
+ })
+
+ console.log('[PasskeyDebug]', debugInfo)
+ return debugInfo
+ } catch (error) {
+ console.error('[PasskeyDebug] Error capturing debug info:', error)
+ Sentry.captureException(error, {
+ extra: { debugContext: context },
+ })
+ return null
+ }
+}