diff --git a/.agents/skills/oneshot-embedded-wallet/SKILL.md b/.agents/skills/oneshot-embedded-wallet/SKILL.md index f308d31..a64796e 100644 --- a/.agents/skills/oneshot-embedded-wallet/SKILL.md +++ b/.agents/skills/oneshot-embedded-wallet/SKILL.md @@ -3,7 +3,7 @@ name: oneshot-embedded-wallet description: >- Integrate the 1Shot embedded wallet (OWS Host Layer) with @1shotapi/ows-provider. Use when embedding wallet.1shotapi.com, wiring OWSProxy, EIP-1193, credentials, - or custom RPC such as setStyle / focusWallet / addAsset / createAccount for + or custom RPC such as configure / focusWallet / addAsset / createAccount for theming, host-driven focus mode, tracked assets, and first-party Safari create. license: MIT metadata: @@ -43,7 +43,7 @@ const container = document.getElementById("wallet-container")!; const proxy = await OWSProxy.create(container, WALLET_URL); // Optional: theme / copy before showing the flyout -await proxy.rpc("setStyle", { +await proxy.rpc("configure", { copy: { productName: "Acme Wallet", tagline: "Powered by 1Shot" }, theme: { primary: "oklch(0.45 0.18 250)" }, }); @@ -61,12 +61,12 @@ const accounts = await proxy.ethereum.request({ method: "eth_requestAccounts" }) - Production wallet URL: **`https://wallet.1shotapi.com/`** (Signing Layer at `/signer/` on the same origin — do not embed `/signer/` from the host). - Safari / iOS WebKit cannot create passkeys inside a **cross-origin** wallet iframe. The branding layer detects this and opens **`https://wallet.1shotapi.com/create/`** in a new tab (same origin as the wallet). Allow pop-ups from the embedding page so that handoff can complete; no host code changes are required. -## Custom RPC — `setStyle` +## Custom RPC — `configure` 1Shot-specific method registered on the Branding Layer. Call via: ```typescript -await proxy.rpc("setStyle", options); +await proxy.rpc("configure", options); ``` `options` is a partial merge (safe to call repeatedly): @@ -84,6 +84,7 @@ await proxy.rpc("setStyle", options); | `features.disableCredentials` | boolean | Hide Credentials tab; default `false`. Host credential flows still work | | `features.disableDelegations` | boolean | Hide Delegations tab; default `false`. Host delegation flows still work | | `features.allowedChains` | `string[]` (hex `0x…` chain ids) | Restrict Network dropdown to these catalog chains; omit or `[]` ⇒ all enabled | +| `destinationUrl` | string \| null | URL to receive transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/relayer/get-started/overview) (≤256 chars). `null` or `""` clears | | `copy.productName` | string | titles / chrome | | `copy.tagline` | string | supporting line | | `copy.connect.title` | string | connect modal title | @@ -258,7 +259,7 @@ await proxy.rpc("createAccount", { accountName: "My Wallet" }); | `proxy.credentials.*` | OID4 offer / present (when enabled in wallet) | | `proxy.analytics.on(listener)` / `.on(name, listener)` / `.off(listener)` | Branding→Host product analytics (`ows:analytics`) | | `proxy.showWallet()` / `hideWallet()` | Host-driven flyout without an EIP-1193 call | -| `proxy.rpc(method, params)` | Custom Branding RPC (`setStyle`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, …) | +| `proxy.rpc(method, params)` | Custom Branding RPC (`configure`, `focusWallet`, `unfocusWallet`, `addAsset`, `createAccount`, …) | Subscribe so in-wallet chain/account changes update host UI without polling: @@ -304,6 +305,6 @@ include a live Analytics panel fed by `proxy.analytics.on` (filter by `name`). - Never embed the Signing Layer iframe from the Host — always Host → Branding → Signing. - Prefer the published wallet URL in production; point at a local Branding origin only while developing this repo. -- Theme with `setStyle`; do not ask integrators to fork CSS for basic brand colors / product name. +- Theme with `configure`; do not ask integrators to fork CSS for basic brand colors / product name. - Use `focusWallet` / `unfocusWallet` for host-driven single-asset flows; do not expose mode switching in the wallet UI. - Use `addAsset` when the host wants a lasting Balances entry; expect a confirm modal (contrast with `focusWallet`). diff --git a/.github/workflows/Deploy Dev.yaml b/.github/workflows/Deploy Dev.yaml index 2c1b903..c227225 100644 --- a/.github/workflows/Deploy Dev.yaml +++ b/.github/workflows/Deploy Dev.yaml @@ -119,14 +119,14 @@ jobs: echo "---------------------------- Docker Deployment ---------------------------------------" # Pull the latest images - docker compose pull + docker compose --env-file secrets.env pull # Stop necessary services docker stop wallet || true docker rm -f wallet || true # Reup everything - docker compose up -d + docker compose --env-file secrets.env up -d # Prune dead resources docker system prune -f diff --git a/AGENTS.md b/AGENTS.md index 0027f98..71873a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Prefer **clean code over backwards compatibility**. Do not add legacy redirects, | `src/lib/implementations/{business,data,utils}/` | Layer implementations | | `src/assets/` | Static media only (SVGs, images) | -Test Host Layer: `host/` (`npm run dev:host`). Browser extension: `extension/` (`npm run dev:extension`) — see [`extension/README.md`](extension/README.md). Style via Host RPC `setStyle`, not in-wallet debug knobs. +Test Host Layer: `host/` (`npm run dev:host`). Browser extension: `extension/` (`npm run dev:extension`) — see [`extension/README.md`](extension/README.md). Branding / host config via Host RPC `configure`, not in-wallet debug knobs. ### Form validation UX @@ -35,13 +35,13 @@ Signing Layer WebAuthn (PRF via `OWSSigner`) shows Confirm/Cancel **inside the s Branding `PasskeyPromptModal` + `withPasskeyPrompt` remain **only** for Branding-native WebAuthn that never enters Signing (e.g. Relayer `createRelayerAssertion` in `webauthnAuth.ts`). -### User-facing copy (`setStyle`) +### Host configuration (`configure`) -When adding or changing UI strings: +When adding or changing UI strings or host-tunable options: -1. Wire them through `style.copy` (defaults, types, and `registerSetStyle` Zod schema) so hosts can override via `setStyle`. +1. Wire them through `style` / `IStyleOptions` (defaults, types, and `registerConfigure` Zod schema) so hosts can override via `configure`. 2. Expose the same keys in **both** WalletConfigurator playgrounds: `host/` in this repo and `app/playground/` in **1Shot-API-Website-New** (`styleForm.ts` + `WalletConfigurator.tsx`). - +3. Optional top-level `destinationUrl` is a URL for transaction status update webhooks from the [1Shot Relayer](https://1shotapi.com/docs/relayer/get-started/overview). ### Domain layers (assets example) - **Utils:** `IBlockchainProvider` / `AddressUtils` (from `@1shotapi/ows-wallet-utils`) / `SupportedChainsBlockchainProvider`, `IEventBus` / `EventBus`, `ITransactionUtils` / `TransactionUtils`, `IConfigProvider` / `ConfigProvider`, `IChainRepository` / `HardcodedChainRepository` diff --git a/README.md b/README.md index 1c40162..a465c0a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ cp .env.example .env # set NGROK_AUTHTOKEN (and optional NGROK_DOMAIN) ```bash npm run dev # Branding Layer + ngrok HTTPS tunnel npm run dev:local # Branding Layer, local HTTP only -npm run dev:host # Test Host Layer (setStyle knobs + EIP-1193) +npm run dev:host # Test Host Layer (configure knobs + EIP-1193) npm run dev:extension # Browser extension (side panel + EIP-1193 shim) ``` @@ -52,7 +52,7 @@ Passkeys need HTTPS — use the printed ngrok wallet URL as the host iframe sour By default Vite uses published `@1shotapi/ows-*` from `node_modules`. To point at a sibling `../prf-wallet` checkout, set `OWS_LOCAL_PACKAGES=1` (Firefox often breaks on the resulting `/@fs/C:` module URLs — prefer Chrome, or leave the flag unset for ngrok). -Style testing: use the **Style (setStyle RPC)** panel on the test host (`host/`), not in-wallet debug UI. See [host/README.md](host/README.md). +Style testing: use the **Style (configure RPC)** panel on the test host (`host/`), not in-wallet debug UI. See [host/README.md](host/README.md). ## Host integration @@ -67,7 +67,7 @@ import { OWSProxy } from "@1shotapi/ows-provider"; const proxy = await OWSProxy.create(container, "https://wallet.1shotapi.com/"); -await proxy.rpc("setStyle", { +await proxy.rpc("configure", { copy: { productName: "Acme Wallet", tagline: "Powered by 1Shot" }, theme: { primary: "oklch(0.45 0.18 250)" }, }); @@ -102,9 +102,16 @@ The same rich payload is also POSTed fire-and-forget to the 1Shot relayer Analytics panel fed by `proxy.analytics.on` — filter by `name` to inspect outcomes while testing. -### `setStyle` (custom RPC) +### `configure` (custom RPC) -Additive merge of theme CSS variables + copy. Safe to call repeatedly. Schema is Zod-strict (unknown keys rejected). Full field list: [skills/oneshot-embedded-wallet/SKILL.md](skills/oneshot-embedded-wallet/SKILL.md). +Additive merge of theme CSS variables, copy, feature flags, and optional +`destinationUrl` (status webhooks from the +[1Shot Relayer](https://1shotapi.com/docs/relayer/get-started/overview)). +Safe to call repeatedly. Schema is Zod-strict (unknown keys rejected). Full field +list: [skills/oneshot-embedded-wallet/SKILL.md](skills/oneshot-embedded-wallet/SKILL.md). + +When `destinationUrl` is set, the wallet asks the 1Shot Relayer to send +transaction status update webhooks to that URL. ### Agent skill @@ -150,4 +157,4 @@ See [roadmap.md](roadmap.md) (ShadCN + customization phases). ## Status -Functional Branding Layer (passkey unlock, EIP-1193, signing consent, recovery, credentials) with Phase 0–1 style foundations (`setStyle` + StyleProvider + shell). ShadCN UI migration in progress. +Functional Branding Layer (passkey unlock, EIP-1193, signing consent, recovery, credentials) with Phase 0–1 style foundations (`configure` + StyleProvider + shell). ShadCN UI migration in progress. diff --git a/extension/entrypoints/sidepanel/main.ts b/extension/entrypoints/sidepanel/main.ts index b9b3c57..ff65624 100644 --- a/extension/entrypoints/sidepanel/main.ts +++ b/extension/entrypoints/sidepanel/main.ts @@ -91,12 +91,12 @@ async function ensureProxy(): Promise { }); try { - await created.rpc("setStyle", { + await created.rpc("configure", { copy: { productName: "1Shot Wallet" }, features: { hideCloseBox: true }, }); } catch (error) { - console.warn("[1Shot sidepanel] setStyle failed", error); + console.warn("[1Shot sidepanel] configure failed", error); } created.ethereum.on("accountsChanged", (...params: unknown[]) => { diff --git a/host/README.md b/host/README.md index 1f1be8c..3fc1068 100644 --- a/host/README.md +++ b/host/README.md @@ -2,7 +2,7 @@ Local Host Layer for exercising Host ↔ Branding communication against this repo’s wallet (served at domain root). -Adapted from OWS [`examples/host`](https://github.com/1Shot-API/open-wallet/tree/main/examples/host), plus a **Style** panel that calls `proxy.rpc("setStyle", options)`. +Adapted from OWS [`examples/host`](https://github.com/1Shot-API/open-wallet/tree/main/examples/host), plus a **Style** panel that calls `proxy.rpc("configure", options)`. ## Run diff --git a/host/package.json b/host/package.json index ed51011..8bb5185 100644 --- a/host/package.json +++ b/host/package.json @@ -2,7 +2,7 @@ "name": "@1shotapi/oneshot-wallet-host", "version": "0.0.0", "private": true, - "description": "Local Host Layer for testing 1Shot Wallet Branding (setStyle + EIP-1193)", + "description": "Local Host Layer for testing 1Shot Wallet Branding (configure + EIP-1193)", "type": "module", "scripts": { "dev": "node scripts/dev.mjs", diff --git a/host/src/App.tsx b/host/src/App.tsx index fe5e4fd..a4a75c6 100644 --- a/host/src/App.tsx +++ b/host/src/App.tsx @@ -22,7 +22,7 @@ const MAX_ANALYTICS_EVENTS = 50; export function App() { const flyoutContainerRef = useRef(null); const proxyRef = useRef(null); - /** Last setStyle payload from Design mode — re-applied after Test recreate. */ + /** Last configure payload from Design mode — re-applied after Test recreate. */ const lastStyleRef = useRef | null>(null); const [previewMount, setPreviewMount] = useState(null); const [mode, setMode] = useState("test"); @@ -107,7 +107,7 @@ export function App() { }); if (lastStyleRef.current) { - await proxy.rpc("setStyle", lastStyleRef.current); + await proxy.rpc("configure", lastStyleRef.current); } if (cancelled) { @@ -121,7 +121,7 @@ export function App() { const connectedChain = await refreshChainFromWallet(proxy); reportStatus( mode === "design" - ? `Design preview connected on ${connectedChain}. Apply setStyle to refresh.` + ? `Design preview connected on ${connectedChain}. Apply configure to refresh.` : mode === "analytics" ? "Live analytics — switch to Test to generate events." : `Wallet connected on ${connectedChain}. Enter a message and click Sign.`, diff --git a/host/src/components/AppHeader.tsx b/host/src/components/AppHeader.tsx index e45b5e9..5336f9d 100644 --- a/host/src/components/AppHeader.tsx +++ b/host/src/components/AppHeader.tsx @@ -27,7 +27,7 @@ export function AppHeader() {

Host Layer → Branding Layer ·{" "} - setStyle + EIP-1193 + configure + EIP-1193

diff --git a/host/src/components/AppSidebar.tsx b/host/src/components/AppSidebar.tsx index c5aefce..b4446e0 100644 --- a/host/src/components/AppSidebar.tsx +++ b/host/src/components/AppSidebar.tsx @@ -39,7 +39,7 @@ const NAV_ITEMS: { { mode: "design", label: "Design", - description: "Live setStyle playground", + description: "Live configure playground", icon: PaletteIcon, }, { diff --git a/host/src/components/DesignPanel.tsx b/host/src/components/DesignPanel.tsx index c95101b..5462b6d 100644 --- a/host/src/components/DesignPanel.tsx +++ b/host/src/components/DesignPanel.tsx @@ -44,7 +44,7 @@ export function DesignPanel({

Fresh inline proxy in this slot. Styles are re-applied via{" "} - setStyle when you return to Test. + configure when you return to Test.

diff --git a/host/src/components/WalletConfigurator.tsx b/host/src/components/WalletConfigurator.tsx index 9df68a0..ee7a06a 100644 --- a/host/src/components/WalletConfigurator.tsx +++ b/host/src/components/WalletConfigurator.tsx @@ -11,7 +11,7 @@ import { CATALOG_CHAIN_OPTIONS, DEFAULTS_PRESET, OCEAN_PRESET, - buildSetStylePayload, + buildConfigurePayload, type IStyleFormState, } from "../styleForm"; @@ -22,7 +22,7 @@ export interface IWalletConfiguratorProps { } /** - * Host-side style playground for `proxy.rpc("setStyle", …)`. + * Host-side style playground for `proxy.rpc("configure", …)`. * Tabs: Basic (identity), Style (theme colors), Text (modal copy). */ export function WalletConfigurator({ @@ -46,13 +46,13 @@ export function WalletConfigurator({ if (next) setForm(next); setBusy(true); setIsError(false); - setStatus("Calling setStyle…"); + setStatus("Calling configure…"); try { - await onApply(buildSetStylePayload(payloadForm)); - setStatus("setStyle applied."); + await onApply(buildConfigurePayload(payloadForm)); + setStatus("Configuration applied."); } catch (error) { setIsError(true); - setStatus(error instanceof Error ? error.message : "setStyle failed"); + setStatus(error instanceof Error ? error.message : "Configuration failed"); } finally { setBusy(false); } @@ -87,6 +87,25 @@ export function WalletConfigurator({ value={form.tagline} onChange={(value) => patch("tagline", value)} /> + patch("destinationUrl", value)} + /> +

+ URL to receive transaction status update webhooks from the{" "} + + 1Shot Relayer + + . Leave empty to clear. +

Features
@@ -151,7 +170,7 @@ export function WalletConfigurator({ Leave all unchecked to allow every catalog network. Checking any restricts the wallet Network dropdown via{" "} - setStyle.features.allowedChains + configure.features.allowedChains .

@@ -281,7 +300,7 @@ export function WalletConfigurator({ void apply(); }} > - Apply setStyle + Apply configuration + ); +} diff --git a/src/components/OnboardingPanel.tsx b/src/components/OnboardingPanel.tsx index 1100e3c..b628ab0 100644 --- a/src/components/OnboardingPanel.tsx +++ b/src/components/OnboardingPanel.tsx @@ -5,6 +5,7 @@ import { useWalletSessionStore } from "../wallet/sessionStore"; import { formatWalletSetupError } from "../wallet/formatWalletSetupError"; import { useStyle } from "../style/StyleProvider"; import { BrandLogo } from "./BrandLogo"; +import { CloseWalletButton } from "./CloseWalletButton"; function taglineLines(tagline: string): string[] { return tagline @@ -65,6 +66,10 @@ export function OnboardingPanel() { return (
+
+ +
+
- {!style.features.hideCloseBox ? ( - - ) : null} +
); diff --git a/src/lib/implementations/business/utils/TransactionUtils.ts b/src/lib/implementations/business/utils/TransactionUtils.ts index 11240ed..8fd4ce8 100644 --- a/src/lib/implementations/business/utils/TransactionUtils.ts +++ b/src/lib/implementations/business/utils/TransactionUtils.ts @@ -371,6 +371,7 @@ export class TransactionUtils implements ITransactionUtils { args: [capabilities.feeCollector, feeAmount], }), ); + const destinationUrl = styleController.get().destinationUrl; return { chainId: chainIdNumber.toString(10), transactions: [ @@ -404,6 +405,7 @@ export class TransactionUtils implements ITransactionUtils { this.options.presentationTransactionUtils.resolveHostDomain(), ), delegationSecret, + ...(destinationUrl ? { destinationUrl } : {}), }; }; diff --git a/src/lib/implementations/data/HardcodedChainRepository.ts b/src/lib/implementations/data/HardcodedChainRepository.ts index c4641f4..e428620 100644 --- a/src/lib/implementations/data/HardcodedChainRepository.ts +++ b/src/lib/implementations/data/HardcodedChainRepository.ts @@ -193,7 +193,7 @@ const CATALOG: readonly SupportedChain[] = [ true, robinhoodLogo, true, - "https://rpc.mainnet.chain.robinhood.com", + `https://robinhood-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, "Robinhood", "https://robinhoodchain.blockscout.com", ), @@ -243,7 +243,7 @@ export class HardcodedChainRepository implements IChainRepository { }; } - /** Full catalog (including disabled), for configurators / setStyle validation. */ + /** Full catalog (including disabled), for configurators / configure validation. */ getCatalog(): readonly SupportedChain[] { return CATALOG; } diff --git a/src/lib/interfaces/data/IChainRepository.ts b/src/lib/interfaces/data/IChainRepository.ts index e50f4fd..f87d8dc 100644 --- a/src/lib/interfaces/data/IChainRepository.ts +++ b/src/lib/interfaces/data/IChainRepository.ts @@ -16,7 +16,7 @@ export interface IChainRepository { /** Subscribe to allowlist (and thus `list()`) changes. Returns unsubscribe. */ onAllowedChainsChanged(handler: () => void): () => void; - /** Full catalog (not filtered by allowlist), for RPC / setStyle validation. */ + /** Full catalog (not filtered by allowlist), for RPC / configure validation. */ getCatalog(): readonly SupportedChain[]; /** diff --git a/src/lib/interfaces/data/IOneshotRelayerRepository.ts b/src/lib/interfaces/data/IOneshotRelayerRepository.ts index f88dcc8..282d0e2 100644 --- a/src/lib/interfaces/data/IOneshotRelayerRepository.ts +++ b/src/lib/interfaces/data/IOneshotRelayerRepository.ts @@ -63,6 +63,8 @@ export interface IRelayer7710Params { context?: string; memo?: string; delegationSecret?: string; + /** URL for transaction status update webhooks from the 1Shot Relayer (≤256 chars). */ + destinationUrl?: string; } export interface IRelayerEstimateResult { diff --git a/src/style/applyStyle.ts b/src/style/applyStyle.ts index 4dfbb72..8d1323e 100644 --- a/src/style/applyStyle.ts +++ b/src/style/applyStyle.ts @@ -162,9 +162,22 @@ export function mergeStyle( ? null : [...patch.features.allowedChains], }, + destinationUrl: + patch.destinationUrl === undefined + ? current.destinationUrl + : normalizeDestinationUrl(patch.destinationUrl), }; } +/** Empty / null clears; otherwise trim. Schema validates URL shape on RPC. */ +function normalizeDestinationUrl(value: string | null): string | null { + if (value == null) { + return null; + } + const trimmed = value.trim(); + return trimmed.length === 0 ? null : trimmed; +} + function cloneDefaultStyle(): IResolvedStyle { return { ...DEFAULT_STYLE, @@ -217,6 +230,7 @@ function cloneDefaultStyle(): IResolvedStyle { disableDelegations: DEFAULT_STYLE.features.disableDelegations, allowedChains: null, }, + destinationUrl: null, }; } diff --git a/src/style/defaults.ts b/src/style/defaults.ts index 9769f42..e4b29db 100644 --- a/src/style/defaults.ts +++ b/src/style/defaults.ts @@ -21,6 +21,7 @@ export const DEFAULT_STYLE: IResolvedStyle = { disableDelegations: false, allowedChains: null, }, + destinationUrl: null, copy: { productName: "1Shot Wallet", tagline: diff --git a/src/style/index.ts b/src/style/index.ts index 4f8636f..ae9194e 100644 --- a/src/style/index.ts +++ b/src/style/index.ts @@ -31,8 +31,8 @@ export { DEFAULT_STYLE } from "./defaults"; export { StyleProvider, useStyle } from "./StyleProvider"; export { styleController } from "./styleController"; export { - registerSetStyleRpc, - SET_STYLE_RPC_METHOD, - setStyleParamsSchema, - type ISetStyleParams, -} from "./registerSetStyle"; + registerConfigureRpc, + CONFIGURE_RPC_METHOD, + configureParamsSchema, + type IConfigureParams, +} from "./registerConfigure"; diff --git a/src/style/registerSetStyle.ts b/src/style/registerConfigure.ts similarity index 93% rename from src/style/registerSetStyle.ts rename to src/style/registerConfigure.ts index e07bd60..7175874 100644 --- a/src/style/registerSetStyle.ts +++ b/src/style/registerConfigure.ts @@ -4,8 +4,8 @@ import type { OWSWallet } from "@1shotapi/ows-wallet-utils"; import type { IChainRepository } from "../lib/interfaces/data/IChainRepository"; import { styleController } from "./styleController"; -/** Custom RPC method name — host: `await proxy.rpc("setStyle", options)`. */ -export const SET_STYLE_RPC_METHOD = "setStyle"; +/** Custom RPC method name — host: `await proxy.rpc("configure", options)`. */ +export const CONFIGURE_RPC_METHOD = "configure"; const themeSchema = z.strictObject({ primary: z.string().optional(), @@ -387,7 +387,7 @@ const copySchema = z.strictObject({ }) .optional(); -export const setStyleParamsSchema = z.strictObject({ +export const configureParamsSchema = z.strictObject({ theme: themeSchema, copy: copySchema, dark: z.boolean().optional(), @@ -401,27 +401,39 @@ export const setStyleParamsSchema = z.strictObject({ .optional(), }) .optional(), + /** + * URL to receive transaction status update webhooks from the 1Shot Relayer + * (≤256 chars). `null` or `""` clears a previously configured value. + * @see https://1shotapi.com/docs/relayer/get-started/overview + */ + destinationUrl: z + .union([ + z.url().max(256), + z.literal(""), + z.null(), + ]) + .optional(), }); -export type ISetStyleParams = z.infer; +export type IConfigureParams = z.infer; -export function registerSetStyleRpc( +export function registerConfigureRpc( wallet: OWSWallet, chainRepository: IChainRepository, ): void { wallet.registerRpc( - SET_STYLE_RPC_METHOD, + CONFIGURE_RPC_METHOD, async (params) => { - const styleParams = params as ISetStyleParams; - const resolved = styleController.merge(styleParams); - if (styleParams.features?.allowedChains !== undefined) { + const configureParams = params as IConfigureParams; + const resolved = styleController.merge(configureParams); + if (configureParams.features?.allowedChains !== undefined) { const catalogIds = new Set( chainRepository .getCatalog() .map((chain) => String(chain.chainId).toLowerCase()), ); const valid: ReturnType[] = []; - for (const id of styleParams.features.allowedChains) { + for (const id of configureParams.features.allowedChains) { const lower = id.toLowerCase(); if (catalogIds.has(lower)) { valid.push(EVMChainId(lower as `0x${string}`)); @@ -434,6 +446,6 @@ export function registerSetStyleRpc( productName: resolved.copy.productName, }; }, - setStyleParamsSchema, + configureParamsSchema, ); } diff --git a/src/style/styleController.ts b/src/style/styleController.ts index 0f63dd9..12f1bec 100644 --- a/src/style/styleController.ts +++ b/src/style/styleController.ts @@ -20,7 +20,7 @@ export const styleController = { return current; }, - /** Deep-merge options (additive; used by setStyle RPC). */ + /** Deep-merge options (additive; used by configure RPC). */ merge(patch: IStyleOptions): IResolvedStyle { current = mergeStyle(current, patch); applyStyleToDocument(current); diff --git a/src/style/types.ts b/src/style/types.ts index 4a78e7d..a5f9d39 100644 --- a/src/style/types.ts +++ b/src/style/types.ts @@ -1,5 +1,5 @@ /** - * Host-facing style knobs for `proxy.rpc("setStyle", options)`. + * Host-facing branding / config knobs for `proxy.rpc("configure", options)`. * All fields optional — omitted keys keep the previous / default value. */ export interface IStyleThemeOptions { @@ -471,7 +471,7 @@ export interface IStyleCopyOptions { advancedOptions?: Partial; } -/** Fully resolved copy map after merging defaults + setStyle. */ +/** Fully resolved copy map after merging defaults + configure. */ export interface IResolvedCopy { productName: string; tagline: string; @@ -530,12 +530,20 @@ export interface IStyleOptions { /** When true, add `.dark` on ; when false, remove it; omit = unchanged */ dark?: boolean; features?: IStyleFeaturesOptions; + /** + * Optional URL to receive transaction status update webhooks from the + * 1Shot Relayer (https://1shotapi.com/docs/relayer/get-started/overview). + * Pass `null` or `""` to clear. + */ + destinationUrl?: string | null; } -/** Fully resolved style after merging defaults + setStyle patches. */ +/** Fully resolved style after merging defaults + configure patches. */ export interface IResolvedStyle { theme: Required; copy: IResolvedCopy; dark: boolean; features: IResolvedStyleFeatures; + /** Status webhook URL for the 1Shot Relayer; `null` when unset. */ + destinationUrl: string | null; } diff --git a/src/wallet/WalletProvider.tsx b/src/wallet/WalletProvider.tsx index f258f5e..215cd1d 100644 --- a/src/wallet/WalletProvider.tsx +++ b/src/wallet/WalletProvider.tsx @@ -632,6 +632,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { session.setCredentialCount(0); session.setTrackedAssetCount(0); session.unfocusWallet(); + walletRef.current?.providerEvents.emit("accountsChanged", []); // Land on OnboardingPanel (login / create). Do not call ensureReady — // that would immediately reopen the setup modal. } diff --git a/src/wallet/useSwipeDownToDismiss.ts b/src/wallet/useSwipeDownToDismiss.ts new file mode 100644 index 0000000..414a41f --- /dev/null +++ b/src/wallet/useSwipeDownToDismiss.ts @@ -0,0 +1,84 @@ +import { useRef, type TouchEvent } from "react"; + +const MIN_DY_PX = 88; +const MAX_DX_RATIO = 0.65; + +function elementCanScrollUp(start: EventTarget | null): boolean { + let node = + start instanceof Element + ? start + : start instanceof Node + ? start.parentElement + : null; + + while (node && node !== document.documentElement) { + if (node instanceof HTMLElement) { + const { overflowY } = getComputedStyle(node); + const scrollable = + overflowY === "auto" || + overflowY === "scroll" || + overflowY === "overlay"; + if (scrollable && node.scrollTop > 0) { + return true; + } + } + node = node.parentElement; + } + return false; +} + +/** + * Touch handlers for swipe-down-to-dismiss (mobile drawer / flyout). + * Ignores gestures that begin inside a scrolled container. + */ +export function useSwipeDownToDismiss( + enabled: boolean, + onDismiss: () => void, +): { + onTouchStart: (event: TouchEvent) => void; + onTouchEnd: (event: TouchEvent) => void; + onTouchCancel: () => void; +} { + const startRef = useRef<{ x: number; y: number } | null>(null); + + const clear = () => { + startRef.current = null; + }; + + return { + onTouchStart: (event) => { + if (!enabled || event.touches.length !== 1) { + clear(); + return; + } + if (elementCanScrollUp(event.target)) { + clear(); + return; + } + const touch = event.touches[0]; + if (!touch) { + clear(); + return; + } + startRef.current = { x: touch.clientX, y: touch.clientY }; + }, + onTouchEnd: (event) => { + const start = startRef.current; + clear(); + if (!enabled || !start || event.changedTouches.length === 0) { + return; + } + const touch = event.changedTouches[0]; + if (!touch) { + return; + } + const dy = touch.clientY - start.y; + const dx = Math.abs(touch.clientX - start.x); + if (dy < MIN_DY_PX || dx > dy * MAX_DX_RATIO) { + return; + } + onDismiss(); + }, + onTouchCancel: clear, + }; +} diff --git a/src/wallet/useWalletBoot.ts b/src/wallet/useWalletBoot.ts index 9d78074..4484247 100644 --- a/src/wallet/useWalletBoot.ts +++ b/src/wallet/useWalletBoot.ts @@ -30,7 +30,7 @@ import { } from "../ows/registerAccountConnect"; import { registerApprovalSigning } from "../ows/registerApprovalSigning"; import { registerCredentialsProvider } from "../ows/registerCredentialsProvider"; -import { registerSetStyleRpc } from "../style/registerSetStyle"; +import { registerConfigureRpc } from "../style/registerConfigure"; import { wrapSignerWithCeremonyCopy } from "./wrapSignerWithCeremonyCopy"; import { DEFAULT_CHAIN_ID } from "../lib/implementations/data/HardcodedChainRepository"; import { @@ -269,7 +269,7 @@ export function useWalletBoot({ }) => ActiveModal, ) => pushModal(build); - registerSetStyleRpc(wallet, chainRepository); + registerConfigureRpc(wallet, chainRepository); registerAccountConnect(wallet, signer, { storage: walletStorage,