diff --git a/.changeset/feebearer-and-quote-fix.md b/.changeset/feebearer-and-quote-fix.md new file mode 100644 index 0000000..09e21c7 --- /dev/null +++ b/.changeset/feebearer-and-quote-fix.md @@ -0,0 +1,14 @@ +--- +"@usewhisk/core": minor +"@usewhisk/react": minor +--- + +Fee bearer control and a stale-quote fix. + +**`feeBearer`** — new config option. Default `"receiver"` preserves current behavior (CCTP + Forwarder fees come out of the transfer). Set `feeBearer: "sender"` to size the burn up by the estimated fees so the recipient receives the full amount — useful for checkout, payroll, and invoice flows. Quotes now expose `amountBurned` (the on-chain transfer amount) alongside `amountIn` (sender debit) and `amountOut` (recipient receives). In sender mode the gross-up pads the forwarding portion by a small margin (2%); the forwarder fee is re-priced from destination gas at mint time, so the cushion keeps the recipient at or above the requested amount despite drift, with any unused margin minted to them. + +**Fix** — changing the destination chain after the recipient resolved kept quoting against the original chain. A host-pinned recipient was resolved only once (a one-shot `useRef` latch) and the "Review" gate ignored the chain, so switching destinations reused the stale resolution and bridged to the wrong chain. Auto-resolve is now keyed on recipient + destination, and a resolved recipient whose chain no longer matches the selected destination is re-resolved before quoting. + +**Fix** — host theme overrides were ignored in dark mode. The widget's dark palette lived on `[data-whisk][data-whisk-theme="dark"]` (specificity 0,2,0), which out-specified a host re-theming `--whisk-*` on `[data-whisk]` (0,1,0) — so a dark integration kept the default wine/terracotta colors. The theme discriminators are now wrapped in `:where()`, keeping every theme block at `[data-whisk]` specificity, so host overrides win in light, dark, and system (on the card and in portals). + +**Fix** — wallet errors surfaced the raw viem/App Kit dump (`Request Arguments: …`, `Version: viem@…`). Declining the wallet prompt now reads "You cancelled the transaction in your wallet.", and other errors are trimmed to their human first line (App Kit's "Unknown blockchain error on \:" wrapper and the argument dump are stripped). The full provider error is preserved on `cause` for debugging. This cleanup now covers every surface that renders an error — the transfer result screen, the connect modal (EVM and Solana), the manual-mint flow, and the ENS resolver — not just the bridge path. The message cleaner is also exported as `cleanErrorMessage` for custom surfaces. diff --git a/README.md b/README.md index 9cbbe17..afb7a49 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ export default { | **Multi-wallet** | Ecosystem-first connect modal — EVM (MetaMask, Coinbase, WalletConnect, Rabby) and Solana (Phantom, Solflare, Backpack) never collide. | | **Recipient resolvers** | Address + ENSIP-11 multichain ENS built in; bring your own (email, phone…). | | **Fee transparency** | Custom + protocol + gas + forwarder, displayed before confirm. | +| **Fee bearer** | `feeBearer: "sender"` grosses up the burn so the recipient nets the exact amount; default `"receiver"` deducts fees from the transfer. | | **Progress streaming** | Live step state from App Kit's bridge events. | | **Mid-flight recovery** | Burn-but-no-mint failures: in-tab retry, cross-refresh persistence (localStorage, 48h), manual-mint escape hatch via direct `MessageTransmitter.receiveMessage`. | | **Pre-flight checks** | Read-only balance / gas / chain-alignment inspection before the user signs. | diff --git a/apps/docs/src/content/docs/api/create-whisk-config.mdx b/apps/docs/src/content/docs/api/create-whisk-config.mdx index 67a862b..19964f0 100644 --- a/apps/docs/src/content/docs/api/create-whisk-config.mdx +++ b/apps/docs/src/content/docs/api/create-whisk-config.mdx @@ -28,7 +28,8 @@ createWhiskConfig(options: CreateWhiskConfigOptions): WhiskClientConfig | `defaultDestinationChain` | `Chain` | no | Initial destination. | | `token` | `Token` | no | Default token alias. USDC today. | | `resolver` | `Resolver` | no | Custom resolver chain. Defaults to address + ENS. | -| `feePolicy` | `FeePolicy` | no | Platform fee config. | +| `feePolicy` | `FeePolicy` | no | Platform fee, charged on top of the transfer. | +| `feeBearer` | `"sender" \| "receiver"` | no | Who absorbs the bridge fees. Defaults to `"receiver"`. | | `rpcUrls` | `Partial>` | no | Override the default public RPC for a chain. | | `useForwarder` | `boolean` | no | Forwarder Service. Defaults to `true`. | | `appLabel` | `string` | no | Shown in some wallet UIs as the connecting app's name. | @@ -94,10 +95,23 @@ const config = createWhiskConfig({ wallets: [evm({ projectId: "abc123…" })], chains: ["Base"], feePolicy: { - flat: "0.05", - percent: 0.002, - recipient: "0xMyFeeTreasury…", - label: "Service fee", + value: "0.05", // absolute USDC, added on top of the transfer + recipient: "0xMyFeeTreasury…", // split 90% to you, 10% to Arc }, }); ``` + +### Recipient receives the exact amount + +Set `feeBearer: "sender"` so the bridge fees are added to the sender's +debit instead of deducted from the transfer. Useful for checkout, +payroll, and invoice flows where the recipient needs to net an exact +figure. See [Fees and quotes](/docs/concepts/fees#who-pays-the-bridge-fees-feebearer). + +```ts +const config = createWhiskConfig({ + wallets: [evm({ projectId: "abc123…" })], + chains: ["Base", "Arbitrum"], + feeBearer: "sender", +}); +``` diff --git a/apps/docs/src/content/docs/api/types.mdx b/apps/docs/src/content/docs/api/types.mdx index 848d4de..c897340 100644 --- a/apps/docs/src/content/docs/api/types.mdx +++ b/apps/docs/src/content/docs/api/types.mdx @@ -83,18 +83,20 @@ type FeeBreakdown = FeeEntry[]; ```ts type Quote = { - route: Route; - sourceChain: Chain; - destinationChain: Chain; - amountIn: string; - amountOut: string; - fees: FeeBreakdown; - expiresAt: number; // ms-epoch + route: Route; // same-chain "send" or cross-chain "bridge" recipient: ResolvedRecipient; + amountIn: string; // total debited from the sender ("You pay") + amountOut: string; // what the recipient receives ("Recipient gets") + amountBurned?: string; // on-chain transfer amount; grossed up in sender mode token: Token; + fees: FeeBreakdown; + estimatedDurationMs?: number; }; ``` +`amountIn`, `amountOut`, and `amountBurned` can all differ on a bridge, +depending on [`feeBearer`](/docs/concepts/fees#who-pays-the-bridge-fees-feebearer). + ## `FeePolicy` ```ts diff --git a/apps/docs/src/content/docs/concepts/fees.mdx b/apps/docs/src/content/docs/concepts/fees.mdx index a1310d2..84034de 100644 --- a/apps/docs/src/content/docs/concepts/fees.mdx +++ b/apps/docs/src/content/docs/concepts/fees.mdx @@ -1,101 +1,164 @@ --- title: Fees and quotes -description: What's in a Quote, how fees are calculated, and how to charge your own platform fee. +description: What a Quote contains, who pays the bridge fees, and how to charge your own platform fee. --- -A `Quote` is the engine's answer to "if I send X to Y, what -happens?". It's pure data until the user clicks confirm, no -transaction is submitted. You can build a quote, throw it away, -build another one, do whatever you want with it before the user -commits. +A `Quote` is the engine's answer to "if I send X to Y, what happens?". +It's pure data. Nothing is signed or submitted until the user confirms, +so you can build a quote, inspect it, throw it away, and build another +one freely. ## What a Quote contains ```ts type Quote = { route: Route; // same-chain "send" or cross-chain "bridge" - sourceChain: Chain; - destinationChain: Chain; - amountIn: string; // what the user types - amountOut: string; // what the recipient receives - fees: FeeBreakdown; // every line item - expiresAt: number; // ms-epoch, quotes get stale recipient: ResolvedRecipient; + amountIn: string; // total debited from the sender ("You pay") + amountOut: string; // what the recipient receives ("Recipient gets") + amountBurned?: string; // the on-chain transfer amount fed to App Kit token: Token; + fees: FeeBreakdown; // every line item + estimatedDurationMs?: number; }; ``` -`amountIn - amountOut === sumFees(fees)`. Whisk never sneaks in a -fee that isn't enumerated in the breakdown. If the math doesn't add -up, that's a bug — file it. +Three amounts, because on a bridge they can all differ: + +- **`amountIn`** is what leaves the sender's wallet. This is the "You + pay" figure on the review screen. +- **`amountOut`** is what the recipient receives. This is the "Recipient + gets" figure. +- **`amountBurned`** is the amount handed to CCTP on the source chain. + Fees are deducted from it during the bridge. It equals `amountOut` in + receiver mode and is grossed up in sender mode (see below). The widget + uses this internally; you rarely read it directly. ## Where fees come from -Two sources contribute: +Two sources contribute, and they behave differently. + +**Circle App Kit fees** are the cost of moving USDC across chains: + +| Fee | When it applies | Sized by | +| ---------------------- | -------------------------------------------- | ------------------------------------------------- | +| CCTP protocol fee | Fast Transfers only (Standard is free) | A few basis points of the amount, by source chain | +| Forwarding Service fee | When the destination mint is relayed for you | Destination-chain gas | +| Gas | Source-chain approve/burn | Paid in the source chain's native token | + +The CCTP protocol and Forwarding fees come out of the bridged USDC. Gas +is paid separately in the native token, so it never reduces the USDC the +recipient receives. + +The Forwarding fee is the one to watch. It tracks destination-chain gas, +not the transfer amount, so it's a near-fixed cost. Bridging to an L2 +(Base, Arbitrum, Optimism, Arc) costs cents; bridging to Ethereum L1 can +cost dollars regardless of how much you send. Pick cheap destinations for +small transfers. -- **App Kit fees.** Gas, the bridge protocol fee, the forwarder fee - when applicable. These come from Circle's quote API; Whisk passes - them through. -- **Your platform fee.** Optional. If you've configured a `feePolicy` - on the config, Whisk adds your line item and (if you've set a - recipient) routes the cut to your treasury. +**Your platform fee** is optional and added on top. See +[Adding a platform fee](#adding-a-platform-fee). -The review step renders each line so the user can see what they're -paying before they sign. +## Who pays the bridge fees: `feeBearer` + +By default the recipient absorbs the App Kit fees, the same way a wire +transfer arrives short of the amount sent. For a checkout, payroll run, +or invoice, that's usually wrong: the merchant needs to net an exact +figure. `feeBearer` controls this. + +```ts +const config = createWhiskConfig({ + wallets: [evm({ projectId })], + chains: ["Base", "Arbitrum"], + feeBearer: "sender", // default is "receiver" +}); +``` + +**`"receiver"` (default)** — the bridge fees are deducted from the +transfer. The sender is debited the amount; the recipient nets +`amount − fees`. + +``` +amount = 100 USDC, fees = 0.30 USDC +You pay: 100.00 USDC +Recipient gets: 99.70 USDC +``` + +**`"sender"`** — the burn is sized up by the estimated fees so the +recipient receives the full amount. The sender's debit grows to cover +them. + +``` +amount = 100 USDC, fees = 0.30 USDC +You pay: 100.30 USDC +Recipient gets: 100.00 USDC +``` + +### The accuracy caveat + +In sender mode the gross-up uses the fee **estimated at quote time**. The +Forwarding fee is priced from destination gas at mint time, which lands a +little later, so the live cost can drift from the estimate. To keep the +recipient whole, the gross-up pads the forwarding portion by a small margin +(2%) before sizing the burn. The recipient therefore receives **at least** +the requested amount; if gas comes in under the estimate, the unused margin +is minted to them. Penny-exact delivery isn't possible with a variable-fee +bridge unless you control the mint with a custom contract, so Whisk rounds +in the recipient's favor. + +Same-chain sends never deduct USDC fees (only native gas), so `feeBearer` +is a no-op there: the recipient always receives the full amount. ## Adding a platform fee -Pass `feePolicy` to `createWhiskConfig`: +`feePolicy` charges your own fee on top of the transfer. Per Circle App +Kit, the fee is split 90% to your `recipient` and 10% to Arc, and it's +collected on the source chain. ```ts const config = createWhiskConfig({ wallets: [evm({ projectId })], chains: ["Base"], feePolicy: { - flat: "0.05", // 5¢ flat - percent: 0.001, // plus 0.1% - recipient: "0xYourTreasury…", // fee wallet - label: "Platform fee", // shown in the review UI + value: "0.05", // absolute USDC amount, added on top + recipient: "0xYourTreasury…", // your fee wallet (source chain) }, }); ``` -Every field is optional. Mix flat + percent, leave `recipient` off -to display the fee without collecting (useful when you're staging a -rollout), or skip `feePolicy` entirely if you don't charge. +`feePolicy` and `feeBearer` are independent. Your platform fee is always +added on top of the amount; `feeBearer` only decides who absorbs the +network fees. With both set to `"sender"` plus a `feePolicy`, the sender +is debited `amount + network fees + your fee`, and the recipient still +nets the full amount. ## Building a quote without rendering -Want a fee preview on a checkout summary before the widget mounts? -Call the engine directly. The engine doesn't render, so this works -on the server or in a button click handler: +Want a fee preview on a checkout summary before the widget mounts? Call +the engine directly. It doesn't render, so this works in a click handler +or anywhere you hold an adapter: ```ts const quote = await engine.quote({ recipient, amount: "100", sourceChain: "Base", + destinationChain: "Arbitrum", adapter, }); -console.log(quote.amountOut); // "99.92" -console.log(quote.fees); +console.log(quote.amountIn); // "100.3" in sender mode +console.log(quote.amountOut); // "100" +console.log(quote.fees.entries); // → [ -// { kind: "gas", label: "Gas", amount: "0.04" }, -// { kind: "platform", label: "Platform fee", amount: "0.04" }, +// { kind: "provider", amount: "0.013", token: "USDC" }, +// { kind: "forwarder", amount: "0.287", token: "USDC" }, // ] ``` -## Stale quotes - -`expiresAt` is in milliseconds since epoch. Quotes go stale fast -because gas prices and bridge availability shift. If a user lingers -on a confirmation screen for a few minutes, re-quote before they -sign — `engine.quote(...)` is idempotent, and the new quote will -have a fresh `expiresAt`. - -The bundled `` handles this automatically. The check -matters when you're driving the engine yourself. +Quotes go stale as gas prices shift, so re-quote before the user signs if +they've lingered. The bundled `` handles that for you; it +matters when you drive the engine yourself. ## Next diff --git a/apps/docs/src/content/docs/getting-started/install.mdx b/apps/docs/src/content/docs/getting-started/install.mdx index 5c0c113..8f74903 100644 --- a/apps/docs/src/content/docs/getting-started/install.mdx +++ b/apps/docs/src/content/docs/getting-started/install.mdx @@ -54,6 +54,45 @@ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your-project-id protocol. No project ID needed on that side. +## Vite + Solana: polyfill `Buffer` + +Skip this unless you're on **Vite** _and_ using Solana. Next.js polyfills +Node globals for you, so App Router and Pages Router apps need nothing here. + +`@solana/web3.js` reaches for Node's `Buffer` global (base58 encoding, +transaction serialization). The browser has no `Buffer`, and Vite doesn't +shim it, so the first Solana operation — connecting a Solana wallet, or a +CCTP bridge that crosses the Solana/EVM boundary — throws +`Buffer is not defined`. + +Add the polyfill once, at the very top of your entry file, before any other +import so it's in place before wallet code loads: + +```tsx title="src/main.tsx" +import { Buffer } from "buffer"; +if (typeof globalThis.Buffer === "undefined") { + globalThis.Buffer = Buffer; +} + +import React from "react"; +import ReactDOM from "react-dom/client"; +import "@usewhisk/react/styles.css"; +import { App } from "./App"; +// … +``` + +The `buffer` package ships as a transitive dependency of the Solana SDK, but +some package managers won't surface it to your app's `node_modules`. If the +import doesn't resolve, install it directly: + + + + + Prefer a plugin? `vite-plugin-node-polyfills` handles `Buffer` (plus `process` + and `crypto`) for you. The three-line shim above is lighter if Buffer is all + you need. + + ## Verify the install Quick sanity check before you move on. Add this to any page in your diff --git a/examples/donate-button/README.md b/examples/donate-button/README.md index b3244b5..e0248e6 100644 --- a/examples/donate-button/README.md +++ b/examples/donate-button/README.md @@ -22,6 +22,8 @@ with a locked recipient + soft-prefilled amount per tier. - Public donor wall, impact stats, and an active-projects grid with per-project progress. - `onSuccess` shows a tier-specific thank-you with a tx hash link. +- Runs `feeBearer: "sender"` so the donor covers the bridge fees and the + treasury receives the full tier amount ($25 lands as $25). ## Stack diff --git a/examples/donate-button/package.json b/examples/donate-button/package.json index c714e42..f51ae72 100644 --- a/examples/donate-button/package.json +++ b/examples/donate-button/package.json @@ -17,6 +17,7 @@ "@tanstack/react-query": "^5.59.0", "@usewhisk/core": "workspace:*", "@usewhisk/react": "workspace:*", + "buffer": "^6.0.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/donate-button/src/components/donate-card.tsx b/examples/donate-button/src/components/donate-card.tsx index 0f4fc4c..9c4a149 100644 --- a/examples/donate-button/src/components/donate-card.tsx +++ b/examples/donate-button/src/components/donate-card.tsx @@ -59,21 +59,23 @@ function WidgetSurface({ }) { return (
- - onPaid({ amount: quote.amountIn, txHash: finalTxHash }) - } - /> +
+ + onPaid({ amount: quote.amountIn, txHash: finalTxHash }) + } + /> +

{widgetAmount - ? `Tier locked — $${widgetAmount} · recipient pinned` - : "Custom amount · recipient pinned"} + ? `Tier locked — $${widgetAmount} · fees covered, OpenForest receives the full amount` + : "Custom amount · fees covered, OpenForest receives the full amount"}

); diff --git a/examples/donate-button/src/data/tiers.ts b/examples/donate-button/src/data/tiers.ts index 5f2cc72..cbdce27 100644 --- a/examples/donate-button/src/data/tiers.ts +++ b/examples/donate-button/src/data/tiers.ts @@ -5,7 +5,7 @@ export type Tier = { caption: string; }; -export const TREASURY_ADDRESS = "0xbFc6981dE968C96058932963e5d2B7621DEa8f59"; +export const TREASURY_ADDRESS = "0xbe03CE9d6001D27BE41fc87e3E3f777d04e70Fe2"; export const ANNUAL_GOAL = 250_000; export const RAISED = 187_420; diff --git a/examples/donate-button/src/main.tsx b/examples/donate-button/src/main.tsx index 401a69c..060bbf5 100644 --- a/examples/donate-button/src/main.tsx +++ b/examples/donate-button/src/main.tsx @@ -1,3 +1,13 @@ +// Polyfill Buffer for the browser before any Solana code loads. +// @solana/web3.js uses Node's `Buffer` global (for base58 encoding, +// transaction serialization, etc.). Next.js polyfills this automatically; +// Vite does not — so without this line, anything that touches Solana +// (e.g. CCTP cross-ecosystem bridges) throws "Buffer is not defined". +import { Buffer } from "buffer"; +if (typeof globalThis.Buffer === "undefined") { + globalThis.Buffer = Buffer; +} + import React from "react"; import ReactDOM from "react-dom/client"; import "@usewhisk/react/styles.css"; diff --git a/examples/donate-button/src/providers.tsx b/examples/donate-button/src/providers.tsx index 138d60c..abca754 100644 --- a/examples/donate-button/src/providers.tsx +++ b/examples/donate-button/src/providers.tsx @@ -15,9 +15,17 @@ export function Providers({ children }: { children: ReactNode }) { () => createWhiskConfig({ mode: "testnet", + // Donor covers the bridge fees so the treasury receives the full tier + // amount (a $25 donation lands as $25, not $25 minus fees). + feeBearer: "sender", wallets: [ evm({ - chains: ["Arc_Testnet", "Base_Sepolia", "Ethereum_Sepolia"], + chains: [ + "Arc_Testnet", + "Base_Sepolia", + "Ethereum_Sepolia", + "Optimism_Sepolia", + ], projectId: import.meta.env.VITE_WALLETCONNECT_PROJECT_ID, appName: "OpenForest", }), @@ -27,14 +35,22 @@ export function Providers({ children }: { children: ReactNode }) { "Arc_Testnet", "Base_Sepolia", "Ethereum_Sepolia", + "Optimism_Sepolia", "Solana_Devnet", ], + // Donors give from any chain (incl. Ethereum + Solana); the treasury + // receives on Optimism, an L2 where the Forwarder mint fee is cents, + // not the dollars it costs to mint on Ethereum L1. defaultSourceChain: "Arc_Testnet", - defaultDestinationChain: "Arc_Testnet", + defaultDestinationChain: "Optimism_Sepolia", appLabel: "whisk-example-donate", }), [], ); - return {children}; + return ( + + {children} + + ); } diff --git a/examples/donate-button/src/styles.css b/examples/donate-button/src/styles.css index 0355033..d657815 100644 --- a/examples/donate-button/src/styles.css +++ b/examples/donate-button/src/styles.css @@ -55,16 +55,42 @@ body { } /* Widget palette overrides inside .of-widget */ -.of-widget [data-whisk] { +/* ============================================================ + Widget theme — overrides every `--whisk-*` token to the OpenForest + palette. The selector is `[data-whisk]` (not `.of-widget [data-whisk]`) + because `` puts the `data-whisk` attribute on the + outer app wrapper — it's an ancestor of every recipe surface, and + CSS variables cascade through portals via WhiskScope. + ============================================================ */ +[data-whisk] { + /* surfaces */ --whisk-bg: var(--color-paper); --whisk-card: var(--color-paper); + --whisk-card-fg: var(--color-ink); --whisk-fg: var(--color-ink); --whisk-fg-muted: color-mix(in srgb, var(--color-ink) 65%, transparent); + /* lines */ --whisk-border: var(--color-line); --whisk-input: var(--color-line); + --whisk-border-w: 1px; + /* accent */ --whisk-primary: var(--color-moss); --whisk-primary-fg: var(--color-paper); + --whisk-ring: var(--color-fern); + /* semantic */ + --whisk-success: var(--color-moss); + --whisk-warning: var(--color-sun); + --whisk-destructive: var(--color-bark); + --whisk-destructive-fg: var(--color-paper); + /* radii */ --whisk-radius: 12px; + --whisk-radius-sm: 8px; + --whisk-radius-md: 12px; + --whisk-radius-lg: 16px; + /* fonts — inherit the recipe's stack so the widget reads native */ + --whisk-font: var(--font-sans); + --whisk-font-display: var(--font-display); + --whisk-font-mono: var(--font-mono); } @keyframes of-grow { diff --git a/examples/ecommerce-checkout/README.md b/examples/ecommerce-checkout/README.md index a96e04b..fb3dcb4 100644 --- a/examples/ecommerce-checkout/README.md +++ b/examples/ecommerce-checkout/README.md @@ -20,6 +20,8 @@ locked into a fixed price + merchant address at the payment step. - `onSuccess` flips state to an order confirmation with a tx hash, order ID, and itemized list. In a real app this is where your backend would catch a webhook and finalize the order. +- Runs `feeBearer: "sender"` so the customer covers the bridge fees and + the merchant receives the exact cart total. ## Stack diff --git a/examples/ecommerce-checkout/src/app/globals.css b/examples/ecommerce-checkout/src/app/globals.css index 8d51fee..a4ee277 100644 --- a/examples/ecommerce-checkout/src/app/globals.css +++ b/examples/ecommerce-checkout/src/app/globals.css @@ -48,16 +48,39 @@ body { widget reads as native to the store, not as a pasted-in card. Scoped to .ah-widget so other surfaces using (if any) stay on the widget's own brand. */ -.ah-widget [data-whisk] { +/* Widget theme — selector is `[data-whisk]` (set on the provider's outer + wrapper) so the cascade reaches every widget element including portals. + The host CSS loads after @usewhisk/react/styles.css in main entry, so + these rules win at equal specificity. */ +[data-whisk] { + /* surfaces */ --whisk-bg: var(--color-sand); --whisk-card: var(--color-paper); + --whisk-card-fg: var(--color-charcoal); --whisk-fg: var(--color-charcoal); --whisk-fg-muted: color-mix(in srgb, var(--color-charcoal) 70%, transparent); + /* lines */ --whisk-border: var(--color-line); --whisk-input: var(--color-line); + --whisk-border-w: 1px; + /* accent */ --whisk-primary: var(--color-charcoal); --whisk-primary-fg: var(--color-sand); + --whisk-ring: var(--color-tobacco); + /* semantic */ + --whisk-success: var(--color-leaf); + --whisk-warning: var(--color-tobacco); + --whisk-destructive: var(--color-tobacco-deep); + --whisk-destructive-fg: var(--color-sand); + /* radii */ --whisk-radius: 10px; + --whisk-radius-sm: 6px; + --whisk-radius-md: 10px; + --whisk-radius-lg: 14px; + /* fonts — serif display matches the editorial brand */ + --whisk-font: var(--font-sans); + --whisk-font-display: var(--font-display); + --whisk-font-mono: var(--font-mono); } @keyframes ah-pop { diff --git a/examples/ecommerce-checkout/src/app/page.tsx b/examples/ecommerce-checkout/src/app/page.tsx index f100621..9b9d663 100644 --- a/examples/ecommerce-checkout/src/app/page.tsx +++ b/examples/ecommerce-checkout/src/app/page.tsx @@ -1,42 +1,5 @@ import { ClientGate } from "./client-gate"; export default function Page() { - return ( -
-
- - - -
- -
-

Pay-with-USDC storefront, in production-shape.

-

- A real e-commerce flow built around <WhiskSend />. - Pick a product, confirm, settle on Arc Testnet — no card form, no - merchant gateway, no chargebacks. -

-
- -
- -
- -
- Demo only · merchant address + amount are pinned via - Whisk's controlled props -
-
- ); + return ; } diff --git a/examples/ecommerce-checkout/src/app/providers.tsx b/examples/ecommerce-checkout/src/app/providers.tsx index 5404a47..ffa6368 100644 --- a/examples/ecommerce-checkout/src/app/providers.tsx +++ b/examples/ecommerce-checkout/src/app/providers.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { WhiskProvider, createWhiskConfig, evm } from "@usewhisk/react"; /** - * The merchant fixes a single chain (Arc Testnet here). The `chains` + * The merchant fixes a single chain (Base Sepolia here). The `chains` * array contains only that chain so the widget's pickers collapse to * a no-op — there's nothing to choose. In a real app you'd configure * whatever chain your back-office has settled on for receivables. @@ -14,20 +14,27 @@ export function Providers({ children }: { children: React.ReactNode }) { () => createWhiskConfig({ mode: "testnet", + // Customer covers the bridge fees so the merchant receives the exact + // cart total. Otherwise fees would be deducted from the price. + feeBearer: "sender", wallets: [ evm({ - chains: ["Arc_Testnet"], + chains: ["Base_Sepolia"], projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, appName: "Atelier Hibiscus", }), ], - chains: ["Arc_Testnet"], - defaultSourceChain: "Arc_Testnet", - defaultDestinationChain: "Arc_Testnet", + chains: ["Base_Sepolia"], + defaultSourceChain: "Base_Sepolia", + defaultDestinationChain: "Base_Sepolia", appLabel: "whisk-example-ecommerce", }), [], ); - return {children}; + return ( + + {children} + + ); } diff --git a/examples/ecommerce-checkout/src/components/checkout/order-summary.tsx b/examples/ecommerce-checkout/src/components/checkout/order-summary.tsx index 639dac2..e4c6769 100644 --- a/examples/ecommerce-checkout/src/components/checkout/order-summary.tsx +++ b/examples/ecommerce-checkout/src/components/checkout/order-summary.tsx @@ -47,9 +47,16 @@ function LineItemRow({
  • + className="h-14 w-14 shrink-0 overflow-hidden rounded-lg" + style={{ backgroundColor: line.product.fallbackColor }} + > + +
    {line.product.name} diff --git a/examples/ecommerce-checkout/src/components/checkout/payment-aside.tsx b/examples/ecommerce-checkout/src/components/checkout/payment-aside.tsx index 6319292..3e208a5 100644 --- a/examples/ecommerce-checkout/src/components/checkout/payment-aside.tsx +++ b/examples/ecommerce-checkout/src/components/checkout/payment-aside.tsx @@ -23,16 +23,16 @@ export function PaymentAside({ totalStr, onPaid }: PaymentAsideProps) {

    - Amount and merchant address are locked from the cart. Pick a chain in - the widget — settles in seconds. + Amount and merchant address are locked from the cart. Bridge fees are + added to your total, so Atelier Hibiscus receives the exact price.

    onPaid(finalTxHash)} />
    diff --git a/examples/ecommerce-checkout/src/components/shop/product-card.tsx b/examples/ecommerce-checkout/src/components/shop/product-card.tsx index 92148c1..6d54ee8 100644 --- a/examples/ecommerce-checkout/src/components/shop/product-card.tsx +++ b/examples/ecommerce-checkout/src/components/shop/product-card.tsx @@ -17,9 +17,15 @@ export function ProductCard({ product, inCart, onAdd }: ProductCardProps) { return (
    + {product.name} + className="h-[18px] w-[18px] shrink-0 overflow-hidden rounded-full" + style={{ backgroundColor: c.product.fallbackColor }} + > + + {c.qty}× {c.product.name} ))} diff --git a/examples/ecommerce-checkout/src/data/catalog.ts b/examples/ecommerce-checkout/src/data/catalog.ts index 3d6f8b5..2ffbfe7 100644 --- a/examples/ecommerce-checkout/src/data/catalog.ts +++ b/examples/ecommerce-checkout/src/data/catalog.ts @@ -8,8 +8,10 @@ export type Product = { caption: string; priceUsdc: number; variants: Variant[]; - /** Two-stop radial gradient composed to read as a product photograph. */ - art: string; + /** Product photo. Unsplash CDN — stable URLs, free commercial license. */ + image: string; + /** Solid fallback while the image loads, picked to match the photograph. */ + fallbackColor: string; category: "Apparel" | "Home" | "Stationery" | "Accessories"; }; @@ -29,25 +31,20 @@ export const CATALOG: Product[] = [ { id: "l", label: "L" }, { id: "xl", label: "XL" }, ], - art: "radial-gradient(120% 80% at 30% 25%, #d6c4a3 0%, #b89a72 55%, #8a6a48 100%)", + image: + "https://images.unsplash.com/photo-1620799140408-edc6dcb6d633?w=800&q=80&auto=format&fit=crop", + fallbackColor: "#b89a72", category: "Apparel", }, - { - id: "candle", - name: "Fig + cedar candle", - caption: "Ceramic vessel · 8oz · 45-hour burn", - priceUsdc: 32, - variants: [{ id: "8oz", label: "8oz" }], - art: "radial-gradient(120% 80% at 70% 30%, #f0e2c4 0%, #c69b6f 60%, #7a4f30 100%)", - category: "Home", - }, { id: "pin", name: "Tortoise reading glasses", caption: "Acetate · amber tortoise · neutral lens", priceUsdc: 29, variants: [{ id: "amber", label: "Amber" }], - art: "radial-gradient(120% 80% at 25% 30%, #b89160 0%, #6a4423 65%, #2a1a0c 100%)", + image: + "https://images.unsplash.com/photo-1574258495973-f010dfbb5371?w=800&q=80&auto=format&fit=crop", + fallbackColor: "#6a4423", category: "Accessories", }, { @@ -59,7 +56,9 @@ export const CATALOG: Product[] = [ { id: "natural", label: "Natural" }, { id: "umber", label: "Umber" }, ], - art: "radial-gradient(120% 80% at 65% 40%, #cdb094 0%, #94714d 60%, #5a3d23 100%)", + image: + "https://images.unsplash.com/photo-1591561954557-26941169b49e?w=800&q=80&auto=format&fit=crop", + fallbackColor: "#94714d", category: "Accessories", }, { @@ -71,7 +70,9 @@ export const CATALOG: Product[] = [ { id: "rust", label: "Rust" }, { id: "olive", label: "Olive" }, ], - art: "radial-gradient(120% 80% at 30% 25%, #c98c66 0%, #944c2e 65%, #4f1f0d 100%)", + image: + "https://images.unsplash.com/photo-1517842645767-c639042777db?w=800&q=80&auto=format&fit=crop", + fallbackColor: "#944c2e", category: "Stationery", }, ]; diff --git a/examples/invoice-link/README.md b/examples/invoice-link/README.md index 7fbc664..d7d5bfb 100644 --- a/examples/invoice-link/README.md +++ b/examples/invoice-link/README.md @@ -21,6 +21,8 @@ The whole integration is a URL. No SDK on the customer's side. and copy-to-clipboard. - Coral/sage palette (distinct from the Atelier Hibiscus ecommerce recipe — same "Hibiscus" mood, different studio). +- Runs `feeBearer: "sender"` so the payer covers the bridge fees and the + freelancer receives the exact invoiced amount. ## Stack diff --git a/examples/invoice-link/src/app/globals.css b/examples/invoice-link/src/app/globals.css index 52841b7..f1177df 100644 --- a/examples/invoice-link/src/app/globals.css +++ b/examples/invoice-link/src/app/globals.css @@ -53,16 +53,38 @@ body { -moz-osx-font-smoothing: grayscale; } -.sh-widget [data-whisk] { +/* Widget theme — set on the provider's `[data-whisk]` so the cascade + reaches portals. Loads after @usewhisk/react/styles.css → wins at + equal specificity. */ +[data-whisk] { + /* surfaces */ --whisk-bg: var(--color-paper); --whisk-card: var(--color-paper); + --whisk-card-fg: var(--color-ink); --whisk-fg: var(--color-ink); --whisk-fg-muted: var(--color-ink-muted); + /* lines */ --whisk-border: var(--color-line); --whisk-input: var(--color-line); + --whisk-border-w: 1px; + /* accent */ --whisk-primary: var(--color-coral); --whisk-primary-fg: var(--color-paper); + --whisk-ring: var(--color-coral-deep); + /* semantic */ + --whisk-success: var(--color-sage-deep); + --whisk-warning: var(--color-sunset); + --whisk-destructive: var(--color-coral-deep); + --whisk-destructive-fg: var(--color-paper); + /* radii */ --whisk-radius: 10px; + --whisk-radius-sm: 6px; + --whisk-radius-md: 10px; + --whisk-radius-lg: 14px; + /* fonts */ + --whisk-font: var(--font-sans); + --whisk-font-display: var(--font-display); + --whisk-font-mono: var(--font-mono); } @keyframes sh-rise { diff --git a/examples/invoice-link/src/app/providers.tsx b/examples/invoice-link/src/app/providers.tsx index 7335ab1..bf381d5 100644 --- a/examples/invoice-link/src/app/providers.tsx +++ b/examples/invoice-link/src/app/providers.tsx @@ -21,6 +21,9 @@ export function Providers({ children }: { children: React.ReactNode }) { () => createWhiskConfig({ mode: "testnet", + // Payer covers the bridge fees so the freelancer receives the exact + // invoiced amount. + feeBearer: "sender", wallets: [ evm({ chains: SUPPORTED, @@ -36,5 +39,9 @@ export function Providers({ children }: { children: React.ReactNode }) { [], ); - return {children}; + return ( + + {children} + + ); } diff --git a/examples/invoice-link/src/components/pay-aside.tsx b/examples/invoice-link/src/components/pay-aside.tsx index fd49b95..4659ad7 100644 --- a/examples/invoice-link/src/components/pay-aside.tsx +++ b/examples/invoice-link/src/components/pay-aside.tsx @@ -41,7 +41,8 @@ function PayPanel({ Settle in seconds with USDC.

    - Amount, recipient, and chain are pinned to this invoice. + Amount, recipient, and chain are pinned to this invoice. Bridge fees + are added to your total, so Studio Hibiscus receives the full amount.

    diff --git a/examples/invoice-link/src/data/demo-invoices.ts b/examples/invoice-link/src/data/demo-invoices.ts index 96860a8..cfbfd09 100644 --- a/examples/invoice-link/src/data/demo-invoices.ts +++ b/examples/invoice-link/src/data/demo-invoices.ts @@ -19,7 +19,7 @@ export const DEMO_INVOICES: DemoInvoice[] = [ label: "Photography · half-day shoot", memo: "Half-day editorial shoot", amount: "850", - chain: "Arc_Testnet", + chain: "Optimism_Sepolia", to: "0x5B8ecaB7096F8aBED873D246629ef9f05f467605", }, ]; diff --git a/examples/payroll-batch/README.md b/examples/payroll-batch/README.md index bac22c4..5804433 100644 --- a/examples/payroll-batch/README.md +++ b/examples/payroll-batch/README.md @@ -18,6 +18,8 @@ one-by-one through a single embedded widget. - `onSuccess` advances to the next payee automatically — no extra clicks between dispatches. - Editorial typography + serif display headers (Studio Fortune brand). +- Runs `feeBearer: "sender"` so the studio treasury covers the bridge + fees and each contractor is paid their exact salary. ## Stack diff --git a/examples/payroll-batch/package.json b/examples/payroll-batch/package.json index 2c614b6..1e86bed 100644 --- a/examples/payroll-batch/package.json +++ b/examples/payroll-batch/package.json @@ -17,6 +17,7 @@ "@tanstack/react-query": "^5.59.0", "@usewhisk/core": "workspace:*", "@usewhisk/react": "workspace:*", + "buffer": "^6.0.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/payroll-batch/src/components/paying-step.tsx b/examples/payroll-batch/src/components/paying-step.tsx index 65de578..431cb76 100644 --- a/examples/payroll-batch/src/components/paying-step.tsx +++ b/examples/payroll-batch/src/components/paying-step.tsx @@ -247,7 +247,7 @@ function ActivePayeeCard({
    - Locked + Locked · fees on the studio

    ${payee.amount.toLocaleString()} USDC →{" "} @@ -259,6 +259,7 @@ function ActivePayeeCard({ onPaid()} /> diff --git a/examples/payroll-batch/src/data/payees.ts b/examples/payroll-batch/src/data/payees.ts index c163043..a8d3c07 100644 --- a/examples/payroll-batch/src/data/payees.ts +++ b/examples/payroll-batch/src/data/payees.ts @@ -1,10 +1,15 @@ +import type { Chain } from "@usewhisk/react"; + export type Payee = { id: string; name: string; role: string; address: string; amount: number; + /** Display label for the chain. */ chain: string; + /** Typed Whisk chain identifier for ``. */ + chainCode: Chain; status: "pending" | "approved" | "sent" | "settled"; initials: string; hue: number; @@ -20,6 +25,7 @@ export const PAYEES: Payee[] = [ address: "0x5B8e…f7605", amount: 6400, chain: "Arc Testnet", + chainCode: "Arc_Testnet", status: "pending", initials: "MC", hue: 340, @@ -31,6 +37,7 @@ export const PAYEES: Payee[] = [ address: "0x9b21…ed12", amount: 4800, chain: "Base Sepolia", + chainCode: "Base_Sepolia", status: "pending", initials: "JH", hue: 12, @@ -42,6 +49,7 @@ export const PAYEES: Payee[] = [ address: "0x3f41…b9aa", amount: 5600, chain: "Arc Testnet", + chainCode: "Arc_Testnet", status: "pending", initials: "SA", hue: 280, @@ -53,6 +61,7 @@ export const PAYEES: Payee[] = [ address: "0x7a92…1cde", amount: 4200, chain: "OP Sepolia", + chainCode: "Optimism_Sepolia", status: "pending", initials: "YT", hue: 200, @@ -63,7 +72,8 @@ export const PAYEES: Payee[] = [ role: "Motion Designer", address: "0x2b11…cd54", amount: 3800, - chain: "Arc Testnet", + chain: "Arbitrum Sepolia", + chainCode: "Arbitrum_Sepolia", status: "pending", initials: "RM", hue: 50, @@ -75,6 +85,7 @@ export const PAYEES: Payee[] = [ address: "0xe4f0…77b1", amount: 5100, chain: "Base Sepolia", + chainCode: "Base_Sepolia", status: "pending", initials: "OP", hue: 320, diff --git a/examples/payroll-batch/src/main.tsx b/examples/payroll-batch/src/main.tsx index 401a69c..db2ae4c 100644 --- a/examples/payroll-batch/src/main.tsx +++ b/examples/payroll-batch/src/main.tsx @@ -1,3 +1,14 @@ +// Polyfill Buffer for the browser before any wallet code loads. +// Some wallet SDKs (and any Solana chain you add later) reach for Node's +// `Buffer` global, which Vite does not provide. Next.js polyfills it +// automatically; in a Vite app this one-liner prevents "Buffer is not +// defined". This recipe is EVM-only today, so it's defensive — but it +// costs nothing and removes the footgun if a Solana chain is added. +import { Buffer } from "buffer"; +if (typeof globalThis.Buffer === "undefined") { + globalThis.Buffer = Buffer; +} + import React from "react"; import ReactDOM from "react-dom/client"; import "@usewhisk/react/styles.css"; diff --git a/examples/payroll-batch/src/providers.tsx b/examples/payroll-batch/src/providers.tsx index 6bdb7ed..72ab62a 100644 --- a/examples/payroll-batch/src/providers.tsx +++ b/examples/payroll-batch/src/providers.tsx @@ -6,20 +6,39 @@ export function Providers({ children }: { children: ReactNode }) { () => createWhiskConfig({ mode: "testnet", + // Studio treasury covers the bridge fees so each contractor is paid + // their exact salary, net of nothing. + feeBearer: "sender", wallets: [ evm({ - chains: ["Arc_Testnet"], + chains: [ + "Arbitrum_Sepolia", + "Arc_Testnet", + "Base_Sepolia", + "Optimism_Sepolia", + ], projectId: import.meta.env.VITE_WALLETCONNECT_PROJECT_ID, appName: "Studio Fortune", }), ], - chains: ["Arc_Testnet"], - defaultSourceChain: "Arc_Testnet", - defaultDestinationChain: "Arc_Testnet", + chains: [ + "Arbitrum_Sepolia", + "Arc_Testnet", + "Base_Sepolia", + "Optimism_Sepolia", + ], + // Treasury sits on Arbitrum for cheap outgoing payouts; each + // dispatch then bridges to the contractor's preferred chain. + defaultSourceChain: "Arbitrum_Sepolia", + defaultDestinationChain: "Arbitrum_Sepolia", appLabel: "whisk-example-payroll", }), [], ); - return {children}; + return ( + + {children} + + ); } diff --git a/examples/payroll-batch/src/styles.css b/examples/payroll-batch/src/styles.css index 42a4b96..058a11f 100644 --- a/examples/payroll-batch/src/styles.css +++ b/examples/payroll-batch/src/styles.css @@ -57,16 +57,38 @@ body { min-height: 100dvh; } -.sf-widget [data-whisk] { +/* Widget theme — set on the provider's `[data-whisk]` so the cascade + reaches portals. Loads after @usewhisk/react/styles.css → wins at + equal specificity. */ +[data-whisk] { + /* surfaces */ --whisk-bg: var(--color-ivory); --whisk-card: var(--color-bone); + --whisk-card-fg: var(--color-ink); --whisk-fg: var(--color-ink); --whisk-fg-muted: var(--color-ink-muted); + /* lines */ --whisk-border: var(--color-line); --whisk-input: var(--color-line); + --whisk-border-w: 1px; + /* accent */ --whisk-primary: var(--color-claret); --whisk-primary-fg: var(--color-ivory); + --whisk-ring: var(--color-burgundy); + /* semantic */ + --whisk-success: var(--color-emerald); + --whisk-warning: var(--color-gold); + --whisk-destructive: var(--color-claret-deep); + --whisk-destructive-fg: var(--color-ivory); + /* radii */ --whisk-radius: 10px; + --whisk-radius-sm: 6px; + --whisk-radius-md: 10px; + --whisk-radius-lg: 14px; + /* fonts — editorial serif for headlines */ + --whisk-font: var(--font-sans); + --whisk-font-display: var(--font-display); + --whisk-font-mono: var(--font-mono); } @keyframes sf-step { diff --git a/examples/playground/src/app/playground/controls.tsx b/examples/playground/src/app/playground/controls.tsx index 4662eed..b1adda2 100644 --- a/examples/playground/src/app/playground/controls.tsx +++ b/examples/playground/src/app/playground/controls.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState, type Dispatch } from "react"; -import { chainInfo, type Chain } from "@usewhisk/react"; +import { chainInfo, type Chain, type FeeBearer } from "@usewhisk/react"; import { PLAYGROUND_CHAINS } from "./providers"; import { ADDRESS_BOOK } from "./address-book"; import type { @@ -56,6 +56,18 @@ export function Controls({ /> +

    + set({ feeBearer })} + /> + + {config.feeBearer === "sender" + ? "Burn is grossed up so the recipient nets the full amount. Switching reconnects the widget." + : "Fees come out of the transfer; recipient nets amount − fees. Switching reconnects the widget."} + +
    +
    void; +}) { + const options: FeeBearer[] = ["receiver", "sender"]; + return ( +
    + {options.map((opt) => ( + + ))} +
    + ); +} + const PALETTE_SWATCHES: Array<{ id: Palette; label: string; color: string }> = [ { id: "wine", label: "Wine", color: "#d65c3c" }, { id: "indigo", label: "Indigo", color: "#6366f1" }, diff --git a/examples/playground/src/app/playground/index.tsx b/examples/playground/src/app/playground/index.tsx index 7067bf9..41a2e75 100644 --- a/examples/playground/src/app/playground/index.tsx +++ b/examples/playground/src/app/playground/index.tsx @@ -39,7 +39,10 @@ export function Playground() { }, [palette]); return ( - +
    diff --git a/examples/playground/src/app/playground/providers.tsx b/examples/playground/src/app/playground/providers.tsx index 6e5fa0d..1f6425c 100644 --- a/examples/playground/src/app/playground/providers.tsx +++ b/examples/playground/src/app/playground/providers.tsx @@ -7,6 +7,7 @@ import { evm, solana, type Chain, + type FeeBearer, } from "@usewhisk/react"; /** @@ -50,21 +51,26 @@ export const PLAYGROUND_CHAINS = TESTNET_CHAINS; * or react-query underneath. Wallet connection survives a theme * change. * - * `config` is memoized with empty deps because it's static for the - * lifetime of the playground — every adjustable knob lives on - * `` props or the provider's `theme` prop, not on - * `createWhiskConfig`. + * `config` is memoized on `feeBearer` — almost every knob lives on + * `` props or the reactive `theme` prop, but `feeBearer` is + * part of `createWhiskConfig`, which the engine reads once at creation. + * The `key={feeBearer}` on the provider forces a clean remount when it + * flips, so a fresh engine picks up the new fee policy. (Toggling it + * therefore drops the wallet connection — fine for a QA knob.) */ export function PlaygroundProviders({ theme, + feeBearer, children, }: { theme: "system" | "light" | "dark"; + feeBearer: FeeBearer; children: React.ReactNode; }) { const config = useMemo( () => createWhiskConfig({ + feeBearer, wallets: [ evm({ chains: TESTNET_CHAINS, @@ -95,11 +101,11 @@ export function PlaygroundProviders({ defaultDestinationChain: "Base_Sepolia", appLabel: "whisk-playground", }), - [], + [feeBearer], ); return ( - + {children} ); diff --git a/examples/playground/src/app/playground/store.ts b/examples/playground/src/app/playground/store.ts index ea3275a..4f4f883 100644 --- a/examples/playground/src/app/playground/store.ts +++ b/examples/playground/src/app/playground/store.ts @@ -1,7 +1,7 @@ "use client"; import { useReducer } from "react"; -import type { Chain } from "@usewhisk/react"; +import type { Chain, FeeBearer } from "@usewhisk/react"; export type Theme = "system" | "light" | "dark"; @@ -14,6 +14,10 @@ export type PlaygroundConfig = { theme: Theme; palette: Palette; + /* Engine-level — part of createWhiskConfig. Changing it remounts the + * provider (the engine is built once per provider lifetime). */ + feeBearer: FeeBearer; + /* Surface toggles */ showFooter: boolean; swapEnabled: boolean; @@ -55,6 +59,7 @@ export type PlaygroundAction = export const INITIAL_CONFIG: PlaygroundConfig = { theme: "system", palette: "wine", + feeBearer: "receiver", showFooter: true, swapEnabled: true, lockAmount: false, diff --git a/examples/themed-saas/README.md b/examples/themed-saas/README.md index 17456cf..d466276 100644 --- a/examples/themed-saas/README.md +++ b/examples/themed-saas/README.md @@ -19,6 +19,8 @@ a serious enterprise product. explorer. - Idle state when no vendor is selected, prompting the user to pick a row. +- Runs `feeBearer: "sender"` so the treasury covers the bridge fees and + each vendor receives their exact invoiced amount. ## Stack diff --git a/examples/themed-saas/src/app/globals.css b/examples/themed-saas/src/app/globals.css index 8341f04..5496c7b 100644 --- a/examples/themed-saas/src/app/globals.css +++ b/examples/themed-saas/src/app/globals.css @@ -56,16 +56,38 @@ body { } /* Steelpath widget theme: dark card, foam-teal primary. */ -.sp-widget [data-whisk] { +/* Widget theme — set on the provider's `[data-whisk]` so the cascade + reaches portals. Loads after @usewhisk/react/styles.css → wins at + equal specificity. */ +[data-whisk] { + /* surfaces */ --whisk-bg: var(--color-card); --whisk-card: var(--color-card-2); + --whisk-card-fg: var(--color-text); --whisk-fg: var(--color-text); --whisk-fg-muted: var(--color-text-muted); + /* lines */ --whisk-border: var(--color-line-strong); --whisk-input: var(--color-line-strong); + --whisk-border-w: 1px; + /* accent */ --whisk-primary: var(--color-foam); --whisk-primary-fg: var(--color-ink); + --whisk-ring: var(--color-foam); + /* semantic */ + --whisk-success: var(--color-pos); + --whisk-warning: var(--color-warn); + --whisk-destructive: var(--color-neg); + --whisk-destructive-fg: var(--color-ink); + /* radii */ --whisk-radius: 10px; + --whisk-radius-sm: 6px; + --whisk-radius-md: 10px; + --whisk-radius-lg: 14px; + /* fonts */ + --whisk-font: var(--font-sans); + --whisk-font-display: var(--font-display); + --whisk-font-mono: var(--font-mono); } @keyframes sp-pulse { diff --git a/examples/themed-saas/src/app/page.tsx b/examples/themed-saas/src/app/page.tsx index a83f0c6..9b9d663 100644 --- a/examples/themed-saas/src/app/page.tsx +++ b/examples/themed-saas/src/app/page.tsx @@ -1,9 +1,5 @@ import { ClientGate } from "./client-gate"; export default function Page() { - return ( -
    - -
    - ); + return ; } diff --git a/examples/themed-saas/src/app/providers.tsx b/examples/themed-saas/src/app/providers.tsx index 7bdc550..01dfd2a 100644 --- a/examples/themed-saas/src/app/providers.tsx +++ b/examples/themed-saas/src/app/providers.tsx @@ -8,6 +8,9 @@ export function Providers({ children }: { children: React.ReactNode }) { () => createWhiskConfig({ mode: "testnet", + // Treasury covers the bridge fees so each vendor receives their exact + // invoiced amount. + feeBearer: "sender", wallets: [ evm({ chains: ["Arc_Testnet", "Base_Sepolia"], @@ -23,5 +26,9 @@ export function Providers({ children }: { children: React.ReactNode }) { [], ); - return {children}; + return ( + + {children} + + ); } diff --git a/examples/themed-saas/src/components/quick-send.tsx b/examples/themed-saas/src/components/quick-send.tsx index 3d8f100..8e38c67 100644 --- a/examples/themed-saas/src/components/quick-send.tsx +++ b/examples/themed-saas/src/components/quick-send.tsx @@ -61,7 +61,8 @@ function SelectedVendor({ {vendor.handle} · {vendor.chain} - Amount + recipient locked from vendor profile + Locked from vendor profile · fees covered, vendor nets the full + amount