Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand All @@ -20,6 +23,9 @@ export const CodeEditor = ({
includePayerInfo,
onPayerInfoToggle,
showPayerInfoToggle = true,
paymasterUrl = '',
onPaymasterUrlChange,
showPaymasterUrlInput = false,
}: CodeEditorProps) => {
return (
<div className={styles.editorPanel}>
Expand Down Expand Up @@ -70,6 +76,22 @@ export const CodeEditor = ({
</div>
)}

{showPaymasterUrlInput && (
<div className={styles.inputContainer}>
<label className={styles.inputLabel}>
<span className={styles.inputLabelText}>Paymaster URL</span>
<input
type="text"
value={paymasterUrl}
onChange={(e) => onPaymasterUrlChange?.(e.target.value)}
disabled={isLoading}
className={styles.textInput}
placeholder="https://your-paymaster.com/api"
/>
</label>
</div>
)}

<div className={styles.editorWrapper}>
<textarea
value={code}
Expand Down
165 changes: 161 additions & 4 deletions examples/testapp/src/pages/pay-playground/components/Output.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
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
Expand Down Expand Up @@ -189,6 +196,156 @@ export const Output = ({ result, error, consoleOutput, isLoading }: OutputProps)
</div>
)}

{result && isTokenPaymentResult(result) && (
<div className={`${styles.resultCard} ${result.success ? styles.success : styles.error}`}>
<div className={styles.resultHeader}>
{result.success ? (
<>
<svg
className={styles.resultIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
<span className={styles.resultTitle}>Token Payment Successful!</span>
</>
) : (
<>
<svg
className={styles.resultIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
<span className={styles.resultTitle}>Token Payment Failed</span>
</>
)}
</div>

<div className={styles.resultBody}>
<div className={styles.resultRow}>
<span className={styles.resultLabel}>Token</span>
<span className={styles.resultValue}>{result.token || 'Unknown'}</span>
</div>
<div className={styles.resultRow}>
<span className={styles.resultLabel}>Amount (base units)</span>
<span className={styles.resultValue}>{result.tokenAmount}</span>
</div>
<div className={styles.resultRow}>
<span className={styles.resultLabel}>Token Address</span>
<code className={styles.resultValue}>{result.tokenAddress}</code>
</div>
<div className={styles.resultRow}>
<span className={styles.resultLabel}>Recipient</span>
<code className={styles.resultValue}>{result.to}</code>
</div>
{result.success && result.id && (
<div className={styles.resultRow}>
<span className={styles.resultLabel}>Transaction ID</span>
<code className={styles.resultValue}>{result.id}</code>
</div>
)}
</div>

{result.success && result.payerInfoResponses && (
<div className={styles.userDataSection}>
<div className={styles.userDataHeader}>
<svg
className={styles.userDataIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
<span>User Info</span>
</div>
<div className={styles.userDataBody}>
{result.payerInfoResponses.name && (
<div className={styles.userDataRow}>
<span className={styles.userDataLabel}>Name</span>
<span className={styles.userDataValue}>
{(() => {
const name = result.payerInfoResponses.name as unknown as {
firstName: string;
familyName: string;
};
return `${name.firstName} ${name.familyName}`;
})()}
</span>
</div>
)}
{result.payerInfoResponses.email && (
<div className={styles.userDataRow}>
<span className={styles.userDataLabel}>Email</span>
<span className={styles.userDataValue}>
{result.payerInfoResponses.email}
</span>
</div>
)}
{result.payerInfoResponses.phoneNumber && (
<div className={styles.userDataRow}>
<span className={styles.userDataLabel}>Phone</span>
<span className={styles.userDataValue}>
{result.payerInfoResponses.phoneNumber.number} (
{result.payerInfoResponses.phoneNumber.country})
</span>
</div>
)}
{result.payerInfoResponses.physicalAddress && (
<div className={styles.userDataRow}>
<span className={styles.userDataLabel}>Address</span>
<span className={styles.userDataValue}>
{(() => {
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(', ');
})()}
</span>
</div>
)}
{result.payerInfoResponses.onchainAddress && (
<div className={styles.userDataRow}>
<span className={styles.userDataLabel}>On-chain Address</span>
<span className={styles.userDataValue}>
{result.payerInfoResponses.onchainAddress}
</span>
</div>
)}
</div>
</div>
)}
</div>
)}

{result && isPaymentStatus(result) && (
<div
className={`${styles.resultCard} ${
Expand Down
4 changes: 4 additions & 0 deletions examples/testapp/src/pages/pay-playground/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
export {
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_CODE_WITH_PAYER_INFO,
PAY_WITH_TOKEN_QUICK_TIPS,
QUICK_TIPS,
} from './playground';
57 changes: 57 additions & 0 deletions examples/testapp/src/pages/pay-playground/constants/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,61 @@ export const GET_PAYMENT_STATUS_QUICK_TIPS = [
'Make sure to use the same testnet setting as the original payment',
];

export const generatePayWithTokenCode = (
paymasterUrl: string,
includePayerInfo: boolean
): string => {
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 <a href="https://faucet.circle.com/" target="_blank" rel="noopener noreferrer">https://faucet.circle.com/</a> - 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;
Original file line number Diff line number Diff line change
@@ -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<PaymentResult | PaymentStatus | null>(null);
const [result, setResult] = useState<PaymentResult | PaymentStatus | PayWithTokenResult | null>(
null
);
const [error, setError] = useState<string | null>(null);
const [consoleOutput, setConsoleOutput] = useState<string[]>([]);
const { captureConsole } = useConsoleCapture();
Expand Down Expand Up @@ -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,
},
};
Expand Down
Loading