Appearance
vela-widget
Embeddable checkout and subscription UX layer that merchants include on their own pages.
Purpose and Role
The product layer that turns protocol capabilities into an understandable subscriber flow. Merchants embed the widget to offer:
- Checkout flow for new subscriptions.
<vela-pricing-table>component for plan selection.- Wallet-native mandate UX that would be unclear or unsafe without guided flow.
The widget exists because wallet-native mandate UX cannot be assumed to be clear or safe enough on its own.
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| React | 19.x | Checkout UI in iframe |
| TypeScript | 5.8+ | Language |
| @vela/sdk | latest | Checkout transaction building (browser-safe barrel) |
| Tailwind CSS | 4.2.x | Styling |
| Shadow DOM | — | Style isolation from parent page |
| postMessage API | — | Wallet ↔ iframe communication |
| Cloudflare Workers | — | Hosting for loader script and iframe bundle |
Directory Structure
vela-widget/
├── loader/ # Small script injected on merchant pages
│ ├── src/
│ │ ├── index.ts # Loader entry — discovers wallet, creates iframe
│ │ ├── wallet-detect.ts # Parent page wallet discovery
│ │ └── postmessage.ts # Typed postMessage relay
│ └── package.json # Published as separate bundle (~5kb)
├── checkout/ # Full iframe checkout app
│ ├── src/
│ │ ├── app/
│ │ │ ├── components/
│ │ │ │ ├── CheckoutFlow.tsx # Main checkout flow
│ │ │ │ ├── PlanSelector.tsx # Plan selection UI
│ │ │ │ ├── WalletConnect.tsx # Wallet connection (via postMessage)
│ │ │ │ └── Confirmation.tsx # Transaction confirmation
│ │ │ └── hooks/
│ │ │ ├── useCheckout.ts # Checkout session management
│ │ │ └── useWalletSign.ts # Wallet signing via postMessage
│ │ └── index.tsx # iframe app entry
│ ├── tailwind.css
│ └── package.json
├── shared/ # Shared types for postMessage protocol
│ ├── messages.ts # Typed postMessage definitions
│ └── events.ts # Widget event types
├── vite.config.ts # Build config (separate bundles for loader + iframe)
└── package.jsonDeployment Target
- Cloudflare Workers: Static assets for loader script and iframe bundle.
- CDN: Loader script served from
widget.velapay.com/loader.js. - iframe: Checkout app loaded from
widget.velapay.com/checkout/.
Dependencies
What It Depends On
| Dependency | Type | Purpose |
|---|---|---|
@vela/sdk | npm package (browser-safe) | Checkout transaction building, PDAFactory |
What Depends on It
Nothing. Merchants embed the widget; no other repo depends on it.
Current Status
- v1.1 complete: Loader script, iframe checkout, postMessage relay.
- v1.5 updates:
<vela-pricing-table>component, Shadow DOM with DSD, theming. - v1.8 updates: Multi-token plan rendering via Phase 46 TokenConfig resolution.
Notable Design Decisions
Wallet-in-Parent, UI-in-Iframe Architecture
Wallets inject into the parent page (merchant's website), not the iframe. The loader handles wallet discovery on the parent page, and the iframe handles checkout UX. Typed postMessage bridges signing requests between the two contexts.
This split exists because:
- Wallet extensions (Phantom, Solflare) inject into the top-level window.
- iframes have a different origin — wallet injection doesn't propagate.
- The loader (on parent page) can access
window.solanaorwindow.phantom. - The iframe (VelaPay origin) renders checkout UI with strict CSP.
Shadow DOM with Declarative Shadow DOM (DSD)
Widget rendering uses Shadow DOM for style isolation from the parent page. DSD support enables SSR-compatible theming — the widget renders correctly before JavaScript loads.
Typed postMessage Protocol
All communication between parent and iframe uses a typed protocol:
typescript
// Parent → iframe
type ParentMessage =
| { type: "WALLET_AVAILABLE"; wallet: WalletAdapter }
| { type: "SIGNATURE_RESULT"; signature: string }
| { type: "SIGNATURE_ERROR"; error: string };
// iframe → Parent
type iframeMessage =
| { type: "SIGN_REQUEST"; transaction: Buffer }
| { type: "CHECKOUT_COMPLETE"; mandateAddress: string }
| { type: "CHECKOUT_CANCELLED" };Strict CSP in iframe
The iframe has a strict Content Security Policy that prevents access to parent page DOM, cookies, or storage. Only postMessage communication is allowed.
~5kb Loader Script
The loader is intentionally tiny — merchants won't embed a large script on their pages. All heavy UI loads lazily in the iframe after the merchant page has rendered.
Browser-Safe SDK
The widget uses @vela/sdk/browser barrel which excludes Node.js-only dependencies. This keeps the iframe bundle small and compatible with browser environments.