From 039499611da8d1d4326c4a102efa75e1542f0845 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 22:57:13 -0700 Subject: [PATCH 1/3] feat: add payWithToken playground implementation - Add payWithToken section to pay playground UI - Add DEFAULT_PAY_WITH_TOKEN_CODE and PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO constants - Add PAY_WITH_TOKEN_QUICK_TIPS for user guidance - Update useCodeExecution hook to include payWithToken function - Update Output component to handle PayWithTokenResult type - Wire up payWithToken execution with payer info toggle support - Auto-populate getPaymentStatus with transaction IDs from token payments --- .../pay-playground/components/Output.tsx | 163 +++++++++++++++++- .../pages/pay-playground/constants/index.ts | 3 + .../pay-playground/constants/playground.ts | 58 +++++++ .../pay-playground/hooks/useCodeExecution.ts | 8 +- .../src/pages/pay-playground/index.page.tsx | 67 ++++++- 5 files changed, 286 insertions(+), 13 deletions(-) diff --git a/examples/testapp/src/pages/pay-playground/components/Output.tsx b/examples/testapp/src/pages/pay-playground/components/Output.tsx index 47a6d8d4e..91c0983d5 100644 --- a/examples/testapp/src/pages/pay-playground/components/Output.tsx +++ b/examples/testapp/src/pages/pay-playground/components/Output.tsx @@ -1,16 +1,21 @@ -import type { PaymentResult, PaymentStatus } from '@base-org/account'; +import type { PaymentResult, PaymentStatus, PayWithTokenResult } from '@base-org/account'; import styles from './Output.module.css'; interface OutputProps { - result: PaymentResult | PaymentStatus | null; + result: PaymentResult | PaymentStatus | PayWithTokenResult | null; error: string | null; consoleOutput: string[]; isLoading: boolean; } -// Type guard to check if result is PaymentResult +// Type guard to check if result is PaymentResult (USDC payment) const isPaymentResult = (result: unknown): result is PaymentResult => { - return result !== null && typeof result === 'object' && 'success' in result; + return result !== null && typeof result === 'object' && 'success' in result && 'amount' in result; +}; + +// Type guard to check if result is PayWithTokenResult (token payment) +const isTokenPaymentResult = (result: unknown): result is PayWithTokenResult => { + return result !== null && typeof result === 'object' && 'success' in result && 'tokenAmount' in result; }; // Type guard to check if result is PaymentStatus @@ -189,6 +194,156 @@ export const Output = ({ result, error, consoleOutput, isLoading }: OutputProps) )} + {result && isTokenPaymentResult(result) && ( +
+
+ {result.success ? ( + <> + + + + + Token Payment Successful! + + ) : ( + <> + + + + + + Token Payment Failed + + )} +
+ +
+
+ Token + {result.token || 'Unknown'} +
+
+ Amount (base units) + {result.tokenAmount} +
+
+ Token Address + {result.tokenAddress} +
+
+ Recipient + {result.to} +
+ {result.success && result.id && ( +
+ Transaction ID + {result.id} +
+ )} +
+ + {result.success && result.payerInfoResponses && ( +
+
+ + + + + User Info +
+
+ {result.payerInfoResponses.name && ( +
+ Name + + {(() => { + const name = result.payerInfoResponses.name as unknown as { + firstName: string; + familyName: string; + }; + return `${name.firstName} ${name.familyName}`; + })()} + +
+ )} + {result.payerInfoResponses.email && ( +
+ Email + + {result.payerInfoResponses.email} + +
+ )} + {result.payerInfoResponses.phoneNumber && ( +
+ Phone + + {result.payerInfoResponses.phoneNumber.number} ( + {result.payerInfoResponses.phoneNumber.country}) + +
+ )} + {result.payerInfoResponses.physicalAddress && ( +
+ Address + + {(() => { + const addr = result.payerInfoResponses.physicalAddress as unknown as { + address1: string; + address2?: string; + city: string; + state: string; + postalCode: string; + countryCode: string; + name?: { + firstName: string; + familyName: string; + }; + }; + const parts = [ + addr.name ? `${addr.name.firstName} ${addr.name.familyName}` : null, + addr.address1, + addr.address2, + `${addr.city}, ${addr.state} ${addr.postalCode}`, + addr.countryCode, + ].filter(Boolean); + return parts.join(', '); + })()} + +
+ )} + {result.payerInfoResponses.onchainAddress && ( +
+ On-chain Address + + {result.payerInfoResponses.onchainAddress} + +
+ )} +
+
+ )} +
+ )} + {result && isPaymentStatus(result) && (
https://faucet.circle.com/ - select "Base Sepolia" as the network', + 'Amount is in the token\'s base units (e.g., 1 USDC = 1000000 with 6 decimals)', + 'Token can be a symbol (e.g., "USDC", "WETH") or a contract address', + 'Requires a paymaster for gas sponsorship', + 'Use payerInfo to request user information', + 'Supports both Base mainnet and Base Sepolia testnet', +]; + export const QUICK_TIPS = PAY_QUICK_TIPS; diff --git a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts index c21a8d505..ffb9de233 100644 --- a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts +++ b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts @@ -1,12 +1,12 @@ -import type { PaymentResult, PaymentStatus } from '@base-org/account'; -import { getPaymentStatus, pay } from '@base-org/account'; +import type { PaymentResult, PaymentStatus, PayWithTokenResult } from '@base-org/account'; +import { getPaymentStatus, pay, payWithToken } from '@base-org/account'; import { useCallback, useState } from 'react'; import { transformAndSanitizeCode } from '../utils/codeTransform'; import { useConsoleCapture } from './useConsoleCapture'; export const useCodeExecution = () => { const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [error, setError] = useState(null); const [consoleOutput, setConsoleOutput] = useState([]); const { captureConsole } = useConsoleCapture(); @@ -48,10 +48,12 @@ export const useCodeExecution = () => { const context = { // Individual functions for direct access pay, + payWithToken, getPaymentStatus, // Namespaced access via base object base: { pay, + payWithToken, getPaymentStatus, }, }; diff --git a/examples/testapp/src/pages/pay-playground/index.page.tsx b/examples/testapp/src/pages/pay-playground/index.page.tsx index 477144558..842976ace 100644 --- a/examples/testapp/src/pages/pay-playground/index.page.tsx +++ b/examples/testapp/src/pages/pay-playground/index.page.tsx @@ -3,19 +3,25 @@ import { CodeEditor, Header, Output, QuickTips } from './components'; import { DEFAULT_GET_PAYMENT_STATUS_CODE, DEFAULT_PAY_CODE, + DEFAULT_PAY_WITH_TOKEN_CODE, GET_PAYMENT_STATUS_QUICK_TIPS, PAY_CODE_WITH_PAYER_INFO, PAY_QUICK_TIPS, + PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO, + PAY_WITH_TOKEN_QUICK_TIPS, } from './constants'; import { useCodeExecution } from './hooks'; import styles from './styles/Home.module.css'; function PayPlayground() { const [includePayerInfo, setIncludePayerInfo] = useState(false); + const [includePayWithTokenPayerInfo, setIncludePayWithTokenPayerInfo] = useState(false); const [payCode, setPayCode] = useState(DEFAULT_PAY_CODE); + const [payWithTokenCode, setPayWithTokenCode] = useState(DEFAULT_PAY_WITH_TOKEN_CODE); const [getPaymentStatusCode, setGetPaymentStatusCode] = useState(DEFAULT_GET_PAYMENT_STATUS_CODE); const payExecution = useCodeExecution(); + const payWithTokenExecution = useCodeExecution(); const getPaymentStatusExecution = useCodeExecution(); const handlePayExecute = () => { @@ -35,6 +41,23 @@ function PayPlayground() { payExecution.reset(); }; + const handlePayWithTokenExecute = () => { + payWithTokenExecution.executeCode(payWithTokenCode); + }; + + const handlePayWithTokenReset = () => { + setIncludePayWithTokenPayerInfo(false); + setPayWithTokenCode(DEFAULT_PAY_WITH_TOKEN_CODE); + payWithTokenExecution.reset(); + }; + + const handlePayWithTokenPayerInfoToggle = (checked: boolean) => { + setIncludePayWithTokenPayerInfo(checked); + const newCode = checked ? PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO : DEFAULT_PAY_WITH_TOKEN_CODE; + setPayWithTokenCode(newCode); + payWithTokenExecution.reset(); + }; + const handleGetPaymentStatusExecute = () => { getPaymentStatusExecution.executeCode(getPaymentStatusCode); }; @@ -46,13 +69,14 @@ function PayPlayground() { // Watch for successful payment results and update getPaymentStatus code with the transaction ID useEffect(() => { + const result = payExecution.result || payWithTokenExecution.result; if ( - payExecution.result && - 'success' in payExecution.result && - payExecution.result.success && - payExecution.result.id + result && + 'success' in result && + result.success && + result.id ) { - const transactionId = payExecution.result.id; + const transactionId = result.id; const updatedCode = `import { base } from '@base-org/account' try { @@ -69,7 +93,7 @@ try { }`; setGetPaymentStatusCode(updatedCode); } - }, [payExecution.result]); + }, [payExecution.result, payWithTokenExecution.result]); return (
@@ -107,6 +131,37 @@ try {
+ {/* payWithToken Section */} +
+

payWithToken Function

+

Send any ERC20 token payment on Base with paymaster sponsorship

+ +
+
+ + +
+ +
+ +
+
+
+ {/* getPaymentStatus Section */}

getPaymentStatus Function

From 9422b9484b46a9eb2810bbc0fceea045cc26608f Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 23:09:41 -0700 Subject: [PATCH 2/3] fix: format issues and remove unsupported chain IDs from erc3770 --- .../src/pages/pay-playground/components/Output.tsx | 4 +++- .../src/pages/pay-playground/constants/playground.ts | 2 +- .../pages/pay-playground/hooks/useCodeExecution.ts | 4 +++- .../testapp/src/pages/pay-playground/index.page.tsx | 11 ++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/testapp/src/pages/pay-playground/components/Output.tsx b/examples/testapp/src/pages/pay-playground/components/Output.tsx index 91c0983d5..0aded4a4b 100644 --- a/examples/testapp/src/pages/pay-playground/components/Output.tsx +++ b/examples/testapp/src/pages/pay-playground/components/Output.tsx @@ -15,7 +15,9 @@ const isPaymentResult = (result: unknown): result is PaymentResult => { // Type guard to check if result is PayWithTokenResult (token payment) const isTokenPaymentResult = (result: unknown): result is PayWithTokenResult => { - return result !== null && typeof result === 'object' && 'success' in result && 'tokenAmount' in result; + return ( + result !== null && typeof result === 'object' && 'success' in result && 'tokenAmount' in result + ); }; // Type guard to check if result is PaymentStatus diff --git a/examples/testapp/src/pages/pay-playground/constants/playground.ts b/examples/testapp/src/pages/pay-playground/constants/playground.ts index 0f166a28b..247f2563e 100644 --- a/examples/testapp/src/pages/pay-playground/constants/playground.ts +++ b/examples/testapp/src/pages/pay-playground/constants/playground.ts @@ -119,7 +119,7 @@ try { export const PAY_WITH_TOKEN_QUICK_TIPS = [ 'Get testnet ETH at https://faucet.circle.com/ - select "Base Sepolia" as the network', - 'Amount is in the token\'s base units (e.g., 1 USDC = 1000000 with 6 decimals)', + "Amount is in the token's base units (e.g., 1 USDC = 1000000 with 6 decimals)", 'Token can be a symbol (e.g., "USDC", "WETH") or a contract address', 'Requires a paymaster for gas sponsorship', 'Use payerInfo to request user information', diff --git a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts index ffb9de233..cc74dada3 100644 --- a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts +++ b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts @@ -6,7 +6,9 @@ import { useConsoleCapture } from './useConsoleCapture'; export const useCodeExecution = () => { const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); + const [result, setResult] = useState( + null + ); const [error, setError] = useState(null); const [consoleOutput, setConsoleOutput] = useState([]); const { captureConsole } = useConsoleCapture(); diff --git a/examples/testapp/src/pages/pay-playground/index.page.tsx b/examples/testapp/src/pages/pay-playground/index.page.tsx index 842976ace..57e37c67a 100644 --- a/examples/testapp/src/pages/pay-playground/index.page.tsx +++ b/examples/testapp/src/pages/pay-playground/index.page.tsx @@ -70,12 +70,7 @@ function PayPlayground() { // Watch for successful payment results and update getPaymentStatus code with the transaction ID useEffect(() => { const result = payExecution.result || payWithTokenExecution.result; - if ( - result && - 'success' in result && - result.success && - result.id - ) { + if (result && 'success' in result && result.success && result.id) { const transactionId = result.id; const updatedCode = `import { base } from '@base-org/account' @@ -134,7 +129,9 @@ try { {/* payWithToken Section */}

payWithToken Function

-

Send any ERC20 token payment on Base with paymaster sponsorship

+

+ Send any ERC20 token payment on Base with paymaster sponsorship +

From 5347b997edfedbc39fca27b1b6dfe72101626bb2 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 23:28:35 -0700 Subject: [PATCH 3/3] fix --- .../components/CodeEditor.module.css | 46 +++++++++++++++ .../pay-playground/components/CodeEditor.tsx | 22 +++++++ .../pages/pay-playground/constants/index.ts | 1 + .../pay-playground/constants/playground.ts | 59 +++++++++---------- .../src/pages/pay-playground/index.page.tsx | 15 ++++- .../pay-playground/utils/codeSanitizer.ts | 4 +- 6 files changed, 113 insertions(+), 34 deletions(-) diff --git a/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css b/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css index 013d1fef6..22fcb4f48 100644 --- a/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css +++ b/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css @@ -72,6 +72,52 @@ user-select: none; } +.inputContainer { + padding: 1rem 1.5rem; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; +} + +.inputLabel { + display: flex; + flex-direction: column; + gap: 0.5rem; + font-size: 0.875rem; + color: #475569; +} + +.inputLabelText { + font-weight: 500; +} + +.textInput { + width: 100%; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace; + color: #0f172a; + background: white; + border: 1px solid #cbd5e1; + border-radius: 6px; + outline: none; + transition: all 0.2s; +} + +.textInput:focus { + border-color: #0052ff; + box-shadow: 0 0 0 3px rgba(0, 82, 255, 0.1); +} + +.textInput:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f1f5f9; +} + +.textInput::placeholder { + color: #94a3b8; +} + .editorWrapper { position: relative; height: 390px; diff --git a/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx b/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx index cf0a0c8d8..4c7d3329d 100644 --- a/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx +++ b/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx @@ -9,6 +9,9 @@ interface CodeEditorProps { includePayerInfo: boolean; onPayerInfoToggle: (checked: boolean) => void; showPayerInfoToggle?: boolean; + paymasterUrl?: string; + onPaymasterUrlChange?: (url: string) => void; + showPaymasterUrlInput?: boolean; } export const CodeEditor = ({ @@ -20,6 +23,9 @@ export const CodeEditor = ({ includePayerInfo, onPayerInfoToggle, showPayerInfoToggle = true, + paymasterUrl = '', + onPaymasterUrlChange, + showPaymasterUrlInput = false, }: CodeEditorProps) => { return (
@@ -70,6 +76,22 @@ export const CodeEditor = ({
)} + {showPaymasterUrlInput && ( +
+ +
+ )} +