From 08d9970857aeecc9a43deac337d9dd82febe5983 Mon Sep 17 00:00:00 2001 From: Must-be-Ash Date: Mon, 6 Apr 2026 14:45:41 -0700 Subject: [PATCH] feat(paywall): update /protected page UI with animations, wallet select, and refreshed styling --- go/http/evm_paywall_template.go | 2 +- go/http/svm_paywall_template.go | 2 +- .../x402/http/paywall/evm_paywall_template.py | 2 +- .../x402/http/paywall/svm_paywall_template.py | 2 +- .../http/paywall/src/IntroAnimation.tsx | 3 + .../packages/http/paywall/src/PaywallApp.tsx | 43 +- .../http/paywall/src/WalletSelect.tsx | 75 ++++ .../packages/http/paywall/src/baseTemplate.ts | 4 + .../http/paywall/src/evm/EvmPaywall.tsx | 223 +++++----- .../packages/http/paywall/src/evm/entry.tsx | 29 +- .../http/paywall/src/evm/gen/template.ts | 2 +- .../packages/http/paywall/src/styles.css | 410 +++++++++++++----- .../http/paywall/src/svm/SolanaPaywall.tsx | 183 ++++---- .../packages/http/paywall/src/svm/entry.tsx | 29 +- .../http/paywall/src/svm/gen/template.ts | 2 +- 15 files changed, 669 insertions(+), 342 deletions(-) create mode 100644 typescript/packages/http/paywall/src/IntroAnimation.tsx create mode 100644 typescript/packages/http/paywall/src/WalletSelect.tsx diff --git a/go/http/evm_paywall_template.go b/go/http/evm_paywall_template.go index 1f03ef1070..f234f9bfde 100644 --- a/go/http/evm_paywall_template.go +++ b/go/http/evm_paywall_template.go @@ -2,4 +2,4 @@ package http // EVMPaywallTemplate is the pre-built EVM paywall template with inlined CSS and JS -const EVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " +const EVMPaywallTemplate = "\n \n \n \n \n \n Payment Required\n \n \n
\n \n \n " diff --git a/go/http/svm_paywall_template.go b/go/http/svm_paywall_template.go index 1ca1b8e095..9efe4c6105 100644 --- a/go/http/svm_paywall_template.go +++ b/go/http/svm_paywall_template.go @@ -2,4 +2,4 @@ package http // SVMPaywallTemplate is the pre-built SVM paywall template with inlined CSS and JS -const SVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " +const SVMPaywallTemplate = "\n \n \n \n \n \n Payment Required\n \n \n
\n \n \n " diff --git a/python/x402/http/paywall/evm_paywall_template.py b/python/x402/http/paywall/evm_paywall_template.py index cb301fe539..e19feac07c 100644 --- a/python/x402/http/paywall/evm_paywall_template.py +++ b/python/x402/http/paywall/evm_paywall_template.py @@ -1,2 +1,2 @@ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT -EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' +EVM_PAYWALL_TEMPLATE = '\n \n \n \n \n \n Payment Required\n \n \n
\n \n \n ' diff --git a/python/x402/http/paywall/svm_paywall_template.py b/python/x402/http/paywall/svm_paywall_template.py index de853d5bf5..3ef43205ef 100644 --- a/python/x402/http/paywall/svm_paywall_template.py +++ b/python/x402/http/paywall/svm_paywall_template.py @@ -1,2 +1,2 @@ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT -SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' +SVM_PAYWALL_TEMPLATE = '\n \n \n \n \n \n Payment Required\n \n \n
\n \n \n ' diff --git a/typescript/packages/http/paywall/src/IntroAnimation.tsx b/typescript/packages/http/paywall/src/IntroAnimation.tsx new file mode 100644 index 0000000000..87fb19ec74 --- /dev/null +++ b/typescript/packages/http/paywall/src/IntroAnimation.tsx @@ -0,0 +1,3 @@ +export function IntroAnimation({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/typescript/packages/http/paywall/src/PaywallApp.tsx b/typescript/packages/http/paywall/src/PaywallApp.tsx index 2abc023aaa..c87d8807ad 100644 --- a/typescript/packages/http/paywall/src/PaywallApp.tsx +++ b/typescript/packages/http/paywall/src/PaywallApp.tsx @@ -28,10 +28,22 @@ export function PaywallApp() { if (!paymentRequired || !paymentRequired.accepts || paymentRequired.accepts.length === 0) { return ( -
-
-

Payment Required

-

Loading payment details...

+
+
+
+
+
...
+
Loading payment details
+
+
+
+ + Powered by{" "} + + x402 + + +
); @@ -59,13 +71,22 @@ export function PaywallApp() { } return ( -
-
-

Payment Required

-

- Unsupported network configuration for this paywall. Please contact the application - developer. -

+
+
+
+
+
Unsupported network
+
Please contact the application developer.
+
+
+
+ + Powered by{" "} + + x402 + + +
); diff --git a/typescript/packages/http/paywall/src/WalletSelect.tsx b/typescript/packages/http/paywall/src/WalletSelect.tsx new file mode 100644 index 0000000000..0d3b95a109 --- /dev/null +++ b/typescript/packages/http/paywall/src/WalletSelect.tsx @@ -0,0 +1,75 @@ +import { useEffect, useMemo, useRef, useState } from "react"; + +type WalletSelectOption = { + value: string; + label: string; +}; + +type WalletSelectProps = { + value: string; + onChange: (value: string) => void; + options: WalletSelectOption[]; + placeholder?: string; +}; + +export function WalletSelect({ + value, + onChange, + options, + placeholder = "Select a wallet", +}: WalletSelectProps) { + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + + const selectedOption = useMemo( + () => options.find(option => option.value === value), + [options, value], + ); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (!containerRef.current) { + return; + } + if (!containerRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ + {isOpen && ( +
+ {options.map(option => ( + + ))} +
+ )} +
+ ); +} diff --git a/typescript/packages/http/paywall/src/baseTemplate.ts b/typescript/packages/http/paywall/src/baseTemplate.ts index 09d9f77f20..d59c018b9d 100644 --- a/typescript/packages/http/paywall/src/baseTemplate.ts +++ b/typescript/packages/http/paywall/src/baseTemplate.ts @@ -13,6 +13,10 @@ export function getBaseTemplate(): string { + + + + Payment Required
diff --git a/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx index b28c130093..1488cec7dd 100644 --- a/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx +++ b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx @@ -10,6 +10,7 @@ import type { PaymentRequired } from "@x402/core/types"; import { getUSDCBalance } from "./utils"; import { Spinner } from "./Spinner"; +import { WalletSelect } from "../WalletSelect"; import { getNetworkDisplayName, isTestnetNetwork } from "../paywallUtils"; import { wagmiToClientSigner } from "./browserAdapter"; @@ -42,6 +43,8 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall const x402 = window.x402; const amount = x402.amount; + const appName = x402.appName; + const appLogo = x402.appLogo; const firstRequirement = paymentRequired.accepts[0]; if (!firstRequirement) { @@ -51,6 +54,7 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall const network = firstRequirement.network; const chainName = getNetworkDisplayName(network); const testnet = isTestnetNetwork(network); + const description = paymentRequired.resource?.description; const chainId = parseInt(network.split(":")[1]); @@ -70,6 +74,15 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall [paymentChain], ); + const selectableConnectors = useMemo(() => { + const filtered = connectors.filter( + connector => + connector.id.toLowerCase() !== "injected" && + connector.name.trim().toLowerCase() !== "injected", + ); + return filtered.length > 0 ? filtered : connectors; + }, [connectors]); + const checkUSDCBalance = useCallback(async () => { if (!address) { return; @@ -117,10 +130,10 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall // Auto-select if only one connector is available useEffect(() => { - if (!selectedConnectorId && connectors.length === 1) { - setSelectedConnectorId(connectors[0].id); + if (!selectedConnectorId && selectableConnectors.length === 1) { + setSelectedConnectorId(selectableConnectors[0].id); } - }, [connectors, selectedConnectorId]); + }, [selectableConnectors, selectedConnectorId]); const handlePayment = useCallback(async () => { if (!address || !x402) { @@ -191,107 +204,119 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall } return ( -
-
-

Payment Required

-

- {paymentRequired.resource?.description && `${paymentRequired.resource.description}.`} To - access this content, please pay ${amount} {chainName} USDC. -

- {testnet && ( -

- Need {chainName} USDC?{" "} - - Get some here. - -

- )} -
+
+
+
+ {/* App branding */} + {(appLogo || appName) && ( +
+ {appLogo && {appName} + {appName && {appName}} +
+ )} -
- {!isConnected ? ( -
- - -
- ) : ( -
- + {/* Amount */} +
+
${amount}
+
USDC on {chainName}
- )} - {isConnected && ( -
-
-
- Wallet: - - {address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "Loading..."} - -
-
- Available balance: - - - -
-
- Amount: - ${amount} USDC -
-
- Network: - {chainName} -
+ + {!isConnected && description &&
{description}
} + + {/* Wallet connection or payment details */} + {!isConnected ? ( +
+ ({ + value: connector.id, + label: connector.name, + }))} + placeholder="Select a wallet" + /> +
+ ) : ( + <> +
+
+ Wallet + + {address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "Loading..."} + +
+
+ Balance + + + +
+
+ Network + {chainName} +
+
-
- {isCorrectChain ? ( - - ) : ( - + ) : ( + + )} + - )} -
-
- )} - {status &&
{status}
} +
+ + )} + + {status &&
{status}
} +
+ + {/* Footer */} +
+ {testnet && ( + + Need {chainName} USDC?{" "} + + Get some here + + + )} + + Powered by{" "} + + x402 + + +
); diff --git a/typescript/packages/http/paywall/src/evm/entry.tsx b/typescript/packages/http/paywall/src/evm/entry.tsx index fc352246d0..5b585a861c 100644 --- a/typescript/packages/http/paywall/src/evm/entry.tsx +++ b/typescript/packages/http/paywall/src/evm/entry.tsx @@ -2,6 +2,7 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { EvmPaywall } from "./EvmPaywall"; import { Providers } from "../Providers"; +import { IntroAnimation } from "../IntroAnimation"; import type {} from "../window"; // EVM-specific paywall entry point @@ -23,19 +24,21 @@ window.addEventListener("load", () => { const root = createRoot(rootElement); root.render( - { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("text/html")) { - document.documentElement.innerHTML = await response.text(); - } else { - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - window.location.href = url; - } - }} - /> + + { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("text/html")) { + document.documentElement.innerHTML = await response.text(); + } else { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + window.location.href = url; + } + }} + /> + , ); }); diff --git a/typescript/packages/http/paywall/src/evm/gen/template.ts b/typescript/packages/http/paywall/src/evm/gen/template.ts index d31f495b40..7efa21069b 100644 --- a/typescript/packages/http/paywall/src/evm/gen/template.ts +++ b/typescript/packages/http/paywall/src/evm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built EVM paywall template with inlined CSS and JS */ export const EVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n \n \n \n Payment Required\n \n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/styles.css b/typescript/packages/http/paywall/src/styles.css index ed9bb00cfb..9f511f546f 100644 --- a/typescript/packages/http/paywall/src/styles.css +++ b/typescript/packages/http/paywall/src/styles.css @@ -10,6 +10,7 @@ body { line-height: 1.5; -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } img, @@ -38,193 +39,354 @@ h6 { overflow-wrap: break-word; } -/* Custom Styles */ +/* ── Design tokens ── */ :root { - --background-color: #f9fafb; - --container-background-color: white; - --text-color: #111827; - --secondary-text-color: #4b5563; - --details-background-color: #f9fafb; - --details-background-color-hover: #f3f4f6; - --button-primary-color: #2563eb; - --button-primary-hover-color: #1d4ed8; - --button-secondary-color: #eef0f3; - --button-secondary-hover-color: #e9ebee; - --button-positive-color: #059669; - --button-positive-hover-color: #047857; - --button-error-color: #ef4444; - --button-error-hover-color: #dc2626; + --bg: #ffffff; + --card-bg: #ffffff; + --text: #1a1a1a; + --text-secondary: #6b7280; + --text-tertiary: #9ca3af; + --border: #e5e7eb; + --border-hover: #d1d5db; + --radius-card: 16px; + --radius-btn: 12px; + --radius-input: 10px; + --radius-detail: 12px; + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04), 0 6px 24px rgba(0, 0, 0, 0.06); + --btn-primary-bg: #0a7739; + --btn-primary-bg-hover: #08612e; + --btn-secondary-bg: #ffffff; + --btn-secondary-bg-hover: #f9fafb; + --positive: #0a7739; + --error: #dc2626; } body { min-height: 100vh; - background-color: var(--background-color); - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; -} - -.container { - max-width: 32rem; - margin: 4rem auto; - padding: 1.5rem; - background-color: var(--container-background-color); - border-radius: 0.75rem; + background-color: var(--bg); + font-family: "Inter", system-ui, -apple-system, sans-serif; + color: var(--text); display: flex; - flex-direction: column; align-items: center; - text-align: center; - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); + justify-content: center; } -.header { - display: flex; - flex-direction: column; - gap: 1rem; +/* Body is a flex container; ensure the React mount node doesn't shrink-wrap content. */ +#root { + width: 100%; } -.title { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-color); - margin-bottom: 0.5rem; +/* ── Page wrapper ── */ +.paywall-page { + width: 100%; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; } -.subtitle { - color: var(--secondary-text-color); +/* ── Card ── */ +.card { + width: 100%; + max-width: 420px; + background: var(--card-bg); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + border: 1px solid var(--border); + overflow: hidden; } -.instructions { - font-size: 0.9rem; - color: var(--secondary-text-color); - font-style: italic; +.card-body { + padding: 2.5rem 2.25rem 1.75rem; + display: flex; + flex-direction: column; + gap: 1.5rem; } -.content { +/* ── App branding (logo + name) ── */ +.app-branding { display: flex; flex-direction: column; - gap: 1rem; + align-items: center; + gap: 0.5rem; } -.input { - width: 100%; +.app-logo { + width: 48px; + height: 48px; + border-radius: 12px; + object-fit: contain; +} + +.app-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); +} + +/* ── Amount display ── */ +.amount-section { + text-align: center; +} + +.amount-value { + font-size: 3rem; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--text); + line-height: 1.1; +} + +.amount-asset { + font-size: 0.875rem; + color: var(--text-secondary); + margin-top: 0.25rem; + font-weight: 400; +} + +.resource-description { + text-align: center; + font-size: 0.8125rem; + color: var(--text-secondary); + line-height: 1.4; +} + +/* ── Payment details ── */ +.payment-details { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: var(--radius-detail); + overflow: hidden; +} + +.payment-row { + display: flex; + justify-content: space-between; + align-items: center; padding: 0.75rem 1rem; - border-radius: 0.5rem; - border: 1px solid #d1d5db; - background-color: white; - transition: border-color 150ms, box-shadow 150ms; + font-size: 0.8125rem; + border-bottom: 1px solid var(--border); } -.input:focus { - outline: none; - border-color: var(--button-primary-color); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); +.payment-row:last-child { + border-bottom: none; +} + +.payment-label { + color: var(--text-secondary); + font-weight: 400; +} + +.payment-value { + font-weight: 500; + color: var(--text); +} + +/* ── Buttons ── */ +.actions { + display: flex; + flex-direction: column; + gap: 0.625rem; } .button { width: 100%; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - font-weight: 600; + padding: 0.75rem 1.25rem; + font-weight: 500; + font-size: 0.9375rem; border: none; cursor: pointer; - transition: background-color 150ms; + border-radius: var(--radius-btn); + transition: background-color 150ms, opacity 150ms; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.button:disabled { + opacity: 0.45; + cursor: not-allowed; } .button-primary { - background-color: var(--button-primary-color); + background-color: var(--btn-primary-bg); + color: white; +} + +.button-primary:hover:not(:disabled) { + background-color: var(--btn-primary-bg-hover); +} + +.button-connect { + background-color: #9ca3af; color: white; } -.button-primary:hover { - background-color: var(--button-primary-hover-color); +.button-connect:hover:not(:disabled) { + background-color: #8b94a3; } .button-secondary { - background-color: var(--button-secondary-color); - color: var(--text-color); + background-color: var(--btn-secondary-bg); + color: var(--text); + border: 1px solid var(--border); } -.button-secondary:hover { - background-color: var(--button-secondary-hover-color); +.button-secondary:hover:not(:disabled) { + background-color: var(--btn-secondary-bg-hover); + border-color: var(--border-hover); } .button-positive { - background-color: var(--button-positive-color); + background-color: var(--positive); color: white; } -.button-positive:hover { - background-color: var(--button-positive-hover-color); -} - .button-error { - background-color: var(--button-error-color); + background-color: var(--error); color: white; } -.button-error:hover { - background-color: var(--button-error-hover-color); +/* ── Select / Input ── */ +.input { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius-input); + background-color: white; + font-size: 0.875rem; + color: var(--text); + transition: border-color 150ms; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236b7280' viewBox='0 0 16 16'%3E%3Cpath d='M4.47 5.97a.75.75 0 0 1 1.06 0L8 8.44l2.47-2.47a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 0 1 0-1.06z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px; + padding-right: 2.5rem; } -.payment-details { - padding: 1rem; - margin-bottom: 1rem; - background-color: var(--details-background-color); - border-radius: 0.5rem; +.input:focus { + outline: none; + border-color: var(--text); } -.payment-row { - display: flex; - justify-content: space-between; - font-size: 0.875rem; - margin-bottom: 0.5rem; +/* ── Custom wallet select ── */ +.wallet-select { + position: relative; + width: 100%; } -.payment-row:last-child { - margin-bottom: 0; +.wallet-select-trigger { + text-align: left; + cursor: pointer; + justify-content: flex-start; } -.payment-label { - color: var(--text-color); +.wallet-select-menu { + position: absolute; + left: 0; + right: 0; + top: calc(100% + 0.375rem); + border: 1px solid var(--border); + border-radius: var(--radius-input); + background: var(--card-bg); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12); + overflow: hidden; + z-index: 30; } -.payment-value { +.wallet-select-option { + width: 100%; + border: none; + background: transparent; + text-align: left; + padding: 0.75rem 1rem; + font-size: 0.875rem; + color: var(--text); + cursor: pointer; +} + +.wallet-select-option:hover, +.wallet-select-option.selected { + background: #f3f4f6; +} + +/* ── Balance toggle ── */ +.balance-button { + background: none; + border: none; + cursor: pointer; + font-size: 0.8125rem; font-weight: 500; + color: var(--text); + display: inline-flex; + align-items: center; + padding: 0; + min-width: 100px; + justify-content: flex-end; } -.hidden { - display: none; +.balance-button:hover { + color: var(--text-secondary); } +/* ── Status ── */ .status { text-align: center; - font-size: 0.875rem; + font-size: 0.8125rem; + color: var(--text-secondary); + padding: 0.25rem 0; } -.cta-container { +/* ── Card footer ── */ +.card-footer { + padding: 1rem 1.75rem 1.25rem; + border-top: 1px solid var(--border); display: flex; - flex-basis: 50%; - flex-direction: row; - gap: 0.5rem; - margin-top: 1rem; + flex-direction: column; + align-items: center; + gap: 0.75rem; } -.balance-button { - background-color: transparent; - border: none; - cursor: pointer; - min-height: 1rem; - min-width: 150px; +.faucet-link { + font-size: 0.75rem; + color: var(--text-tertiary); +} + +.faucet-link a { + color: var(--text-secondary); + font-weight: 500; + text-decoration: none; +} + +.faucet-link a:hover { + color: var(--text); + text-decoration: underline; +} + +.powered-by { + font-size: 0.6875rem; + color: var(--text-tertiary); display: flex; - justify-content: flex-end; align-items: center; + gap: 0.25rem; +} + +.powered-by a { + color: var(--text-secondary); + font-weight: 600; + text-decoration: none; + letter-spacing: -0.01em; } +.powered-by a:hover { + color: var(--text); +} + +/* ── Spinner ── */ @keyframes spin { to { transform: rotate(360deg); @@ -238,10 +400,26 @@ body { } .spinner > div { - animation: spin 1s linear infinite; - border: 2px solid #e5e7eb; - border-top-color: #3b82f6; + animation: spin 0.7s linear infinite; + border: 2px solid rgba(255, 255, 255, 0.25); + border-top-color: #ffffff; border-radius: 50%; - width: 1rem; - height: 1rem; -} \ No newline at end of file + width: 1.125rem; + height: 1.125rem; +} + +/* ── Lock icon for pay button ── */ +.lock-icon { + width: 14px; + height: 14px; + opacity: 0.6; +} + +/* ── Utilities ── */ +.hidden { + display: none; +} + +.w-full { + width: 100%; +} diff --git a/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx index 74fa2f4409..df4380c783 100644 --- a/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx +++ b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx @@ -8,6 +8,7 @@ import { encodePaymentSignatureHeader } from "@x402/core/http"; import type { PaymentRequired } from "@x402/core/types"; import { Spinner } from "./Spinner"; +import { WalletSelect } from "../WalletSelect"; import { getNetworkDisplayName, SOLANA_NETWORK_REFS } from "../paywallUtils"; import { getStandardConnectFeature, getStandardDisconnectFeature } from "./solana/features"; import { useSolanaBalance } from "./solana/useSolanaBalance"; @@ -42,11 +43,14 @@ export function SolanaPaywall({ paymentRequired, onSuccessfulResponse }: SolanaP const x402 = window.x402; const amount = x402.amount; + const appName = x402.appName; + const appLogo = x402.appLogo; const firstRequirement = paymentRequired.accepts[0]; if (!firstRequirement) { throw new Error("No payment requirements in paymentRequired.accepts"); } + const description = paymentRequired.resource?.description; const network = firstRequirement.network; const chainName = getNetworkDisplayName(network); @@ -218,105 +222,116 @@ export function SolanaPaywall({ paymentRequired, onSuccessfulResponse }: SolanaP ]); return ( -
-
-

Payment Required

-

- {paymentRequired.resource?.description && `${paymentRequired.resource.description}.`} To - access this content, please pay ${amount} {chainName} USDC. -

- {String(network).includes("devnet") && ( -

- Need Solana Devnet USDC?{" "} - - Request some here. - -

- )} -
+
+
+
+ {/* App branding */} + {(appLogo || appName) && ( +
+ {appLogo && {appName} + {appName && {appName}} +
+ )} -
-
-
- Wallet: - - {activeAccount - ? `${activeAccount.address.slice(0, 6)}...${activeAccount.address.slice(-4)}` - : "-"} - -
-
- Available balance: - - {activeAccount ? ( - - ) : ( - "-" - )} - + {/* Amount */} +
+
${amount}
+
USDC on {chainName}
-
- Amount: - ${amount} USDC -
-
- Network: - {chainName} + + {/* Payment details */} +
+
+ Wallet + + {activeAccount + ? `${activeAccount.address.slice(0, 6)}...${activeAccount.address.slice(-4)}` + : "-"} + +
+
+ Balance + + {activeAccount ? ( + + ) : ( + "-" + )} + +
+
+ Network + {chainName} +
-
-
- {activeAccount ? ( - - ) : ( - <> - + onChange={setSelectedWalletValue} + options={walletOptions.map(option => ({ + value: option.value, + label: option.wallet.name, + }))} + placeholder="Select a wallet" + /> - +
+ ) : ( +
+ + +
)} - {activeAccount && ( - + + {!walletOptions.length && ( +
+ Install a Solana wallet such as Phantom to continue, then refresh this page. +
)} -
- {!walletOptions.length && ( -
- Install a Solana wallet such as Phantom to continue, then refresh this page. -
- )} + {status &&
{status}
} +
- {status &&
{status}
} + {/* Footer */} +
+ {String(network).includes("devnet") && ( + + Need Solana Devnet USDC?{" "} + + Get some here + + + )} + + Powered by{" "} + + x402 + + +
); diff --git a/typescript/packages/http/paywall/src/svm/entry.tsx b/typescript/packages/http/paywall/src/svm/entry.tsx index 2350e0bbcc..c52f9e84cb 100644 --- a/typescript/packages/http/paywall/src/svm/entry.tsx +++ b/typescript/packages/http/paywall/src/svm/entry.tsx @@ -1,6 +1,7 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { SolanaPaywall } from "./SolanaPaywall"; +import { IntroAnimation } from "../IntroAnimation"; import type {} from "../window"; // SVM-specific paywall entry point @@ -21,18 +22,20 @@ window.addEventListener("load", () => { const root = createRoot(rootElement); root.render( - { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("text/html")) { - document.documentElement.innerHTML = await response.text(); - } else { - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - window.location.href = url; - } - }} - />, + + { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("text/html")) { + document.documentElement.innerHTML = await response.text(); + } else { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + window.location.href = url; + } + }} + /> + , ); }); diff --git a/typescript/packages/http/paywall/src/svm/gen/template.ts b/typescript/packages/http/paywall/src/svm/gen/template.ts index 954431627c..df841b60c5 100644 --- a/typescript/packages/http/paywall/src/svm/gen/template.ts +++ b/typescript/packages/http/paywall/src/svm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built SVM paywall template with inlined CSS and JS */ export const SVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n \n \n \n Payment Required\n \n \n
\n \n \n ';