diff --git a/package.json b/package.json index 84248a0..d892961 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "node server.js", "build": "next build --webpack", "start": "NODE_ENV=production node server.js", - "start:prod": "PORT=3005 node server.js", + "start:prod": "PORT=3001 node server.js", "lint": "eslint .", "lint:a11y": "eslint . --no-ignore", "format": "prettier --write .", @@ -18,22 +18,22 @@ "@chakra-ui/react": "^3.30.0", "@emotion/react": "^11.14.0", "@neondatabase/serverless": "^1.0.2", + "@rhinestone/module-sdk": "^0.4.0", + "@safe-global/api-kit": "^4.0.0", + "@safe-global/protocol-kit": "^6.1.1", "@types/react-syntax-highlighter": "^15.5.13", "ethers": "^6.15.0", + "framer-motion": "^12.23.3", + "jszip": "^3.10.1", "next": "^16.0.7", "next-themes": "^0.4.6", + "qrcode.react": "^4.2.0", "react": "^19.2.1", "react-dom": "^19.2.1", + "react-icons": "^5.5.0", "react-syntax-highlighter": "^16.1.0", "secrets.js-34r7h": "^2.0.2", - "@rhinestone/module-sdk": "^0.4.0", - "@safe-global/api-kit": "^4.0.0", - "@safe-global/protocol-kit": "^6.1.1", - "framer-motion": "^12.23.3", - "jszip": "^3.10.1", - "qrcode.react": "^4.2.0", - "react-icons": "^5.5.0", - "w3pk": "^0.8.6", + "w3pk": "^0.8.7", "ws": "^8.18.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2a8c51..87b9128 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^2.0.2 version: 2.0.2 w3pk: - specifier: ^0.8.6 - version: 0.8.6(ethers@6.16.0) + specifier: ^0.8.7 + version: 0.8.7(ethers@6.16.0) ws: specifier: ^8.18.3 version: 8.18.3 @@ -3416,8 +3416,8 @@ packages: typescript: optional: true - w3pk@0.8.6: - resolution: {integrity: sha512-kIgTAXfkw9CZraXu/J5F57tfdOE93OA/eSCrverQBP2Uxlzyb5hkNPG7hGtEddNZiI7axhS6o+TabvC7qt46iA==} + w3pk@0.8.7: + resolution: {integrity: sha512-0IfxXaYL+0XEbq5blIhqLI6nhx2D4NHcTnpRyDJ1qnljIVWZY1DCXGYtb8YBc8uiHhZh65fquHEIr5LKPVcLJg==} peerDependencies: ethers: ^6.0.0 @@ -6013,7 +6013,7 @@ snapshots: eslint: 9.39.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1))(eslint@9.39.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1))(eslint@9.39.1))(eslint@9.39.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1) eslint-plugin-react: 7.37.5(eslint@9.39.1) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1) @@ -6050,7 +6050,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1))(eslint@9.39.1))(eslint@9.39.1) transitivePeerDependencies: - supports-color @@ -6065,7 +6065,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1))(eslint@9.39.1))(eslint@9.39.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7905,7 +7905,7 @@ snapshots: - utf-8-validate - zod - w3pk@0.8.6(ethers@6.16.0): + w3pk@0.8.7(ethers@6.16.0): dependencies: ethers: 6.16.0 optionalDependencies: diff --git a/src/app/api/safe/send-tx/route.ts b/src/app/api/safe/send-tx/route.ts index a8436ad..3dfc3c2 100644 --- a/src/app/api/safe/send-tx/route.ts +++ b/src/app/api/safe/send-tx/route.ts @@ -12,34 +12,34 @@ const SMART_SESSIONS_MODULE = '0x00000000008bDABA73cD9815d79069c247Eb4bDA' /** * POST /api/safe/send-tx * Send gasless transaction from Safe wallet (relayer pays fees) - * Body: { userAddress, safeAddress, chainId, to, amount, sessionKeyAddress, signature, userPrivateKey } + * Body: { ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress, signature } */ interface TransactionParams { txId: string - userAddress: string + ownerAddress: string safeAddress: string chainId: number to: string amount: string sessionKeyAddress: string - signature: string + sessionKeySignature: string + ownerSignature: string sessionKeyValidUntil?: number - userPrivateKey: string } async function processTransactionSync(params: TransactionParams) { const { txId, - userAddress, + ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress, - signature, + sessionKeySignature, + ownerSignature, sessionKeyValidUntil, - userPrivateKey, } = params const statusTimestamps: { @@ -112,7 +112,7 @@ async function processTransactionSync(params: TransactionParams) { data: transferData, // ERC-20 transfer call } const message = JSON.stringify(txData) - const recoveredAddress = ethers.verifyMessage(message, signature) + const recoveredAddress = ethers.verifyMessage(message, sessionKeySignature) if (recoveredAddress.toLowerCase() !== sessionKeyAddress.toLowerCase()) { return { @@ -122,15 +122,16 @@ async function processTransactionSync(params: TransactionParams) { } } - console.log(`✅ Signature verified from session key ${sessionKeyAddress}`) + console.log(`✅ Session key signature verified from ${sessionKeyAddress}`) - const userProtocolKit = await Safe.init({ + // Use relayer to check module status (read-only operation) + const protocolKit = await Safe.init({ provider: rpcUrl, - signer: userPrivateKey, + signer: process.env.RELAYER_PRIVATE_KEY!, safeAddress: safeAddress, }) - const isModuleEnabled = await userProtocolKit.isModuleEnabled(SMART_SESSIONS_MODULE) + const isModuleEnabled = await protocolKit.isModuleEnabled(SMART_SESSIONS_MODULE) if (isModuleEnabled) { console.log(`✅ Smart Sessions module is enabled on-chain`) @@ -161,7 +162,9 @@ async function processTransactionSync(params: TransactionParams) { statusTimestamps.verified = Date.now() const verifiedDuration = (statusTimestamps.verified - statusTimestamps.started) / 1000 - const safeTransaction = await userProtocolKit.createTransaction({ + // Create Safe transaction + // Session key signature was already verified above + const safeTransaction = await protocolKit.createTransaction({ transactions: [ { to: EURO_TOKEN_ADDRESS, // Send to token contract @@ -171,16 +174,21 @@ async function processTransactionSync(params: TransactionParams) { ], }) - const signedSafeTx = await userProtocolKit.signTransaction(safeTransaction) - console.log(` Transaction signed by user (owner)`) + // Add the owner's signature to the transaction + const signedSafeTx = await protocolKit.copyTransaction(safeTransaction) - const relayerProtocolKit = await Safe.init({ - provider: rpcUrl, - signer: process.env.RELAYER_PRIVATE_KEY!, - safeAddress: safeAddress, - }) + const safeSignature = { + signer: ownerAddress, + data: ownerSignature, + isContractSignature: false, + staticPart: () => ownerSignature, + dynamicPart: () => '', + } + + signedSafeTx.addSignature(safeSignature as any) + console.log(` Transaction signed by owner ${ownerAddress}`) - const executeTxResponse = await relayerProtocolKit.executeTransaction(signedSafeTx) + const executeTxResponse = await protocolKit.executeTransaction(signedSafeTx) let txHash: string | undefined @@ -235,15 +243,15 @@ async function processTransactionSync(params: TransactionParams) { async function processTransaction(params: TransactionParams) { const { txId, - userAddress, + ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress, - signature, + sessionKeySignature, + ownerSignature, sessionKeyValidUntil, - userPrivateKey, } = params const statusTimestamps: { @@ -273,7 +281,8 @@ async function processTransaction(params: TransactionParams) { console.log(` Amount type: ${typeof amount}`) console.log(` Chain: ${chainId}`) console.log(` Session key: ${sessionKeyAddress}`) - console.log(` Signature provided: ${signature ? 'Yes' : 'No'}`) + console.log(` Session key signature provided: ${sessionKeySignature ? 'Yes' : 'No'}`) + console.log(` Owner signature provided: ${ownerSignature ? 'Yes' : 'No'}`) // Validate amount const amountBigInt = BigInt(amount) @@ -326,7 +335,7 @@ async function processTransaction(params: TransactionParams) { data: transferData, // ERC-20 transfer call } const message = JSON.stringify(txData) - const recoveredAddress = ethers.verifyMessage(message, signature) + const recoveredAddress = ethers.verifyMessage(message, sessionKeySignature) if (recoveredAddress.toLowerCase() !== sessionKeyAddress.toLowerCase()) { sendTransactionStatus(txId, 'started', { @@ -336,17 +345,18 @@ async function processTransaction(params: TransactionParams) { return } - console.log(`✅ Signature verified from session key ${sessionKeyAddress}`) + console.log(`✅ Session key signature verified from ${sessionKeyAddress}`) // Check if Smart Sessions module is enabled on-chain try { - const userProtocolKit = await Safe.init({ + // Use relayer to check module status and execute transaction + const protocolKit = await Safe.init({ provider: rpcUrl, - signer: userPrivateKey, + signer: process.env.RELAYER_PRIVATE_KEY!, safeAddress: safeAddress, }) - const isModuleEnabled = await userProtocolKit.isModuleEnabled(SMART_SESSIONS_MODULE) + const isModuleEnabled = await protocolKit.isModuleEnabled(SMART_SESSIONS_MODULE) if (isModuleEnabled) { console.log(`✅ Smart Sessions module is enabled on-chain`) @@ -409,7 +419,8 @@ async function processTransaction(params: TransactionParams) { }) // Create Safe transaction with ERC-20 transfer - const safeTransaction = await userProtocolKit.createTransaction({ + // Session key signature was already verified above + const safeTransaction = await protocolKit.createTransaction({ transactions: [ { to: EURO_TOKEN_ADDRESS, // Send to token contract @@ -419,18 +430,22 @@ async function processTransaction(params: TransactionParams) { ], }) - // Sign with user (the owner) - const signedSafeTx = await userProtocolKit.signTransaction(safeTransaction) - console.log(` Transaction signed by user (owner)`) + // Add the owner's signature to the transaction + const signedSafeTx = await protocolKit.copyTransaction(safeTransaction) - // Execute transaction (relayer pays gas, user signed) - const relayerProtocolKit = await Safe.init({ - provider: rpcUrl, - signer: process.env.RELAYER_PRIVATE_KEY!, - safeAddress: safeAddress, - }) + const safeSignature = { + signer: ownerAddress, + data: ownerSignature, + isContractSignature: false, + staticPart: () => ownerSignature, + dynamicPart: () => '', + } + + signedSafeTx.addSignature(safeSignature as any) + console.log(` Transaction signed by owner ${ownerAddress}`) - const executeTxResponse = await relayerProtocolKit.executeTransaction(signedSafeTx) + // Execute transaction (relayer pays gas) + const executeTxResponse = await protocolKit.executeTransaction(signedSafeTx) // Get transaction hash from the response let txHash: string | undefined @@ -493,30 +508,30 @@ export async function POST(request: NextRequest) { const body = await request.json() const { - userAddress, + ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress, - signature, + sessionKeySignature, + ownerSignature, sessionKeyValidUntil, - userPrivateKey, useWebSocket, // Optional: frontend can explicitly request WebSocket mode } = body // Validation - if (!userAddress || !safeAddress || !chainId || !to || !amount || !sessionKeyAddress) { + if (!ownerAddress || !safeAddress || !chainId || !to || !amount || !sessionKeyAddress) { return NextResponse.json( { error: - 'Missing required fields: userAddress, safeAddress, chainId, to, amount, sessionKeyAddress', + 'Missing required fields: ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress', }, { status: 400 } ) } - if (!signature) { + if (!sessionKeySignature) { return NextResponse.json( { error: @@ -526,26 +541,25 @@ export async function POST(request: NextRequest) { ) } + if (!ownerSignature) { + return NextResponse.json( + { + error: 'Owner signature required. The Safe owner must sign the transaction hash.', + }, + { status: 400 } + ) + } + // Validate addresses if ( !/^0x[a-fA-F0-9]{40}$/.test(safeAddress) || !/^0x[a-fA-F0-9]{40}$/.test(to) || - !/^0x[a-fA-F0-9]{40}$/.test(sessionKeyAddress) + !/^0x[a-fA-F0-9]{40}$/.test(sessionKeyAddress) || + !/^0x[a-fA-F0-9]{40}$/.test(ownerAddress) ) { return NextResponse.json({ error: 'Invalid Ethereum address' }, { status: 400 }) } - if (!userPrivateKey) { - return NextResponse.json( - { - error: 'User private key required', - details: - 'The user must provide their private key to sign this transaction as the Safe owner', - }, - { status: 400 } - ) - } - // Check if WebSocket is available (check if server has WebSocket support) const hasWebSocket = typeof global !== 'undefined' && (global as any).wsConnections @@ -564,15 +578,15 @@ export async function POST(request: NextRequest) { setTimeout(() => { processTransaction({ txId, - userAddress, + ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress, - signature, + sessionKeySignature, + ownerSignature, sessionKeyValidUntil, - userPrivateKey, }) }, 100) // 100ms delay to allow WebSocket connection @@ -592,15 +606,15 @@ export async function POST(request: NextRequest) { console.log('Processing transaction synchronously (no WebSocket)') const result = await processTransactionSync({ txId, - userAddress, + ownerAddress, safeAddress, chainId, to, amount, sessionKeyAddress, - signature, + sessionKeySignature, + ownerSignature, sessionKeyValidUntil, - userPrivateKey, }) return NextResponse.json(result, { status: result.success ? 200 : 400 }) diff --git a/src/app/page.tsx b/src/app/page.tsx index 3163c2a..dc1c5d7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -30,6 +30,8 @@ import { EURO_TOKEN_ADDRESS, ERC20_ABI } from '@/lib/constants' import { FaSatellite, FaQrcode } from 'react-icons/fa' import { brandColors } from '@/theme' import { useTranslation } from '@/hooks/useTranslation' +import { OnboardingProgress, OnboardingStep } from '@/components/OnboardingProgress' +import { setupSafeWithSessionKey } from '@/lib/safeActions' interface SessionKey { sessionKeyAddress: string @@ -44,7 +46,7 @@ interface SessionKey { } export default function PaymentPage() { - const { isAuthenticated, user, deriveWallet, login } = useW3PK() + const { isAuthenticated, user, deriveWallet, login, signMessage } = useW3PK() const t = useTranslation() // State @@ -65,6 +67,11 @@ export default function PaymentPage() { const [amount, setAmount] = useState('1') const [paymentRequestDetected, setPaymentRequestDetected] = useState(false) + // Onboarding state + const [isOnboarding, setIsOnboarding] = useState(false) + const [onboardingStep, setOnboardingStep] = useState('deploying-safe') + const [onboardingError, setOnboardingError] = useState() + const [isRequestModalOpen, setIsRequestModalOpen] = useState(false) const onRequestModalOpen = () => setIsRequestModalOpen(true) const onRequestModalClose = () => setIsRequestModalOpen(false) @@ -201,6 +208,60 @@ export default function PaymentPage() { ? Date.now() > sessionKey.permissions.validUntil * 1000 : false + // Function to start automatic onboarding + const startOnboarding = async () => { + if (!user || !deriveWallet || !signMessage) return + + try { + setOnboardingStep('deploying-safe') + setOnboardingError(undefined) + + const result = await setupSafeWithSessionKey( + { + userId: user.id, + chainId: 10200, + deriveWallet, + signMessage, + }, + { + onDeploying: () => { + setOnboardingStep('deploying-safe') + }, + onDeployed: (deployResult) => { + setOnboardingStep('safe-deployed') + setSafeAddress(deployResult.safeAddress) + }, + onEnablingModule: () => { + setOnboardingStep('enabling-module') + }, + onModuleEnabled: () => { + setOnboardingStep('module-enabled') + }, + onCreatingSessionKey: () => { + setOnboardingStep('creating-session-key') + }, + onSessionKeyCreated: (key) => { + setOnboardingStep('session-key-created') + setSessionKey(key) + }, + onError: (error) => { + setOnboardingError(error) + }, + } + ) + + if (result.success) { + setOnboardingStep('complete') + // Wait a moment to show the complete state + setTimeout(() => { + setIsOnboarding(false) + }, 2000) + } + } catch (error: any) { + setOnboardingError(error.message || 'An error occurred during setup') + } + } + // Cleanup timeout on unmount useEffect(() => { return () => { @@ -210,27 +271,36 @@ export default function PaymentPage() { } }, []) - // Load saved Safe data from localStorage + // Load saved Safe data from localStorage and trigger onboarding if needed useEffect(() => { - if (isAuthenticated && user && user.ethereumAddress) { - const saved = localStorage.getItem(`safe_${user.id}`) - if (saved) { - const data = JSON.parse(saved) - setSafeAddress(data.safeAddress) - if (data.sessionKey) { - setSessionKey(data.sessionKey) + const loadSafeData = async () => { + if (isAuthenticated && user) { + const saved = localStorage.getItem(`safe_${user.id}`) + if (saved) { + const data = JSON.parse(saved) + setSafeAddress(data.safeAddress) + if (data.sessionKey) { + setSessionKey(data.sessionKey) + } + } else { + // No Safe found - trigger automatic onboarding + setIsOnboarding(true) + await startOnboarding() } - } - // Using user.ethereumAddress directly - const userAddr = user.ethereumAddress - setUserAddress(userAddr) + // Derive the owner wallet address (STANDARD + OWNER) + const ownerWallet = await deriveWallet('STANDARD', 'OWNER') + setUserAddress(ownerWallet.address) - const safeData = SafeStorage.getSafeData(userAddr, 10200) - if (safeData?.deploymentBlockNumber) { - setDeploymentBlock(safeData.deploymentBlockNumber) + const safeData = SafeStorage.getSafeData(ownerWallet.address, 10200) + if (safeData?.deploymentBlockNumber) { + setDeploymentBlock(safeData.deploymentBlockNumber) + } } } + + loadSafeData() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated, user]) useEffect(() => { @@ -469,7 +539,7 @@ export default function PaymentPage() { }, [safeAddress]) const getTxBaseUrl = () => { - if (typeof window === 'undefined') return 'https://w3pk.w3hc.org/' + if (typeof window === 'undefined') return 'https://shebam.w3hc.org/' // Check if we're in development (localhost) if ( @@ -481,7 +551,7 @@ export default function PaymentPage() { } // Otherwise, assume production - return 'https://w3pk.w3hc.org/' + return 'https://shebam.w3hc.org/' } const generatePaymentRequestUrl = ( @@ -580,23 +650,49 @@ export default function PaymentPage() { throw new Error('User address not available') } - // Derive the session key wallet to sign the transaction (using YOLO mode) - const sessionKeyWallet = await deriveWallet('YOLO', 'BONUS') + // Sign transaction with session key using STANDARD mode + // STANDARD mode provides convenience without exposing private keys + const message = JSON.stringify(txData) + const sessionKeySignature = await signMessage(message, { + mode: 'STANDARD', + tag: 'USER', + signingMethod: 'EIP191', // Use EIP-191 for session key authorization + }) - if (!sessionKeyWallet.privateKey) { - throw new Error('Session key private key not available') + if (!sessionKeySignature) { + throw new Error('Failed to sign transaction with session key') } - // Sign with the session key's private key - const message = JSON.stringify(txData) - const sessionKeySigner = new ethers.Wallet(sessionKeyWallet.privateKey) - const signature = await sessionKeySigner.signMessage(message) + // Get owner wallet address + const ownerWallet = await deriveWallet('STANDARD', 'OWNER') + + // Get the Safe transaction hash to sign with owner + const hashResponse = await fetch('/api/safe/get-tx-hash', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + safeAddress, + to: EURO_TOKEN_ADDRESS, + data: transferData, + value: '0', + chainId: 10200, + }), + }) + + const hashData = await hashResponse.json() + if (!hashData.success) { + throw new Error(hashData.error || 'Failed to get transaction hash') + } - // Get derived wallet for signing (using YOLO mode with SHEBAM tag) - const wallet0 = await deriveWallet('YOLO', 'SHEBAM') + // Sign the Safe transaction hash with OWNER wallet + const ownerSignature = await signMessage(hashData.txHash, { + mode: 'STANDARD', + tag: 'OWNER', + signingMethod: 'rawHash', + }) - if (!wallet0.privateKey) { - throw new Error('Owner wallet private key not available') + if (!ownerSignature) { + throw new Error('Failed to sign transaction hash with owner wallet') } // Try WebSocket mode first, fall back to sync mode @@ -604,15 +700,15 @@ export default function PaymentPage() { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - userAddress: wallet0.address, + ownerAddress: ownerWallet.address, safeAddress, chainId: 10200, to: recipient, amount: transferAmount, // Send the EUR token amount sessionKeyAddress: sessionKey.sessionKeyAddress, sessionKeyValidUntil: sessionKey.permissions.validUntil, - userPrivateKey: wallet0.privateKey, - signature, + sessionKeySignature, // Session key signature for validation + ownerSignature, // Owner signature for Safe transaction useWebSocket: true, // Request WebSocket mode }), }) @@ -635,9 +731,6 @@ export default function PaymentPage() { description: `Verified in ${update.duration?.toFixed(2)}s`, type: 'success', duration: 4000, - // containerStyle: { - // bg: 'green.500', - // }, }) if (window.history && window.history.replaceState) { @@ -674,16 +767,6 @@ export default function PaymentPage() { // Start showing refetch loader setIsRefetchingAfterConfirmation(true) } else if (update.status === 'confirmed') { - // toast({ - // title: '✅ Settled!', - // description: `Settled onchain in ${update.duration?.toFixed(2)}s.\nTx hash: ${update.txHash?.slice(0, 10) || 'N/A'}...`, - // status: 'info', - // duration: 5000, - // // containerStyle: { - // // bg: 'green.500', - // // }, - // }) - // Update the pending transaction to 'confirmed' status setPendingTransactions(prev => prev.map(tx => @@ -747,18 +830,6 @@ export default function PaymentPage() { setIsSending(false) - if (data.durations?.confirmed && data.txHash) { - // toast({ - // title: '✅ Settled!', - // description: `Settled onchain in ${data.durations.confirmed.toFixed(2)}s. \nTx hash: ${data.txHash?.slice(0, 10) || 'N/A'}...`, - // status: 'info', - // duration: 5000, - // // containerStyle: { - // // bg: 'green.500', - // // }, - // }) - } - // Clear form and reload balance and transactions setRecipient('0x502fb0dFf6A2adbF43468C9888D1A26943eAC6D1') setAmount('1') @@ -843,7 +914,13 @@ export default function PaymentPage() { ) } + // Show onboarding progress if setting up Safe for first time + if (isOnboarding) { + return + } + if (!safeAddress) { + // This should rarely be shown now since onboarding is automatic return ( @@ -868,11 +945,22 @@ export default function PaymentPage() { {/* Header */} - - - Payment - - Send and receive EUR + + + {t.header.intro}{' '} + + {t.header.faster} + {' '} + {t.header.and}{' '} + + {t.header.cheaper} + {' '} + {t.header.thanAnyExisting}{' '} + + {t.header.legallyRegulated} + + {t.header.testNetwork} + {/* Send Block */} @@ -964,8 +1052,9 @@ export default function PaymentPage() { value={amount} onValueChange={e => setAmount(e.value)} min={0} - step={0.001} + step={1} disabled={!sessionKey || isSessionKeyExpired || isSending || isCooldown} + width="full" > - + - - {isWebNFCSupported ? ( + + - ) : ( - - - - - - )} - - + {isWebNFCSupported ? ( + + ) : ( + + + + + + )} + + + + + + ) : (