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 && (
+
+
+
+ )}
+
)}
+ {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 && (
+
+
+
+ {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) && (
{
+ const paymasterConfig = paymasterUrl
+ ? `paymaster: {
+ url: '${paymasterUrl}'
+ }`
+ : `paymaster: {
+ // Paymaster configuration for gas sponsorship
+ // url: 'https://your-paymaster.com/api'
+ }`;
+
+ const payerInfoConfig = includePayerInfo
+ ? `,
+ payerInfo: {
+ requests: [
+ { type: 'name'},
+ { type: 'email' },
+ { type: 'phoneNumber', optional: true },
+ { type: 'physicalAddress', optional: true },
+ { type: 'onchainAddress' }
+ ]
+ }`
+ : '';
+
+ return `import { base } from '@base-org/account'
+
+try {
+ const result = await base.payWithToken({
+ amount: '1000000', // Amount in base units (e.g., 1 USDC = 1000000)
+ to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ token: 'USDC', // Token symbol or contract address
+ testnet: true,
+ ${paymasterConfig}${payerInfoConfig}
+ })
+
+ return result;
+} catch (error) {
+ console.error('Token payment failed:', error.message);
+ throw error;
+}`;
+};
+
+export const DEFAULT_PAY_WITH_TOKEN_CODE = generatePayWithTokenCode('', false);
+
+export const PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO = generatePayWithTokenCode('', true);
+
+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)",
+ '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..cc74dada3 100644
--- a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts
+++ b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts
@@ -1,12 +1,14 @@
-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 +50,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..7614c9465 100644
--- a/examples/testapp/src/pages/pay-playground/index.page.tsx
+++ b/examples/testapp/src/pages/pay-playground/index.page.tsx
@@ -3,19 +3,26 @@ import { CodeEditor, Header, Output, QuickTips } from './components';
import {
DEFAULT_GET_PAYMENT_STATUS_CODE,
DEFAULT_PAY_CODE,
+ DEFAULT_PAY_WITH_TOKEN_CODE,
+ generatePayWithTokenCode,
GET_PAYMENT_STATUS_QUICK_TIPS,
PAY_CODE_WITH_PAYER_INFO,
PAY_QUICK_TIPS,
+ 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 [payWithTokenPaymasterUrl, setPayWithTokenPaymasterUrl] = useState('');
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 +42,30 @@ function PayPlayground() {
payExecution.reset();
};
+ const handlePayWithTokenExecute = () => {
+ payWithTokenExecution.executeCode(payWithTokenCode);
+ };
+
+ const handlePayWithTokenReset = () => {
+ setIncludePayWithTokenPayerInfo(false);
+ setPayWithTokenPaymasterUrl('');
+ setPayWithTokenCode(DEFAULT_PAY_WITH_TOKEN_CODE);
+ payWithTokenExecution.reset();
+ };
+
+ const handlePayWithTokenPayerInfoToggle = (checked: boolean) => {
+ setIncludePayWithTokenPayerInfo(checked);
+ const newCode = generatePayWithTokenCode(payWithTokenPaymasterUrl, checked);
+ setPayWithTokenCode(newCode);
+ payWithTokenExecution.reset();
+ };
+
+ const handlePayWithTokenPaymasterUrlChange = (url: string) => {
+ setPayWithTokenPaymasterUrl(url);
+ const newCode = generatePayWithTokenCode(url, includePayWithTokenPayerInfo);
+ setPayWithTokenCode(newCode);
+ };
+
const handleGetPaymentStatusExecute = () => {
getPaymentStatusExecution.executeCode(getPaymentStatusCode);
};
@@ -46,13 +77,9 @@ function PayPlayground() {
// Watch for successful payment results and update getPaymentStatus code with the transaction ID
useEffect(() => {
- if (
- payExecution.result &&
- 'success' in payExecution.result &&
- payExecution.result.success &&
- payExecution.result.id
- ) {
- const transactionId = payExecution.result.id;
+ const result = payExecution.result || payWithTokenExecution.result;
+ if (result && 'success' in result && result.success && result.id) {
+ const transactionId = result.id;
const updatedCode = `import { base } from '@base-org/account'
try {
@@ -69,7 +96,7 @@ try {
}`;
setGetPaymentStatusCode(updatedCode);
}
- }, [payExecution.result]);
+ }, [payExecution.result, payWithTokenExecution.result]);
return (
@@ -107,6 +134,42 @@ try {
+ {/* payWithToken Section */}
+
+ payWithToken Function
+
+ Send any ERC20 token payment on Base with paymaster sponsorship
+
+
+
+
+
{/* getPaymentStatus Section */}
getPaymentStatus Function
diff --git a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts
index 8c909c0c6..416ed8789 100644
--- a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts
+++ b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts
@@ -3,11 +3,11 @@ import * as acorn from 'acorn';
// Define the whitelist of allowed operations
export const WHITELIST = {
// Allowed SDK functions
- allowedFunctions: ['pay', 'getPaymentStatus'],
+ allowedFunctions: ['pay', 'getPaymentStatus', 'payWithToken'],
// Allowed object properties and methods
allowedObjects: {
- base: ['pay', 'getPaymentStatus'],
+ base: ['pay', 'getPaymentStatus', 'payWithToken'],
console: ['log', 'error', 'warn', 'info'],
Promise: ['resolve', 'reject', 'all', 'race'],
Object: ['keys', 'values', 'entries', 'assign'],