From c163dffa6fb526cfe0551025145a25ceeb1ca89f Mon Sep 17 00:00:00 2001 From: SuPuHe Date: Sun, 13 Sep 2026 12:17:00 +0200 Subject: [PATCH] fix(web): auto-verify user wallet payments --- apps/web/src/App.tsx | 2 + apps/web/src/auth/privy-session.tsx | 4 +- apps/web/src/components/JobWorkspace.tsx | 48 ++++++++++++++++++++++-- apps/web/src/components/LoginGate.tsx | 9 ++++- apps/web/test/components.test.tsx | 23 +++++++++--- apps/web/test/login-gate.test.tsx | 7 ++++ 6 files changed, 83 insertions(+), 10 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 71c47ac..1d8d545 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -183,6 +183,7 @@ function CabinetPage(props: { session={props.session} machineToken={props.machineToken} onMachineTokenChange={props.setMachineToken} + {...(props.userWallet ? { userWallet: props.userWallet } : {})} >
@@ -508,6 +509,7 @@ export function App(props: AppProps = {}) { session={session} machineToken={machineToken} onMachineTokenChange={setMachineToken} + {...(props.userWallet ? { userWallet: props.userWallet } : {})} >
Open a request by identifier (advanced) diff --git a/apps/web/src/auth/privy-session.tsx b/apps/web/src/auth/privy-session.tsx index b94c80b..055c2d6 100644 --- a/apps/web/src/auth/privy-session.tsx +++ b/apps/web/src/auth/privy-session.tsx @@ -105,6 +105,7 @@ export function usePrivyUserWallet(): UserWalletSession { const { user } = usePrivy(); const { ready: walletsReady, wallets } = useWallets(); const { wallet: activeWallet, connect: connectWallet } = useActiveWallet(); + const [connectedWalletAddress, setConnectedWalletAddress] = useState(null); const explicitlyConnectedWallet = useRef<{ readonly subject: string | null; readonly wallet: EthereumWallet; @@ -125,6 +126,7 @@ export function usePrivyUserWallet(): UserWalletSession { const result = await connectWallet({ reset: true }); if (result.wallet?.type !== 'ethereum') return undefined; explicitlyConnectedWallet.current = { subject, wallet: result.wallet }; + setConnectedWalletAddress(result.wallet.address); return result.wallet; } @@ -171,7 +173,7 @@ export function usePrivyUserWallet(): UserWalletSession { address: selectedWallet?.address ?? (explicitlyConnectedWallet.current?.subject === subject - ? explicitlyConnectedWallet.current.wallet.address + ? (connectedWalletAddress ?? explicitlyConnectedWallet.current.wallet.address) : null), connect, sendTransfer, diff --git a/apps/web/src/components/JobWorkspace.tsx b/apps/web/src/components/JobWorkspace.tsx index 9a53532..8763e52 100644 --- a/apps/web/src/components/JobWorkspace.tsx +++ b/apps/web/src/components/JobWorkspace.tsx @@ -35,6 +35,33 @@ function explorerHref(transactionHash: string | undefined): string | undefined { : undefined; } +const USER_WALLET_PAYMENT_CHECK_DELAY_MS = 500; +const USER_WALLET_PAYMENT_CHECK_ATTEMPTS = 30; + +function waitForPaymentCheck(): Promise { + return new Promise((resolve) => { + window.setTimeout(resolve, USER_WALLET_PAYMENT_CHECK_DELAY_MS); + }); +} + +async function resolveUserWalletPayment( + client: JobApiClient, + jobId: string, + transactionHash: string, + initial: JobView, +): Promise { + let latest = initial; + for ( + let attempt = 0; + attempt < USER_WALLET_PAYMENT_CHECK_ATTEMPTS && latest.payment_state === 'UNKNOWN'; + attempt += 1 + ) { + await waitForPaymentCheck(); + latest = await client.submitUserWalletPayment(jobId, transactionHash); + } + return latest; +} + export function SupplierQuotePanel({ quote, heading = 'Supplier quote', @@ -221,13 +248,21 @@ export function JobWorkspace(props: { const transactionHash = await userWallet.sendTransfer(job.user_payment); submittedHash = transactionHash; setPaymentHash(transactionHash); - const updated = await props.client.submitUserWalletPayment(job.job_id, transactionHash); + const initial = await props.client.submitUserWalletPayment(job.job_id, transactionHash); + setApprovedJob(initial); + setPaymentChecking(initial.payment_state === 'UNKNOWN'); + const updated = await resolveUserWalletPayment( + props.client, + job.job_id, + transactionHash, + initial, + ); setApprovedJob(updated); setNotice( updated.payment_state === 'COMMITTED' ? 'Payment confirmed from your connected wallet. Supplier delivery can now continue.' : updated.payment_state === 'UNKNOWN' - ? 'Transaction recorded but not final. Check the same transaction later; do not pay again.' + ? 'Transaction recorded but not final. Automatic checks ended; use the same transaction check if needed. Do not pay again.' : `Payment state: ${updated.payment_state}.`, ); } catch { @@ -239,6 +274,7 @@ export function JobWorkspace(props: { : 'The payment was not prepared. Keep the same task key if you need to inspect it.', ); } finally { + setPaymentChecking(false); setStarting(false); } } @@ -247,7 +283,13 @@ export function JobWorkspace(props: { if (!approvedJob || !paymentHash) return; setPaymentChecking(true); try { - const updated = await props.client.submitUserWalletPayment(approvedJob.job_id, paymentHash); + const initial = await props.client.submitUserWalletPayment(approvedJob.job_id, paymentHash); + const updated = await resolveUserWalletPayment( + props.client, + approvedJob.job_id, + paymentHash, + initial, + ); setApprovedJob(updated); setNotice( updated.payment_state === 'COMMITTED' diff --git a/apps/web/src/components/LoginGate.tsx b/apps/web/src/components/LoginGate.tsx index f972df3..af0a935 100644 --- a/apps/web/src/components/LoginGate.tsx +++ b/apps/web/src/components/LoginGate.tsx @@ -1,5 +1,5 @@ import { useState, type ReactNode } from 'react'; -import type { OperatorSession } from '../auth/session.js'; +import type { OperatorSession, UserWalletSession } from '../auth/session.js'; import { maskIdentifier } from './workspace-copy.js'; export interface LoginGateProps { @@ -7,6 +7,7 @@ export interface LoginGateProps { readonly machineToken: string; readonly onMachineTokenChange: (value: string) => void; readonly showMachineToken?: boolean; + readonly userWallet?: UserWalletSession; readonly children: ReactNode; } @@ -87,6 +88,12 @@ export function LoginGate(props: LoginGateProps) { {maskIdentifier(props.session.subject, 8)} +
+ Wallet address + + {props.userWallet?.address ?? 'No wallet connected'} + +