diff --git a/AGENTS.md b/AGENTS.md index 7dbf497f609..309316d55f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de - Biome governs formatting and linting; its rules live in biome.json. - Run pnpm biome check --apply before committing. - Avoid editor‑specific configs; rely on the shared settings. +- make sure everything builds after each file change by running `pnpm build` ⸻ @@ -39,7 +40,8 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de - Co‑locate tests: foo.ts ↔ foo.test.ts. - Use real function invocations with stub data; avoid brittle mocks. - For network interactions, use Mock Service Worker (MSW) to intercept fetch/HTTP calls, mocking only scenarios that are hard to reproduce. -- Keep tests deterministic and side‑effect free; Jest is pre‑configured. +- Keep tests deterministic and side‑effect free; Vitest is pre‑configured. +- to run the tests: `cd packages thirdweb & pnpm test:dev ` ⸻ diff --git a/apps/playground-web/src/app/connect/pay/commerce/page.tsx b/apps/playground-web/src/app/connect/pay/commerce/page.tsx index 41694596a20..67eaca12fc3 100644 --- a/apps/playground-web/src/app/connect/pay/commerce/page.tsx +++ b/apps/playground-web/src/app/connect/pay/commerce/page.tsx @@ -63,7 +63,8 @@ function BuyMerch() { sellerAddress: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675", }, metadata: { - name: "Black Hoodie (Size L)", + name: "Black Hoodie", + description: "Size L. Ships worldwide.", image: "/drip-hoodie.png", }, }} diff --git a/apps/playground-web/src/app/connect/pay/components/types.ts b/apps/playground-web/src/app/connect/pay/components/types.ts index 4c02e602a22..a8ab0e4b14e 100644 --- a/apps/playground-web/src/app/connect/pay/components/types.ts +++ b/apps/playground-web/src/app/connect/pay/components/types.ts @@ -12,6 +12,7 @@ export type PayEmbedPlaygroundOptions = { mode?: "fund_wallet" | "direct_payment" | "transaction"; title: string | undefined; image: string | undefined; + description: string | undefined; // fund_wallet mode options buyTokenAddress: string | undefined; diff --git a/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx b/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx index b96239a8364..c25d57d05c7 100644 --- a/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx @@ -496,6 +496,26 @@ export function LeftSection(props: { /> + + {/* Modal description */} +
+ + + setOptions((v) => ({ + ...v, + payOptions: { + ...payOptions, + description: e.target.value, + }, + })) + } + /> +
diff --git a/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx b/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx index 18b939a2983..0b484d9ac95 100644 --- a/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx @@ -70,15 +70,19 @@ export function RightSection(props: { (props.options.payOptions.mode === "transaction" ? "Transaction" : props.options.payOptions.mode === "direct_payment" - ? "Purchase" + ? "Product Name" : "Buy Crypto"), + description: + props.options.payOptions.description || "Your own description here", image: props.options.payOptions.image || - `https://placehold.co/600x400/${ - props.options.theme.type === "dark" - ? "1d1d23/7c7a85" - : "f2eff3/6f6d78" - }?text=Your%20Product%20Here&font=roboto`, + props.options.payOptions.mode === "direct_payment" + ? `https://placehold.co/600x400/${ + props.options.theme.type === "dark" + ? "1d1d23/7c7a85" + : "f2eff3/6f6d78" + }?text=Your%20Product%20Here&font=roboto` + : undefined, }, // Mode-specific options diff --git a/apps/playground-web/src/app/connect/pay/embed/page.tsx b/apps/playground-web/src/app/connect/pay/embed/page.tsx index ccf7cef3e0d..3d2fe159e26 100644 --- a/apps/playground-web/src/app/connect/pay/embed/page.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/page.tsx @@ -17,6 +17,7 @@ const defaultConnectOptions: PayEmbedPlaygroundOptions = { mode: "fund_wallet", title: "", image: "", + description: "", buyTokenAddress: NATIVE_TOKEN_ADDRESS, buyTokenAmount: "0.01", buyTokenChain: base, diff --git a/apps/playground-web/src/components/pay/direct-payment.tsx b/apps/playground-web/src/components/pay/direct-payment.tsx index c3120ab9e75..a0b88154776 100644 --- a/apps/playground-web/src/components/pay/direct-payment.tsx +++ b/apps/playground-web/src/components/pay/direct-payment.tsx @@ -20,6 +20,7 @@ export function BuyMerchPreview() { }, metadata: { name: "Black Hoodie (Size L)", + description: "Size L. Ships worldwide.", image: "/drip-hoodie.png", }, }} diff --git a/packages/thirdweb/scripts/wallets/generate.ts b/packages/thirdweb/scripts/wallets/generate.ts index 4791b56bcab..001f5949902 100644 --- a/packages/thirdweb/scripts/wallets/generate.ts +++ b/packages/thirdweb/scripts/wallets/generate.ts @@ -238,11 +238,11 @@ export type MinimalWalletInfo = { /** * @internal */ -const ALL_MINIMAL_WALLET_INFOS = ${JSON.stringify( +const ALL_MINIMAL_WALLET_INFOS = ${JSON.stringify( [...walletInfos, ...customWalletInfos], null, 2, - )} satisfies MinimalWalletInfo[]; + )} as const satisfies MinimalWalletInfo[]; export default ALL_MINIMAL_WALLET_INFOS; `, diff --git a/packages/thirdweb/src/bridge/Routes.ts b/packages/thirdweb/src/bridge/Routes.ts index d023c7d7e95..012a9fb42fc 100644 --- a/packages/thirdweb/src/bridge/Routes.ts +++ b/packages/thirdweb/src/bridge/Routes.ts @@ -131,6 +131,7 @@ export async function routes(options: routes.Options): Promise { sortBy, limit, offset, + includePrices, } = options; const clientFetch = getClientFetch(client); @@ -159,6 +160,9 @@ export async function routes(options: routes.Options): Promise { if (sortBy) { url.searchParams.set("sortBy", sortBy); } + if (includePrices) { + url.searchParams.set("includePrices", includePrices.toString()); + } const response = await clientFetch(url.toString()); if (!response.ok) { @@ -185,6 +189,7 @@ export declare namespace routes { transactionHash?: ox__Hex.Hex; sortBy?: "popularity"; maxSteps?: number; + includePrices?: boolean; limit?: number; offset?: number; }; diff --git a/packages/thirdweb/src/bridge/Token.ts b/packages/thirdweb/src/bridge/Token.ts index 68699915fb9..29a67e8d45f 100644 --- a/packages/thirdweb/src/bridge/Token.ts +++ b/packages/thirdweb/src/bridge/Token.ts @@ -158,7 +158,7 @@ export async function tokens(options: tokens.Options): Promise { export declare namespace tokens { /** - * Input parameters for {@link Bridge.tokens}. + * Input parameters for {@link tokens}. */ type Options = { /** Your {@link ThirdwebClient} instance. */ @@ -182,3 +182,84 @@ export declare namespace tokens { */ type Result = Token[]; } + +/** + * Adds a token to the Universal Bridge for indexing. + * + * This function requests the Universal Bridge to index a specific token on a given chain. + * Once indexed, the token will be available for cross-chain operations. + * + * @example + * ```typescript + * import { Bridge } from "thirdweb"; + * + * // Add a token for indexing + * const result = await Bridge.add({ + * client: thirdwebClient, + * chainId: 1, + * tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + * }); + * ``` + * + * @param options - The options for adding a token. + * @param options.client - Your thirdweb client. + * @param options.chainId - The chain ID where the token is deployed. + * @param options.tokenAddress - The contract address of the token to add. + * + * @returns A promise that resolves when the token has been successfully submitted for indexing. + * + * @throws Will throw an error if there is an issue adding the token. + * @bridge + * @beta + */ +export async function add(options: add.Options): Promise { + const { client, chainId, tokenAddress } = options; + + const clientFetch = getClientFetch(client); + const url = `${getThirdwebBaseUrl("bridge")}/v1/tokens`; + + const requestBody = { + chainId, + tokenAddress, + }; + + const response = await clientFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorJson = await response.json(); + throw new ApiError({ + code: errorJson.code || "UNKNOWN_ERROR", + message: errorJson.message || response.statusText, + correlationId: errorJson.correlationId || undefined, + statusCode: response.status, + }); + } + + const { data }: { data: Token } = await response.json(); + return data; +} + +export declare namespace add { + /** + * Input parameters for {@link add}. + */ + type Options = { + /** Your {@link ThirdwebClient} instance. */ + client: ThirdwebClient; + /** The chain ID where the token is deployed. */ + chainId: number; + /** The contract address of the token to add. */ + tokenAddress: string; + }; + + /** + * The result returned from {@link Bridge.add}. + */ + type Result = Token; +} diff --git a/packages/thirdweb/src/bridge/types/BridgeAction.ts b/packages/thirdweb/src/bridge/types/BridgeAction.ts index 7a83d5c9d3a..ffc4d58a31e 100644 --- a/packages/thirdweb/src/bridge/types/BridgeAction.ts +++ b/packages/thirdweb/src/bridge/types/BridgeAction.ts @@ -1 +1 @@ -export type Action = "approval" | "transfer" | "buy" | "sell"; +export type Action = "approval" | "transfer" | "buy" | "sell" | "fee"; diff --git a/packages/thirdweb/src/bridge/types/Errors.ts b/packages/thirdweb/src/bridge/types/Errors.ts index ffa8da23164..f7f2074da20 100644 --- a/packages/thirdweb/src/bridge/types/Errors.ts +++ b/packages/thirdweb/src/bridge/types/Errors.ts @@ -1,3 +1,5 @@ +import { stringify } from "../../utils/json.js"; + type ErrorCode = | "INVALID_INPUT" | "ROUTE_NOT_FOUND" @@ -22,4 +24,13 @@ export class ApiError extends Error { this.correlationId = args.correlationId; this.statusCode = args.statusCode; } + + override toString() { + return stringify({ + code: this.code, + message: this.message, + statusCode: this.statusCode, + correlationId: this.correlationId, + }); + } } diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts index 2ce99e691be..d7c722f2b9f 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -291,9 +291,9 @@ export async function getBuyWithFiatQuote( provider?: FiatProvider, ): "stripe" | "coinbase" | "transak" => { switch (provider) { - case "STRIPE": + case "stripe": return "stripe"; - case "TRANSAK": + case "transak": return "transak"; default: // default to coinbase when undefined or any other value return "coinbase"; diff --git a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts index fc6b04b0d81..6b80fd3a353 100644 --- a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts +++ b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts @@ -2,7 +2,7 @@ import type { Address } from "abitype"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; import { isAddress } from "../../utils/address.js"; -import { getTokenPrice } from "./get-token.js"; +import { getToken } from "./get-token.js"; import type { SupportedFiatCurrency } from "./type.js"; /** @@ -73,11 +73,11 @@ export async function convertCryptoToFiat( "Invalid fromTokenAddress. Expected a valid EVM contract address", ); } - const price = await getTokenPrice(client, fromTokenAddress, chain.id); - if (!price) { + const token = await getToken(client, fromTokenAddress, chain.id); + if (token.priceUsd === 0) { throw new Error( `Error: Failed to fetch price for token ${fromTokenAddress} on chainId: ${chain.id}`, ); } - return { result: price * fromAmount }; + return { result: token.priceUsd * fromAmount }; } diff --git a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts index 82ab392727e..f0843f0974d 100644 --- a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts +++ b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts @@ -2,7 +2,7 @@ import type { Address } from "abitype"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; import { isAddress } from "../../utils/address.js"; -import { getTokenPrice } from "./get-token.js"; +import { getToken } from "./get-token.js"; import type { SupportedFiatCurrency } from "./type.js"; /** @@ -72,11 +72,11 @@ export async function convertFiatToCrypto( if (!isAddress(to)) { throw new Error("Invalid `to`. Expected a valid EVM contract address"); } - const price = await getTokenPrice(client, to, chain.id); - if (!price || price === 0) { + const token = await getToken(client, to, chain.id); + if (!token || token.priceUsd === 0) { throw new Error( `Error: Failed to fetch price for token ${to} on chainId: ${chain.id}`, ); } - return { result: fromAmount / price }; + return { result: fromAmount / token.priceUsd }; } diff --git a/packages/thirdweb/src/pay/convert/get-token.ts b/packages/thirdweb/src/pay/convert/get-token.ts index 6ec2eace996..351f7cdce9d 100644 --- a/packages/thirdweb/src/pay/convert/get-token.ts +++ b/packages/thirdweb/src/pay/convert/get-token.ts @@ -1,12 +1,13 @@ -import { tokens } from "../../bridge/Token.js"; +import { add, tokens } from "../../bridge/Token.js"; +import type { Token } from "../../bridge/types/Token.js"; import type { ThirdwebClient } from "../../client/client.js"; import { withCache } from "../../utils/promise/withCache.js"; -export async function getTokenPrice( +export async function getToken( client: ThirdwebClient, tokenAddress: string, chainId: number, -) { +): Promise { return withCache( async () => { const result = await tokens({ @@ -14,7 +15,19 @@ export async function getTokenPrice( tokenAddress, chainId, }); - return result[0]?.priceUsd; + const token = result[0]; + if (!token) { + // Attempt to add the token + const tokenResult = await add({ + client, + chainId, + tokenAddress, + }).catch(() => { + throw new Error("Token not supported"); + }); + return tokenResult; + } + return token; }, { cacheKey: `get-token-price-${tokenAddress}-${chainId}`, diff --git a/packages/thirdweb/src/pay/utils/commonTypes.ts b/packages/thirdweb/src/pay/utils/commonTypes.ts index 2f8fc723a4d..ef94c3fef4d 100644 --- a/packages/thirdweb/src/pay/utils/commonTypes.ts +++ b/packages/thirdweb/src/pay/utils/commonTypes.ts @@ -19,4 +19,4 @@ export type PayOnChainTransactionDetails = { export type FiatProvider = (typeof FiatProviders)[number]; -export const FiatProviders = ["COINBASE", "STRIPE", "TRANSAK"] as const; +export const FiatProviders = ["coinbase", "stripe", "transak"] as const; diff --git a/packages/thirdweb/src/react/PRODUCT.md b/packages/thirdweb/src/react/PRODUCT.md new file mode 100644 index 00000000000..3ac6557f242 --- /dev/null +++ b/packages/thirdweb/src/react/PRODUCT.md @@ -0,0 +1,158 @@ +# BridgeEmbed 2.0 — **Product Specification (Revised)** + +**Version:** 1.0 +**Updated:** 30 May 2025 +**Author:** Product Team, thirdweb + +--- + +## 1 · Purpose + +BridgeEmbed is a drop-in replacement for PayEmbed that unlocks multi-hop cross-chain payments, token swaps, and fiat on-ramp flows by building on the new Bridge.\* API layer. +Developers should adopt the widget with zero code changes to existing PayEmbed integrations (same props & callbacks) while gaining: + +- Swap, bridge, or transfer any crypto asset to any asset on the desired chain. +- Accept fiat (card/Apple Pay/Google Pay) via on-ramp partners and settle in the target token. +- Support three payment contexts—funding a wallet, paying a seller, or funding a transaction. +- Automatic route discovery, optimisation, and step-by-step execution via Bridge.routes, quote, prepare, and status. + +### Goal + +| Success Criteria | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------- | +| Drop-in upgrade | Swap `` for `` and see identical behaviour for same-chain / same-token payments. | +| Multi-hop routing | Fund USDC on Base using an NZD credit card, or swap MATIC→ETH→USDC across chains in one flow. | +| Unified UX | All three modes share one cohesive flow (quote → route preview → step runner → success). | +| Fast integration | ≤ 5-minute copy-paste setup, props-only—no back-end work. | + +### 3 modes to cover different use cases + +| Mode | Typical Use-case | Destination of Funds | +| --------------------- | ------------------------------------------------------------------- | -------------------------------------------- | +| fund_wallet (default) | User acquires Token X for their own wallet. | Connected wallet | +| direct_payment | User buys a product; seller requires Token Y on Chain C. | Seller address | +| transaction | dApp needs to cover value/erc20Value of a prepared on-chain action. | Connected wallet, then transaction broadcast | + +BridgeEmbed 2.0 is the successor to **PayEmbed** and delivers a **modular, cross-platform hook library and UI component** for fiat / crypto on-ramping, token swaps, bridging, and direct payments. +Developers can import: + +| Layer | What it contains | Platform variants | +| -------------------------- | ------------------------------------------------------------------------ | ------------------------------- | +| **Core hooks & utilities** | Logic, data-fetching, state machines, type helpers | **Shared** (single TS codebase) | +| **Core UI components** | Payment-method picker, route preview, step runner, error & success views | **Web / React Native** | +| **Higher-level flows** | ``, ``, `` | **Web / React Native** | +| **Turn-key widget** | `` (switches flow by `mode` prop) | **Web / React Native** | + +This structure keeps one business-logic layer while letting each platform ship native UX. + +--- + +## 2 · High-Level Goals + +| Goal | Success Criteria | +| ------------------------- | --------------------------------------------------------------------------------------------- | +| **Drop-in replacement** | Existing PayEmbed users swap imports; same props still work. | +| **Modularity** | Apps may import only `useBridgeQuote` or `` without the full widget. | +| **Cross-platform parity** | Web and React Native share ≥ 90 % of code via core hooks; UI feels native on each platform. | +| **Robust error UX** | Every failure surfaces the underlying Bridge API message and offers a **Retry** action. | + +--- + +## 3 · Package & File Structure + +``` +packages/thirdweb/src/react/ + ├─ core/ # shared TS logic & hooks + │ └─ src/ + │ ├─ hooks/ + │ ├─ machines/ # XState or equivalent + │ └─ utils/ + ├─ web/ # React (DOM) components + │ └─ components/ + └─ native/ # React Native components + └─ components/ +``` + +--- + +## 4 · Key Exports + +### 4.1 Hooks (core) + +| Hook | Responsibility | +| -------------------------- | -------------------------------------------------------------------------- | +| `usePaymentMethods()` | Detect connected wallet balances, other-wallet option, available on-ramps. | +| `useBridgeRoutes(params)` | Thin wrapper over `Bridge.routes/quote`; caches and re-tries. | +| `useBridgePrepare(params)` | Call `Bridge.prepare`, returns signed tx set & metadata. | +| `useStepExecutor(steps)` | Drive sequential execution + status polling, emits progress/error events. | +| `useBridgeError()` | Provide typed error object with `.code`, `.message`, `.retry()` helper. | + +### 4.2 Core UI Components + +| Component | Props | Web / RN notes | +| ----------------------- | -------------------------------- | --------------------------------------------- | +| `PaymentMethodSelector` | `methods`, `onSelect` | Web: dropdown / wallet list; RN: ActionSheet. | +| `RoutePreview` | `route`, `onConfirm` | Shows hops, fees, ETA. | +| `StepRunner` | `steps`, `onComplete`, `onError` | Progress bar + per-step status. | +| `ErrorBanner` | `error` | Always shows retry CTA. | +| `SuccessScreen` | `receipt` | Shows final tx hash, share buttons. | + +### 4.3 Higher-Level Components + +| Name | Mode encapsulated | +| ------------------------ | ------------------ | +| `` | `"fund_wallet"` | +| `` | `"direct_payment"` | +| `` | `"transaction"` | + +### 4.4 Turn-key Widget + +```tsx +import { BridgeEmbed } from "thirdweb/react"; +``` + +EXACT Same prop surface as `` for this one, should be a drop replacement with no code changes. + +--- + +## 5 · User Flows + +_All flows share the same state machine; UI differs by platform._ + +1. **Requirement Resolution** – derive target token/chain/amount. +2. **Method Selection** – `PaymentMethodSelector`. +3. **Quote & Route** – `useBridgeRoutes` → show `RoutePreview`. +4. **Confirm** – user approves (wallet popup or on-ramp). +5. **Execute Steps** – `StepRunner` driven by `useStepExecutor`. +6. **Success** – `SuccessScreen` with receipts. +7. **Error & Retry** – Any failure shows `ErrorBanner`; calling `.retry()` re-enters machine at the failed state (idempotent by design). + +## 6. UX & UI Requirements + +- Responsive (mobile-first; desktop ≤ 480 px width). +- Single modal with internal stepper—no new windows. +- Progress feedback: percent bar + "Step 2 / 4: Swapping MATIC → USDC". +- Retry / resume: if closed mid-flow, reopening fetches Bridge.status and resumes. +- Theming: inherits PayEmbed theme prop (light/dark & accent). +- Localization: reuse existing i18n keys; add new strings. + +--- + +## 7 · Error Handling Guidelines + +- **Surface origin:** Display `error.message` from Bridge/on-ramp APIs; prepend user-friendly context ("Swap failed – "). +- **Retry always available:** `StepRunner` pauses; user can press **Retry** (calls hook's `.retry()`) or **Cancel**. +- **Automatic back-off:** Core hooks implement exponential back-off for transient network errors. +- **Developer visibility:** All hooks throw typed errors so host apps can catch & log if using components piecemeal. + +--- + +## 8 · Cross-Platform Parity Requirements + +| Feature | Web | React Native | +| ----------------- | ---------------------------------------- | ------------------------------------------------------ | +| Wallet connectors | MetaMask, Coinbase Wallet, WalletConnect | WalletConnect, MetaMask Mobile Deeplink, in-app wallet | +| Fiat on-ramp UI | window popup (Stripe, Ramp) | Safari/Chrome Custom Tab / In-App Browser | +| Step progress | Horizontal stepper with overall progress | Vertical list with checkmarks | + +The **state machine & hooks are identical**; only presentation components differ. diff --git a/packages/thirdweb/src/react/TASK_LIST.md b/packages/thirdweb/src/react/TASK_LIST.md new file mode 100644 index 00000000000..57e593aa597 --- /dev/null +++ b/packages/thirdweb/src/react/TASK_LIST.md @@ -0,0 +1,720 @@ +# BridgeEmbed 2.0 — Engineering Task List + +All tasks below are **actionable check-boxes** that an AI coding agent can tick off sequentially. Follow milestones in order; each item should result in one or more concise commits / PRs. + +--- + +## 🗂️ Milestone 1 · Folder Structure & Scaffolding + +TECH_SPEC §2, §2.1 + +> Goal: establish empty folder skeletons for shared logic and platform UI layers. + +### Tasks + +- [x] Create directory `core/hooks/` with `.keep` placeholder. +- [x] Create directory `core/machines/` with `.keep`. +- [x] Create directory `core/utils/` with `.keep`. +- [x] Create directory `core/errors/` with `.keep`. +- [x] Create directory `core/types/` with `.keep`. +- [x] Create directory `core/adapters/` with `.keep`. +- [x] Create directory `web/components/` with `.keep`. +- [x] Create directory `web/flows/` with `.keep`. +- [x] Create directory `native/components/` with `.keep`. +- [x] Create directory `native/flows/` with `.keep`. + +Acceptance ✅: running `pnpm build` still succeeds (no new source yet). + +--- + +## ⚙️ Milestone 2 · Error Normalisation Helpers + +TECH_SPEC §6 + +> Goal: convert raw `ApiError` instances from the Bridge SDK (see `bridge/types/Errors.ts`) into UI-friendly domain errors. + +### Tasks + +- [x] Add `core/errors/mapBridgeError.ts` exporting `mapBridgeError(e: ApiError): ApiError` (initially returns the same error; will evolve). +- [x] Unit-test `mapBridgeError` with at least three representative `ApiError.code` cases. +- [x] Export a typed helper `isRetryable(code: ApiError["code"]): boolean` alongside the map (treat `INTERNAL_SERVER_ERROR` & `UNKNOWN_ERROR` as retryable). + +Acceptance ✅: Vitest suite green (`pnpm test:dev mapBridgeError`); typing passes. + +--- + +## 🔌 Milestone 3 · Dependency Adapters + +TECH_SPEC §13 + +> Goal: define inversion interfaces and provide default Web / RN implementations. + +### Core Interface Definitions (`core/adapters/`) + +- [x] `WindowAdapter` – `open(url: string): Promise` +- ~~[x] `StorageAdapter` – `get(key)`, `set(key,value)`, `delete(key)` async methods~~ (using existing `AsyncStorage` from `utils/storage`) + +### Default Web Implementations (`web/adapters/`) + +- [x] `window` wrapper implementing `WindowAdapter`. +- ~~[x] LocalStorage wrapper implementing `StorageAdapter`.~~ (using existing `webLocalStorage`) + +### Default RN Implementations (`native/adapters/`) + +- [x] `Linking.openURL` wrapper (`WindowAdapter`). +- ~~[x] AsyncStorage wrapper (`StorageAdapter`).~~ (using existing `nativeLocalStorage`) + +### Tests + +- [x] Web adapter unit tests with vitest mocks for each browser API. + +Acceptance ✅: All interfaces compile, Web tests pass (`pnpm test:dev adapters`). + +--- + +## 🔄 Milestone 4 · Payment State Machine (XState 5) + +TECH_SPEC §4.1 + +> Goal: scaffold the deterministic state machine driving every flow with improved field naming and discriminated union PaymentMethod type. + +### State Machine Flow + +The payment machine follows a linear progression through 8 states, with error handling and retry capabilities at each step: + +``` +┌─────────────────┐ REQUIREMENTS_RESOLVED ┌─────────────────┐ +│ resolveRequire- │ ──────────────────────────→ │ methodSelection │ +│ ments │ │ │ +└─────────────────┘ └─────────────────┘ + │ │ + │ │ PAYMENT_METHOD_SELECTED + │ │ (wallet or fiat + data) + │ ▼ + │ ┌─────────────────┐ + │ │ quote │ + │ └─────────────────┘ + │ │ + │ │ QUOTE_RECEIVED + │ ▼ + │ ┌─────────────────┐ + │ │ preview │ + │ └─────────────────┘ + │ │ + │ │ ROUTE_CONFIRMED + │ ▼ + │ ┌─────────────────┐ + │ │ prepare │ + │ └─────────────────┘ + │ │ + │ │ STEPS_PREPARED + │ ▼ + │ ┌─────────────────┐ + │ │ execute │ + │ └─────────────────┘ + │ │ + │ │ EXECUTION_COMPLETE + │ ▼ + │ ┌─────────────────┐ + │ │ success │ + │ └─────────────────┘ + │ │ + │ ERROR_OCCURRED │ RESET + │ (from any state) │ + ▼ ▼ +┌─────────────────┐ RETRY ┌─────────────────┐ +│ error │ ──────────────────────────→ │ resolveRequire- │ +│ │ │ ments │ +└─────────────────┘ ←─────────────────────────── └─────────────────┘ + RESET +``` + +**Key Flow Characteristics:** + +1. **Linear Progression**: Each state transitions to the next in sequence when successful +2. **Error Recovery**: Any state can transition to `error` state via `ERROR_OCCURRED` event +3. **Retry Logic**: From `error` state, `RETRY` returns to `resolveRequirements` (UI layer handles resume logic based on `retryState`) +4. **Reset Capability**: `RESET` event returns to initial state from `error` or `success` +5. **Type Safety**: `PaymentMethod` discriminated union ensures wallet/fiat data is validated + +**State Responsibilities:** + +- **resolveRequirements**: Determine destination chain, token, and amount +- **methodSelection**: Choose payment method with complete configuration +- **quote**: Fetch routing options from Bridge SDK +- **preview**: Display route details for user confirmation +- **prepare**: Prepare transaction steps for execution +- **execute**: Execute prepared steps (signatures, broadcasts, etc.) +- **success**: Payment completed successfully +- **error**: Handle errors with retry capabilities + +### Tasks + +- [x] ~~Add dev dependency `@xstate/fsm`.~~ **Updated**: Migrated to full XState v5 library for better TypeScript support and new features. +- [x] In `core/machines/paymentMachine.ts`, define context & eight states (`resolveRequirements`, `methodSelection`, `quote`, `preview`, `prepare`, `execute`, `success`, `error`) with: + - **Updated field names**: `destinationChainId` (number), `destinationTokenAddress`, `destinationAmount` + - **Discriminated union PaymentMethod**: `{ type: "wallet", originChainId, originTokenAddress }` or `{ type: "fiat", currency }` + - **Simplified events**: Single `PAYMENT_METHOD_SELECTED` event that includes all required data for the selected method +- [x] Wire minimal transitions with streamlined methodSelection flow (single event with complete method data). +- [x] Create `core/utils/persist.ts` with `saveSnapshot`, `loadSnapshot` that use injected AsyncStorage and support discriminated union structure. +- [x] Unit-test happy-path transition sequence including wallet and fiat payment method flows with type safety. + +Acceptance ✅: Machine file compiles; Vitest model test green (`pnpm test:dev paymentMachine`) - 8 tests covering core flow and error handling. + +--- + +## 📚 Milestone 5 · Core Data Hooks (Logic Only) + +PRODUCT §4.1, TECH_SPEC §5 + +> Goal: implement framework-agnostic data hooks. + +### Setup + +- [x] Ensure `@tanstack/react-query` peer already in workspace; if not, add. + +### Hook Tasks + +- [x] `usePaymentMethods()` – returns available payment method list (mock stub: returns `["wallet","fiat"]`). +- [x] `useBridgeRoutes(params)` – wraps `Bridge.routes()`; includes retry + cache key generation. +- [x] `useBridgePrepare(route)` – delegates to `Bridge.Buy.prepare` / etc. depending on route kind. +- [ ] `useStepExecutor(steps)` – sequentially executes steps; includes batching + in-app signer optimisation (TECH_SPEC §9). +- [x] `useBridgeError()` – consumes `mapBridgeError` & `isRetryable`. + +### 🛠️ Sub-Milestone 5.1 · Step Executor Hook + +`core/hooks/useStepExecutor.ts` + +**High-level flow (from PRODUCT §5 & TECH_SPEC §9)** + +1. Receive **prepared quote** (result of `useBridgePrepare`) containing `steps` ― each step has a `transactions[]` array. +2. If **onramp is configured**, open the payment URL first and wait for completion before proceeding with transactions. +3. UI shows a full route preview; on user confirmation we enter **execution mode** handled by this hook. +4. For every transaction **in order**: + 1. Convert the raw Bridge transaction object to a wallet-specific `PreparedTransaction` via existing `prepareTransaction()` util. + 2. Call `account.sendTransaction(preparedTx)` where `account = wallet.getAccount()` supplied via params. + 3. Capture & emit the resulting transaction hash. + 4. Poll `Bridge.status({ hash, chainId })` until status `"completed"` (exponential back-off, 1 → 2 → 4 → ... max 16 s). + +**Public API** + +```ts +const { + currentStep, // RouteStep | undefined + currentTxIndex, // number | undefined + progress, // 0-100 number (includes onramp if configured) + isExecuting, // boolean + error, // ApiError | undefined + start, // () => void + cancel, // () => void (sets state to cancelled, caller decides UI) + retry, // () => void (restarts from failing tx) +} = useStepExecutor({ + steps, // RouteStep[] from Bridge.prepare + wallet, // Wallet instance (has getAccount()) + windowAdapter, // WindowAdapter for on-ramp links + client, // ThirdwebClient for API calls + onramp: { + // Optional onramp configuration + paymentUrl, // URL to open for payment + sessionId, // Onramp session ID for polling + }, + onComplete: (completedStatuses) => { + // Called when all steps complete successfully - receives array of completed status results + // completedStatuses contains all Bridge.status and Onramp.status responses with status: "COMPLETED" + // Show next UI step, navigate, etc. + }, +}); +``` + +**Execution rules** + +- **Onramp first**: If onramp is configured, it executes before any transactions +- **Sequential**: never execute next tx before previous is `completed`. +- **Batch optimisation**: if `account.sendBatchTransaction` exists **and** all pending tx are on same chain → batch them. +- **In-app signer**: if `isInAppSigner(wallet)` returns true, hook auto-confirms silently (no extra UI prompt). +- **Retry** uses `mapBridgeError` – only allowed when `isRetryable(code)`. +- Emits React Query mutations for each tx so UI can subscribe. + +### ❑ Sub-tasks + +- [x] Define `StepExecutorOptions` & return type. +- [x] Flatten `RouteStep[]` → `BridgeTx[]` util. +- [x] Implement execution loop with batching & signer optimisation. +- [x] Integrate on-ramp polling path. +- [x] Expose progress calculation (completedTx / totalTx). +- [x] Handle cancellation & cleanup (abort polling timers). +- [x] Unit tests: + - [x] Happy-path multi-tx execution (wallet signer). + - [x] Batching path (`sendBatchTransaction`). + - [x] In-app signer auto-execution. + - [x] Retryable network error mid-flow. + - [x] On-ramp flow polling completes. + - [x] Cancellation stops further polling. + +Acceptance ✅: `useStepExecutor.test.ts` green; lint & build pass. Ensure no unhandled promises, and timers are cleared on unmount. + +### Tests + +- [x] Unit tests for each hook with mocked Bridge SDK + adapters. + +Acceptance ✅: All hook tests green (`pnpm test:dev useStepExecutor`); type-check passes. + +--- + +## 🔳 Milestone 6 · Tier-0 Primitive Audit & Gaps + +TECH_SPEC §8.1 + +> Goal: catalogue existing components. + +### Tasks + +- [x] Find all the core UI components for web under src/react/web/ui/components +- [x] Find all the prebuilt components for web under src/react/web/ui/prebuilt +- [x] Find all the re-used components for web under src/react/web/ui +- [x] Generate markdown table of discovered components under `src/react/components.md`, categorized by Core vs prebuilt vs re-used components and mark the number of ocurrences for each + +Acceptance ✅: Storybook renders all re-used components without errors. + +--- + +## ✅ Milestone 7: Bridge Flow Components & XState v5 Migration (COMPLETED) + +**Goal**: Create working screen-to-screen navigation using dummy data, migrate to XState v5, and establish proper component patterns. + +### 🔄 **Phase 1: XState v5 Migration** + +**Tasks Executed:** + +- [x] **Migrated from @xstate/fsm to full XState v5** + - Updated `paymentMachine.ts` to use XState v5's `setup()` function with proper type definitions + - Converted to named actions for better type safety + - Updated `FundWallet.tsx` to use `useMachine` hook instead of `useActorRef` + `useSelector` + - Updated package dependencies: removed `@xstate/fsm`, kept full `xstate` v5.19.4 + - Updated tests to use XState v5 API with `createActor()` pattern + - **Result**: All 8 tests passing, enhanced TypeScript support, modern API usage + +**Learning**: XState v5 provides superior TypeScript support and the `useMachine` hook is simpler than the useActorRef + useSelector pattern for basic usage. + +### 🎨 **Phase 2: RoutePreview Story Enhancement** + +**Tasks Executed:** + +- [x] **Enhanced RoutePreview.stories.tsx with comprehensive dummy data** + - Added realistic 3-step transaction flow (approve → bridge → confirm) + - Created multiple story variations: WithComplexRoute, FastAndCheap + - Added light and dark theme variants for all stories + - Included realistic route details: fees, timing estimates, token amounts, chain information + - Fixed TypeScript errors by ensuring dummy data conformed to DummyRoute interface + +### 🔄 **Phase 3: Component Rename & Architecture** + +**Tasks Executed:** + +- [x] **Renamed FundWallet → BridgeOrchestrator with comprehensive updates** + - Updated component files: `FundWallet.tsx` → `BridgeOrchestrator.tsx` + - Updated props: `FundWalletProps` → `BridgeOrchestratorProps` + - Updated storybook files with better documentation + - Updated all documentation: TASK_LIST.md, PRODUCT.md, TECH_SPEC.md + - Updated factory function names: `createFundWalletFlow()` → `createBridgeOrchestratorFlow()` + - Cleaned up old files + +### 💰 **Phase 4: New FundWallet Component Creation** + +**Tasks Executed:** + +- [x] **Created interactive FundWallet component for fund_wallet mode** + - Large editable amount input with dynamic font sizing and validation + - Token icon, symbol and chain icon with visual indicators + - Dollar value display using real token price data + - Continue button that sends "REQUIREMENTS_RESOLVED" event + - Proper accessibility with keyboard navigation support + - Integration with BridgeOrchestrator state machine + +**Features:** + +- Dynamic amount input with width and font size adjustment +- Real-time validation and button state management +- Token and chain display with placeholder icons +- USD price calculation using `token.priceUsd` +- Click-to-focus input wrapper for better UX + +### 🏗️ **Phase 5: Real Types Migration** + +**Tasks Executed:** + +- [x] **Replaced all dummy types with real Bridge SDK types** + - `DummyChain` → `Chain` from `../../../../chains/types.js` + - `DummyToken` → `Token` from `../../../../bridge/types/Token.js` + - `DummyClient` → `ThirdwebClient` from `../../../../client/client.js` + - Updated all component props and examples to use real type structures + - Enhanced functionality with real price data (`token.priceUsd`) + - Added proper type safety throughout components + +### 🎯 **Phase 6: Best Practices Implementation** + +**Tasks Executed:** + +- [x] **Implemented proper thirdweb patterns** + - Used `defineChain(chainId)` helper instead of manual chain object construction + - Made `ThirdwebClient` a required prop in `BridgeOrchestrator` for dependency injection + - Updated storybook to use `storyClient` from utils instead of dummy client objects + - Simplified chain creation: `defineChain(1)` vs manual RPC configuration + - Centralized client configuration for consistency + +### 📚 **Phase 7: Storybook Pattern Compliance** + +**Tasks Executed:** + +- [x] **Updated all stories to follow ErrorBanner.stories.tsx pattern** + - Created proper wrapper components with theme props + - Used `CustomThemeProvider` with theme parameter + - Added comprehensive story variants (Light/Dark for all examples) + - Implemented proper `argTypes` for theme control + - Added background parameters for better visual testing + +### 🧪 **Technical Verification** + +- [x] **Build & Test Success**: All builds passing, 8/8 payment machine tests ✓ +- [x] **TypeScript Compliance**: Full type safety with real SDK types +- [x] **Component Integration**: FundWallet properly integrated with BridgeOrchestrator +- [x] **Storybook Ready**: All components with comprehensive stories + +--- + +### 🎓 **Key Learnings & Best Practices** + +#### **1. Storybook Patterns** + +```typescript +// ✅ Correct Pattern (follow ErrorBanner.stories.tsx) +interface ComponentWithThemeProps extends ComponentProps { + theme: "light" | "dark" | Theme; +} + +const ComponentWithTheme = (props: ComponentWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; +``` + +**Rule**: Always follow existing established patterns instead of creating custom wrapper solutions. + +#### **2. Type System Usage** + +```typescript +// ❌ Wrong: Dummy types +type DummyChain = { id: number; name: string }; + +// ✅ Correct: Real SDK types +import type { Chain } from "../../../../chains/types.js"; +import type { Token } from "../../../../bridge/types/Token.js"; +``` + +**Rule**: Use real types from the SDK from the beginning - don't create dummy types as placeholders. + +#### **3. Chain Creation** + +```typescript +// ❌ Wrong: Manual construction +chain: { + id: 1, + name: "Ethereum", + rpc: "https://ethereum.blockpi.network/v1/rpc/public", +} as Chain + +// ✅ Correct: Helper function +import { defineChain } from "../../chains/utils.js"; +chain: defineChain(1) // Auto-gets metadata, RPC, icons +``` + +**Rule**: Use `defineChain(chainId)` helper for automatic chain metadata instead of manual object construction. + +#### **4. Dependency Injection** + +```typescript +// ❌ Wrong: Create client internally +const client = { clientId: "demo", secretKey: undefined } as ThirdwebClient; + +// ✅ Correct: Pass as prop +interface BridgeOrchestratorProps { + client: ThirdwebClient; // Required prop +} +``` + +**Rule**: ThirdwebClient should be passed as a prop for proper dependency injection, not created internally. + +#### **5. Storybook Client Usage** + +```typescript +// ❌ Wrong: Dummy client objects +client: { clientId: "demo_client_id", secretKey: undefined } as ThirdwebClient + +// ✅ Correct: Use configured storyClient +import { storyClient } from "../utils.js"; +client: storyClient +``` + +**Rule**: Use the pre-configured `storyClient` in storybook stories instead of creating dummy client objects. + +#### **6. Design System Spacing** + +```typescript +// ❌ Wrong: Hardcoded px values +style={{ + padding: "8px 16px", + margin: "12px 24px", +}} + +// ✅ Correct: Use spacing constants +import { spacing } from "../../../core/design-system/index.js"; +style={{ + padding: `${spacing.xs} ${spacing.md}`, // 8px 16px + margin: `${spacing.sm} ${spacing.lg}`, // 12px 24px +}} +``` + +**Rule**: Always use spacing constants from the design system instead of hardcoded px values for consistent spacing throughout the application. + +**Available spacing values:** + +- `4xs`: 2px, `3xs`: 4px, `xxs`: 6px, `xs`: 8px, `sm`: 12px, `md`: 16px, `lg`: 24px, `xl`: 32px, `xxl`: 48px, `3xl`: 64px + +**Rule**: Use the simple `useMachine` hook for most cases unless you specifically need the actor pattern for complex state management. + +--- + +### 🚀 **Milestone 7 Achievements** + +✅ **XState v5 Migration**: Modern state management with enhanced TypeScript support +✅ **Component Architecture**: Clean separation of concerns with proper props +✅ **Real Type Integration**: Full SDK type compliance from the start +✅ **Interactive FundWallet**: Production-ready initial screen for fund_wallet mode +✅ **Best Practices**: Follows established thirdweb patterns for chains, clients, and storybook +✅ **Comprehensive Testing**: All builds and tests passing throughout development + +**Result**: A solid foundation for Bridge components using modern patterns, real types, and proper dependency management. + +--- + +## Milestone 7: PaymentSelection Real Data Implementation (COMPLETED) + +### Goals + +- Update PaymentSelection component to show real route data instead of dummy payment methods +- Integrate with Bridge.routes API to fetch available origin tokens for a given destination token +- Display available origin tokens as payment options with proper UI + +### Implementation Summary + +#### 1. Enhanced usePaymentMethods Hook + +- **Updated API**: Now accepts `{ destinationToken: Token, client: ThirdwebClient }` +- **Real Data Fetching**: Uses `useQuery` to fetch routes via `Bridge.routes()` API +- **Data Transformation**: Groups routes by origin token to avoid duplicates +- **Return Format**: + - `walletMethods`: Array of origin tokens with route data + - `fiatMethods`: Static fiat payment options + - Standard query state: `isLoading`, `error`, `isSuccess`, etc. + +#### 2. PaymentSelection Component Updates + +- **New Props**: Added required `client: ThirdwebClient` prop +- **Loading States**: Added skeleton loading while fetching routes +- **Real Token Display**: Shows actual origin tokens from Bridge API +- **UI Improvements**: + - Token + chain icons via TokenAndChain component + - Token symbol and chain name display + - Limited to top 5 most popular routes +- **Error Handling**: Proper error propagation via onError callback + +#### 3. Storybook Integration + +- **Updated Stories**: Added required props (destinationToken, client) +- **Multiple Examples**: Different destination tokens (Ethereum USDC, Arbitrum USDC) +- **Proper Theme Handling**: Following established ErrorBanner.stories.tsx pattern + +#### 4. Type Safety Improvements + +- **Chain Handling**: Used `defineChain()` instead of `getCachedChain()` for better type safety +- **Proper Fallbacks**: Chain name with fallback to `Chain ${id}` format +- **PaymentMethod Integration**: Proper creation of wallet payment methods with origin token data + +### Key Learnings Added + +**#7 Chain Type Safety**: When displaying chain names, use `defineChain(chainId)` for better type safety rather than `getCachedChain()` which can return limited chain objects. + +### Technical Verification + +- ✅ Build passing (all TypeScript errors resolved) +- ✅ Proper error handling for API failures +- ✅ Loading states implemented +- ✅ Storybook stories working with real data examples +- ✅ Integration with existing BridgeOrchestrator component + +### Integration Notes + +- **BridgeOrchestrator**: Updated to pass `client` prop to PaymentSelection +- **State Machine**: PaymentSelection properly creates PaymentMethod objects that integrate with existing payment machine +- **Route Data**: Real routes provide origin token information for wallet-based payments +- **Fallback**: Fiat payment option always available regardless of route availability + +--- + +## Milestone 8.1: PaymentSelection 2-Step Flow Refinement (COMPLETED) + +### Goals + +- Refine PaymentSelection component to implement a 2-step user flow +- Step 1: Show connected wallets, connect wallet option, and pay with fiat option +- Step 2a: If wallet selected → show available tokens using usePaymentMethods hook +- Step 2b: If fiat selected → show onramp provider selection (Coinbase, Stripe, Transak) + +### Implementation Summary + +#### 1. 2-Step Flow Architecture + +- **Step Management**: Added internal state management with discriminated union Step type +- **Navigation Logic**: Proper back button handling that adapts to current step +- **Dynamic Titles**: Step-appropriate header titles ("Choose Payment Method" → "Select Token" → "Select Payment Provider") + +#### 2. Step 1: Wallet & Fiat Selection + +- **Connected Wallets Display**: Shows all connected wallets with wallet icons, names, and addresses +- **Connect Another Wallet**: Prominent button with dashed border and plus icon (placeholder for wallet connection modal) +- **Pay with Fiat**: Single option to proceed to onramp provider selection +- **Visual Design**: Consistent button styling with proper theming and spacing + +#### 3. Step 2a: Token Selection (Existing Functionality) + +- **Real Data Integration**: Uses existing usePaymentMethods hook with selected wallet context +- **Loading States**: Skeleton loading while fetching available routes +- **Token Display**: Shows origin tokens with amounts, balances, and proper token/chain icons +- **Empty States**: Helpful messaging when no tokens available with guidance to try different wallet + +#### 4. Step 2b: Fiat Provider Selection + +- **Three Providers**: Coinbase, Stripe, and Transak options +- **Provider Branding**: Custom colored containers with provider initials (temporary until real icons added) +- **Provider Descriptions**: Brief descriptive text for each provider +- **PaymentMethod Creation**: Proper creation of fiat PaymentMethod objects with selected provider + +#### 5. Technical Implementation + +- **Type Safety**: Proper TypeScript handling for wallet selection and payment method creation +- **Error Handling**: Graceful error handling with proper user feedback +- **Hook Integration**: Seamless integration with existing usePaymentMethods, useConnectedWallets, and useActiveWallet hooks +- **State Management**: Clean internal state management without affecting parent components + +#### 6. Storybook Updates + +- **Enhanced Documentation**: Comprehensive descriptions of the 2-step flow +- **Multiple Stories**: Examples showcasing different scenarios and configurations +- **Story Descriptions**: Detailed explanations of each step and interaction flow +- **Theme Support**: Full light/dark theme support with proper backgrounds + +### Key Features Implemented + +✅ **Connected Wallets Display**: Shows all connected wallets with proper identification +✅ **Connect Wallet Integration**: Placeholder for wallet connection modal integration +✅ **Fiat Provider Selection**: Full onramp provider selection (Coinbase, Stripe, Transak) +✅ **Dynamic Navigation**: Step-aware back button and title handling +✅ **Real Token Integration**: Uses existing usePaymentMethods hook for token selection +✅ **Loading & Error States**: Proper loading states and error handling throughout +✅ **Type Safety**: Full TypeScript compliance with proper error handling +✅ **Storybook Documentation**: Comprehensive stories showcasing the full flow + +### Integration Notes + +- **BridgeOrchestrator**: No changes needed - already passes required `client` prop +- **Payment Machine**: PaymentSelection creates proper PaymentMethod objects that integrate seamlessly +- **Existing Hooks**: Leverages useConnectedWallets, useActiveWallet, and usePaymentMethods without modifications +- **Theme System**: Uses existing design system tokens and follows established patterns + +### Technical Verification + +- ✅ Build passing (all TypeScript errors resolved) +- ✅ Proper error handling for wallet selection and payment method creation +- ✅ Loading states implemented for token fetching +- ✅ Storybook stories working with enhanced documentation +- ✅ Integration with existing state machine and components + +**Result**: A polished 2-step payment selection flow that provides clear wallet and fiat payment options while maintaining seamless integration with the existing Bridge system architecture. + +--- + +## 🏗️ Milestone 8 · Tier-2 Composite Screens + +TECH_SPEC §8.3 + +### Tasks (put all new components in src/react/web/ui/Bridge) + +- [x] Fetch available origin tokens when destination token is selected +- [x] `PaymentSelection`- show list of available origin tokens and fiat payment method. +- [x] `RoutePreview` – shows hops, fees, ETA, provider logos ◦ props `{ route, onConfirm, onBack }`. +- [x] update `PaymentSelection` to show a 2 step screen - first screen shows the list of connected wallets, a button to connect another wallet, and a pay with debit card button. If clicking a wallet -> goes into showing the list of tokens available using the usePaymentMethods hooks (what we show right now), if clicking pay with debit card - shows a button for each onramp provider we have: "coinbase", "stripe" and "transak" +- [x] `StepRunner` – Handle different types of BridgePrepareResult quotes. All crypto quotes will have explicit 'steps' with transactions to execute. There is a special case for onramp, where we need to FIRST do the onramp (open payment link, poll for status) and THEN execute the transactions inside 'steps' (steps can be empty array as well). +- [x] `SuccessScreen` – final receipt view with success icon & simple icon animation. +- [x] `ErrorBanner` – inline banner with retry handler ◦ props `{ error, onRetry }`. + +Acceptance ✅: Storybook stories interactive; tests pass (`pnpm test:dev composite`). + +--- + +## 🚦 Milestone 9 · Tier-3 Flow Components + +TECH_SPEC §8.4 + +### Tasks + +- [x] `` – uses passed in token; destination = connected wallet. +- [x] `` – adds seller address prop & summary line. +- [ ] `` – accepts preparedTransaction with value or erc20Value; signs + broadcasts at end of the flow. +- [ ] Provide factory helpers (`createBridgeOrchestratorFlow()` etc.) for tests. +- [ ] Flow tests: ensure correct sequence of screens for happy path. + +Acceptance ✅: Flows render & test pass (`pnpm test:dev flows`) in Storybook. + +--- + +## 📦 Milestone 10 · `` Widget Container + +TECH_SPEC §8.5 + +### Tasks + +- [ ] Implement `BridgeEmbed.tsx` that selects one of the three flows by `mode` prop. +- [ ] Ensure prop surface matches legacy `` (same names & defaults). +- [ ] Internally inject platform-specific default adapters via `BridgeEmbedProvider`. +- [ ] Storybook example embedding widget inside modal. + +Acceptance ✅: Legacy integration tests pass unchanged. + +--- + +## 🧪 Milestone 11 · Cross-Layer Testing & Coverage + +TECH_SPEC §10, §14 + +### Tasks + +- [ ] Reach ≥90 % unit test coverage for `core/` & `web/flows/`. +- [ ] Add Chromatic visual regression run for all components. +- [ ] Playwright integration tests for Web dummy dApp (happy path & retry). +- [ ] Detox smoke test for RN widget. + +Acceptance ✅: CI coverage (`pnpm test:dev --coverage`) & E2E jobs green. + +--- + +## 🚀 Milestone 12 · CI, Linting & Release Prep + +### Tasks + +- [ ] Extend GitHub Actions to include size-limit check. +- [ ] Add `format:check` script using Biome; ensure pipeline runs `biome check --apply`. +- [ ] Generate `CHANGELOG.md` entry for ` diff --git a/packages/thirdweb/src/react/TECH_SPEC.md b/packages/thirdweb/src/react/TECH_SPEC.md new file mode 100644 index 00000000000..b7864229914 --- /dev/null +++ b/packages/thirdweb/src/react/TECH_SPEC.md @@ -0,0 +1,322 @@ +# BridgeEmbed 2.0 — **Technical Specification** + +**Version:** 1.0 +**Updated:** 30 May 2025 +**Author:** Engineering / Architecture Team, thirdweb + +--- + +## 1 · Overview + +BridgeEmbed 2.0 is a **cross-platform payment and asset-bridging widget** that replaces PayEmbed while unlocking multi-hop bridging, token swaps, and fiat on-ramping. +This document describes the **technical architecture, folder structure, component catalogue, hooks, utilities, error strategy, theming, and testing philosophy** required to implement the product specification (`PRODUCT.md`). +It is written for junior-to-mid engineers new to the codebase, with explicit naming conventions and patterns to follow. + +Key principles: + +- **Single shared business-logic layer** (`core/`) reused by Web and React Native. +- **Dependency inversion** for all platform-specific interactions (window pop-ups, deeplinks, analytics, etc.). +- **Strict component layering:** low-level primitives → composite UI → flow components → widget. +- **Typed errors & deterministic state machine** for predictable retries and resumability. +- **100 % test coverage of critical Web paths**, colocated unit tests, and XState model tests. +- **Zero global React context** — all dependencies are passed explicitly via **props** (prop-drilling) to maximise traceability and testability. + +--- + +## 2 · Folder Structure + +``` +packages/thirdweb/src/react/ +├─ core/ # Shared TypeScript logic +│ ├─ hooks/ # React hooks (pure, no platform code) +│ ├─ machines/ # State-machine definitions (XState) +│ ├─ utils/ # Pure helpers (formatting, math, caches) +│ ├─ errors/ # Typed error classes & factories +│ ├─ types/ # Shared types & interfaces (re-exported from `bridge/`) +│ └─ adapters/ # Dependency-inversion interfaces & default impls +├─ web/ # DOM-specific UI +│ ├─ components/ # **Only** low-level primitives live here (already present) +│ └─ flows/ # Composite & flow components (to be created) +├─ native/ # React Native UI +│ ├─ components/ # RN low-level primitives (already present) +│ └─ flows/ # Composite & flow components (to be created) +└─ TECH_SPEC.md # <–– ***YOU ARE HERE*** +``` + +### 2.1 Naming & Mirroring Rules + +- Every file created under `web/flows/` must have a 1-for-1 counterpart under `native/flows/` with identical **name, export, and test file**. +- Shared logic **never** imports from `web/` or `native/`. Platform layers may import from `core/`. +- Test files live next to the SUT (`Something.test.tsx`). Jest is configured for `web` & `native` targets. + +--- + +## 3 · External Dependencies + +The widget consumes the **Bridge SDK** located **in the same monorepo** (`packages/thirdweb/src/bridge`). **Always import via relative paths** to retain bundle-tooling benefits and avoid accidental external resolution: + +```ts +// ✅ Correct – relative import from react/core files +import * as Bridge from "../../bridge/index.js"; + +// ❌ Never do this +import * as Bridge from "thirdweb/bridge"; +``` + +Only the following Bridge namespace members are consumed directly in hooks; all others remain encapsulated: + +- `Bridge.routes()` — path-finding & quote generation +- `Bridge.status()` — polling of prepared routes / actions +- `Bridge.Buy.prepare / Bridge.Sell.prepare / Bridge.Transfer.prepare / Bridge.Onramp.prepare` — executed inside `useBridgePrepare` +- `Bridge.chains()` — one-time chain metadata cache + +Types imported & re-exported in `core/types/`: + +- `Quote`, `PreparedQuote`, `Route`, `RouteStep`, `RouteQuoteStep`, `RouteTransaction`, `Status`, `Token`, `Chain`, `ApiError`, `Action`. + +--- + +## 4 · Architecture & State Management + +### 4.1 State Machine (`machines/paymentMachine.ts`) + +We use **XState 5** to model the end-to-end flow. The machine is **platform-agnostic**, receives adapters via **context**, and exposes typed events/actions consumed by hooks. + +States: + +1. `resolveRequirements` → derive destination chain/token/amount. +2. `methodSelection` → user picks payment method. +3. `quote` → fetch quotes via `useBridgeRoutes`. +4. `preview` → show `RoutePreview`; wait for confirmation. +5. `prepare` → sign & prepare via `useBridgePrepare`. +6. `execute` → run sequenced steps with `useStepExecutor`. +7. `success` → route completed; show `SuccessScreen`. +8. `error` → sub-state handling (`retryable`, `fatal`). + +Each state stores a **canonical snapshot** in localStorage / AsyncStorage (`core/utils/persist.ts`) so the flow can resume if the modal closes unexpectedly. + +### 4.2 Dependency Injection via Props (No Context) + +Rather than React context, **every component receives its dependencies through props**. + +These props are threaded down to all child flows and low-level components. Shared hooks accept an `options` parameter containing references to the same adapters so that hooks remain pure and testable. + +--- + +## 5 · Hooks + +All hooks use **React Query**—`useQuery` for data-fetching, `useMutation` for state-changing actions. The `queryClient` instance is provided by the host application; BridgeEmbed does **not** create its own provider. + +| Hook | Query / Mutation | Behaviour | +| ------------------------- | ---------------- | ------------------------------------------------------------------------------------ | +| `usePaymentMethods()` | `useQuery` | Detects available payment methods. | +| `useBridgeRoutes(params)` | `useQuery` | Fetch & cache routes; auto-retries. | +| `useBridgePrepare(route)` | `useMutation` | Prepares on-chain steps. | +| `useStepExecutor(steps)` | `useMutation` | Executes steps sequentially; internally uses `useQuery` polling for `Bridge.status`. | +| `useBridgeError()` | pure fn | Normalises errors. | + +> **Batching & Auto-execution:** Inside `useStepExecutor` we inspect `account.sendBatchTransaction` and `isInAppSigner` (see _Execution Optimisations_ §9) to minimise user confirmations. + +--- + +## 6 · Error Handling + +``` +class BridgeError extends Error { + code: "NETWORK" | "INSUFFICIENT_FUNDS" | "USER_REJECTED" | "UNKNOWN" | ... ; + data?: unknown; + retry: () => Promise; +} +``` + +- For every Bridge SDK error we map to a domain error code in `core/errors/mapBridgeError.ts`. +- The `.retry()` function is **bound** to the failing action & machine snapshot. UI components always expose a **Retry** CTA. +- Errors bubble up to the provider's `onError?(e)` callback for host app logging. + +--- + +## 7 · Theme, Design Tokens & Styling + +- Use `useCustomTheme()` from existing catalog; it returns `{ colors, typography, radius, spacing, iconSize }`. +- **Never hard-code sizes**; use constants `FONT_SIZE.md`, `ICON_SIZE.lg`, `RADIUS.default`, etc. (Existing tokens live in `web/components/basic.tsx` & friends.) +- Composite components accept optional `className` / `style` overrides but **no inline colour overrides** to preserve theme integrity. +- All Web styles use **CSS-in-JS (emotion)** already configured. RN uses `StyleSheet.create`. + +--- + +## 8 · Component Catalogue + +We now break the catalogue into **three layers**: + +1. **Tier-0 Primitives** – Already present (`Container`, `Text`, `Button`, `Spinner`, `Icon`, etc.) plus prebuilt rows. +2. **Tier-1 Building Blocks** – Small, reusable composites (new): `TokenRow`, `WalletRow`, `ChainRow`, `StepConnectorArrow`, etc. +3. **Tier-2 Composite Screens** – `PaymentMethodSelector`, `RoutePreview`, `StepRunner`, `ErrorBanner`, `SuccessScreen`. +4. **Tier-3 Flows** – ``, ``, ``. +5. **Tier-4 Widget** – `` (mode selector). + +#### 8.1 Tier-0 Primitives (existing & prebuilt) + +| Category | Web Source | RN Source | +| ------------- | ------------------------------------------------- | ------------------------ | +| Layout | `components/Container.tsx` | `components/view.tsx` | +| Typography | `components/text.tsx` | `components/text.tsx` | +| Spacing | `components/Spacer.tsx` | `components/spacer.tsx` | +| Icons | `components/ChainIcon.tsx`, `TokenIcon.tsx`, etc. | same | +| Buttons | `components/buttons.tsx` | `components/button.tsx` | +| Prebuilt rows | `web/ui/prebuilt/*/*` | `native/ui/prebuilt/*/*` | + +#### 8.2 Tier-1 Building Blocks (new) + +| Component | Purpose | +| ---------------------------- | --------------------------------------------- | +| `TokenRow` | Show token icon, symbol, amount. | +| `WalletRow` (already exists) | Display address / ENS & chain. | +| `ChainRow` | Chain icon + name badge. | +| `StepIndicator` | Visual status (pending / completed / failed). | + +These live under `web/flows/building-blocks/` and mirrored in `native/...`. + +#### 8.3 Tier-2 Composite Screens + +| Component | File | Props | Notes | +| ----------------------- | --------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------ | +| `PaymentMethodSelector` | `PaymentMethodSelector.tsx` | `{ methods: PaymentMethod[]; onSelect(m: PaymentMethod): void }` | Web = dropdown list; RN = ActionSheet. | +| `RoutePreview` | `RoutePreview.tsx` | `{ route: Route; onConfirm(): void; onBack(): void }` | Shows hops, fees, ETA, fiat cost, provider logos. | +| `StepRunner` | `StepRunner.tsx` | `{ steps: RouteStep[]; onComplete(): void; onError(e): void }` | Horizontal bar (Web) / vertical list (RN). | +| `ErrorBanner` | `ErrorBanner.tsx` | `{ error: BridgeError; onRetry(): void }` | Inline banner under modal header. | +| `SuccessScreen` | `SuccessScreen.tsx` | `{ receipt: PreparedQuote; onClose(): void }` | Confetti 🎉 emitted via adapter to avoid DOM coupling. | + +All composites import low-level primitives only; never call Bridge SDK directly. + +#### 8.4 Tier-3 Flow Components + +| Name | Mode | Description | +| ------------------------ | ------------------ | ------------------------------------------------------------------------- | +| `` | `"fund_wallet"` | Simplest flow; destination = connected wallet. | +| `` | `"direct_payment"` | Adds seller address prop; shows seller summary in preview. | +| `` | `"transaction"` | Accepts serialized transaction & `erc20Value`; signs & broadcasts at end. | + +Each exports both named component **and** factory: `createBridgeOrchestratorFlow(config)`. Factories are helpful for test stubs. + +### 8.5 Widget Container + +`BridgeEmbed` is **presentation-agnostic**. It renders the selected flow inline; host apps decide whether to house it in a modal, drawer, or page: + +```tsx +import { BridgeEmbed } from "thirdweb/react"; + +function Checkout() { + return ( + + + + ); +} +``` + +No platform-specific modal logic is embedded. + +--- + +## 9 · Execution Optimisations + +To minimise user confirmations: + +1. **In-App Signer Automation** – If `isInAppSigner({ wallet })` returns `true`, `useStepExecutor` automatically calls `submit()` for each prepared step as soon as the previous one succeeds; no UI prompt is rendered. +2. **Batching Transactions** – When `account.sendBatchTransaction` exists and all pending actions are on the **same chain**, hooks combine the ERC-20 `approve` and primary swap/bridge transaction into a single batched request, mirroring logic from `OnRampScreen.tsx` (`canBatch`). + +Both optimisations emit analytics events (`trackPayEvent`) reflecting whether automation/batching was used. + +--- + +## 10 · Testing Strategy (Web-Only) + +- **Unit tests (Jest + Testing Library)** for shared hooks (`core/hooks`) and Web components (`web/flows`). +- **Component snapshots** via Storybook for Tier-1 & Tier-2 composites. +- **State-machine model tests** validate all transitions using XState testing utils. + +React Native components are **not** covered by automated tests in this phase. + +--- + +## 11 · Build, Lint, Format + +- **Biome** (`biome.json`) handles linting _and_ formatting. CI runs `biome check --apply`. +- Tree-shaking: ensure `core/` stays framework-free; use `export type`. +- Package exports configured per platform in `package.json#exports`. + +--- + +## 12 · CI & Linting + +- ESLint & Prettier already configured. Rules: **no-unused-vars**, strict-null-checks. +- GitHub Actions pipeline runs: `pnpm test && pnpm build && pnpm format:check`. +- Add **bundle-size check** for `BridgeEmbed` via `size-limit`. + +--- + +## 13 · Dependency Inversion & Adapters + +Create interfaces in `core/adapters/` so shared code never touches platform APIs. + +| Interface | Methods | Default Web Impl | RN Impl | +| ----------------- | ---------------------------------- | ------------------------------ | ----------------------- | +| `WindowAdapter` | `open(url: string): Promise` | `window.open()` | `Linking.openURL()` | +| `SignerAdapter` | `sign(tx): Promise` | Injected from ethers.js wallet | WalletConnect signer | +| `StorageAdapter` | `get`, `set`, `delete` | `localStorage` | `AsyncStorage` | +| `ConfettiAdapter` | `fire(): void` | canvas-confetti | `react-native-confetti` | + +Adapters are provided via `BridgeEmbedProvider`; defaults are determined by platform entry file. + +--- + +## 14 · Testing Strategy + +- **Unit tests** for every hook & util using Jest + `@testing-library/react` + - Hooks: mock all adapters via `createTestContext`. + - Error mapping: snapshot test codes ↔ messages. +- **Component tests** for every composite UI using Storybook stories as fixtures. +- **State-machine model tests** in `core/machines/__tests__/paymentMachine.test.ts` covering all happy & error paths. +- Web widget **integration tests** with Playwright launching a dummy dApp. +- RN widget **E2E tests** with Detox. + +Test files are named `.test.ts(x)` and live **next to** their source. + +--- + +## 15 · Build, Packaging & Tree-Shaking + +- The React package already emits ESM + CJS builds. Ensure new files use `export type` to avoid type erasure overhead. +- `core/` must have **zero React JSX** so it can be tree-shaken for non-widget consumers (e.g., just hooks). +- Web & RN entry points defined in `package.json#exports`. + +--- + +## 16 · CI & Linting + +- ESLint & Prettier already configured. Rules: **no-unused-vars**, strict-null-checks. +- GitHub Actions pipeline runs: `pnpm test && pnpm build && pnpm format:check`. +- Add **bundle-size check** for `BridgeEmbed` via `size-limit`. + +--- + +## 17 · Execution Minimisation + +To minimise user confirmations: + +1. **In-App Signer Automation** – If `isInAppSigner({ wallet })` returns `true`, `useStepExecutor` automatically calls `submit()` for each prepared step as soon as the previous one succeeds; no UI prompt is rendered. +2. **Batching Transactions** – When `account.sendBatchTransaction` exists and all pending actions are on the **same chain**, hooks combine the ERC-20 `approve` and primary swap/bridge transaction into a single batched request, mirroring logic from `OnRampScreen.tsx` (`canBatch`). + +Both optimisations emit analytics events (`trackPayEvent`) reflecting whether automation/batching was used. + +--- + +## 18 · Future Work + +- Ledger & Trezor hardware-wallet support via new `SignerAdapter`. +- Dynamic gas-sponsor integration (meta-tx) in `useBridgePrepare`. +- Accessibility audit; ARIA attributes & screen-reader flow. + +--- + +> **Contact**: #bridge-embed-engineering Slack channel for questions or PR reviews. diff --git a/packages/thirdweb/src/react/components.md b/packages/thirdweb/src/react/components.md new file mode 100644 index 00000000000..7c47a245182 --- /dev/null +++ b/packages/thirdweb/src/react/components.md @@ -0,0 +1,134 @@ +# Web UI Components Catalog + +This document catalogs the UI components found within `packages/thirdweb/src/react/web/ui`. + +## Core Components (`packages/thirdweb/src/react/web/ui/components`) + +| Component | Occurrences | +| ------------------- | ----------- | +| Container | 100+ | +| Text | 93 | +| Spacer | 85 | +| Button | 58 | +| Skeleton | 40 | +| ModalHeader | 40 | +| Spinner | 31 | +| Img | 31 | +| Line | 28 | +| ChainIcon | 19 | +| TokenIcon | 16 | +| Input | 11 | +| SwitchNetworkButton | 10 | +| WalletImage | 11 | +| ToolTip | 6 | +| Drawer | 5 | +| QRCode | 5 | +| CopyIcon | 4 | +| ChainActiveDot | 3 | +| Label | 3 | +| ModalTitle | 3 | +| TextDivider | 3 | +| DynamicHeight | 3 | +| StepBar | 2 | +| IconContainer | 2 | +| OTPInput | 2 | +| ChainName | 1 | +| BackButton | 1 | +| IconButton | 1 | +| ButtonLink | 1 | +| Overlay | 1 | +| Tabs | 1 | +| FadeIn | 0 | +| InputContainer | 0 | + +## Prebuilt Components (`packages/thirdweb/src/react/web/ui/prebuilt`) + +### NFT + +| Component | Occurrences (internal) | +| -------------- | ---------------------- | +| NFTName | 0 | +| NFTMedia | 0 | +| NFTDescription | 0 | +| NFTProvider | 0 | + +### Account + +| Component | Occurrences (internal) | +| -------------- | ---------------------- | +| AccountBalance | 6 | +| AccountAvatar | 2 | +| AccountBlobbie | 4 | +| AccountName | 2 | +| AccountAddress | 4 | + +### Chain + +| Component | Occurrences (internal) | +| ------------- | ---------------------- | +| ChainName | 5 | +| ChainIcon | 7 | +| ChainProvider | 2 | + +### Token + +| Component | Occurrences (internal) | +| ------------- | ---------------------- | +| TokenName | 0 | +| TokenSymbol | 12 | +| TokenIcon | 7 | +| TokenProvider | 0 | + +### Wallet + +| Component | Occurrences (internal) | +| ---------- | ---------------------- | +| WalletName | 0 | +| WalletIcon | 0 | + +### Thirdweb + +| Component | Occurrences (internal) | +| ------------------------- | ---------------------- | +| ClaimButton | 0 | +| BuyDirectListingButton | 0 | +| CreateDirectListingButton | 0 | + +## Re-used Components (`packages/thirdweb/src/react/web/ui`) + +### Non-Core/Non-Prebuilt Components (ConnectWallet folder analysis) + +| Component | Occurrences | Source/Type | +| ------------------------------ | ----------- | ---------------------------- | +| LoadingScreen | 19 | Wallets shared component | +| Suspense | 8 | React built-in | +| WalletRow | 8 | Buy/swap utility component | +| PoweredByThirdweb | 6 | Custom branding component | +| Modal | 5 | Core UI component | +| WalletUIStatesProvider | 4 | Wallet state management | +| NetworkSelectorContent | 4 | Network selection component | +| PayTokenIcon | 3 | Buy screen utility component | +| FiatValue | 3 | Buy/swap utility component | +| TOS | 3 | Terms of service component | +| ErrorState | 3 | Error handling component | +| AnimatedButton | 3 | Animation component | +| ConnectModalContent | 3 | Modal content layout | +| AnyWalletConnectUI | 2 | Wallet connection screen | +| SmartConnectUI | 2 | Smart wallet connection UI | +| WalletEntryButton | 2 | Wallet selection button | +| TokenSelector | 2 | Token selection component | +| SignatureScreen | 2 | Wallet signature screen | +| WalletSwitcherConnectionScreen | 2 | Wallet switching UI | +| ErrorText | 2 | Error display component | +| SwapSummary | 2 | Swap transaction summary | +| EstimatedTimeAndFees | 2 | Transaction info component | + +### Other Re-used Components + +| Component | Occurrences | +| --------- | ----------- | +| PayEmbed | 1 | +| SiteEmbed | 0 | +| SiteLink | 0 | + +**Note:** Occurrences are based on direct import and usage (e.g., `; +} diff --git a/packages/thirdweb/src/react/core/errors/.keep b/packages/thirdweb/src/react/core/errors/.keep new file mode 100644 index 00000000000..fa0e58ded98 --- /dev/null +++ b/packages/thirdweb/src/react/core/errors/.keep @@ -0,0 +1,2 @@ +# Placeholder file to maintain directory structure +# This directory will contain error mapping and normalization utilities \ No newline at end of file diff --git a/packages/thirdweb/src/react/core/errors/mapBridgeError.test.ts b/packages/thirdweb/src/react/core/errors/mapBridgeError.test.ts new file mode 100644 index 00000000000..57c2d357243 --- /dev/null +++ b/packages/thirdweb/src/react/core/errors/mapBridgeError.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from "vitest"; +import { ApiError } from "../../../bridge/types/Errors.js"; +import { isRetryable, mapBridgeError } from "./mapBridgeError.js"; + +describe("mapBridgeError", () => { + it("should return the same error for INVALID_INPUT", () => { + const error = new ApiError({ + code: "INVALID_INPUT", + message: "Invalid input provided", + statusCode: 400, + correlationId: "test-correlation-id", + }); + + const result = mapBridgeError(error); + + expect(result).toBe(error); + expect(result.code).toBe("INVALID_INPUT"); + expect(result.message).toBe("Invalid input provided"); + expect(result.statusCode).toBe(400); + expect(result.correlationId).toBe("test-correlation-id"); + }); + + it("should return the same error for INTERNAL_SERVER_ERROR", () => { + const error = new ApiError({ + code: "INTERNAL_SERVER_ERROR", + message: "Internal server error occurred", + statusCode: 500, + correlationId: "internal-error-id", + }); + + const result = mapBridgeError(error); + + expect(result).toBe(error); + expect(result.code).toBe("INTERNAL_SERVER_ERROR"); + expect(result.message).toBe("Internal server error occurred"); + expect(result.statusCode).toBe(500); + expect(result.correlationId).toBe("internal-error-id"); + }); + + it("should return the same error for ROUTE_NOT_FOUND", () => { + const error = new ApiError({ + code: "ROUTE_NOT_FOUND", + message: "No route found for the requested parameters", + statusCode: 404, + }); + + const result = mapBridgeError(error); + + expect(result).toBe(error); + expect(result.code).toBe("ROUTE_NOT_FOUND"); + expect(result.message).toBe("No route found for the requested parameters"); + expect(result.statusCode).toBe(404); + expect(result.correlationId).toBeUndefined(); + }); + + it("should return the same error for AMOUNT_TOO_LOW", () => { + const error = new ApiError({ + code: "AMOUNT_TOO_LOW", + message: "Amount is below minimum threshold", + statusCode: 400, + correlationId: "amount-validation-id", + }); + + const result = mapBridgeError(error); + + expect(result).toBe(error); + expect(result.code).toBe("AMOUNT_TOO_LOW"); + expect(result.message).toBe("Amount is below minimum threshold"); + expect(result.statusCode).toBe(400); + expect(result.correlationId).toBe("amount-validation-id"); + }); +}); + +describe("isRetryable", () => { + it("should return true for INTERNAL_SERVER_ERROR", () => { + expect(isRetryable("INTERNAL_SERVER_ERROR")).toBe(true); + }); + + it("should return true for UNKNOWN_ERROR", () => { + expect(isRetryable("UNKNOWN_ERROR")).toBe(true); + }); + + it("should return false for INVALID_INPUT", () => { + expect(isRetryable("INVALID_INPUT")).toBe(false); + }); + + it("should return false for ROUTE_NOT_FOUND", () => { + expect(isRetryable("ROUTE_NOT_FOUND")).toBe(false); + }); + + it("should return false for AMOUNT_TOO_LOW", () => { + expect(isRetryable("AMOUNT_TOO_LOW")).toBe(false); + }); + + it("should return false for AMOUNT_TOO_HIGH", () => { + expect(isRetryable("AMOUNT_TOO_HIGH")).toBe(false); + }); +}); diff --git a/packages/thirdweb/src/react/core/errors/mapBridgeError.ts b/packages/thirdweb/src/react/core/errors/mapBridgeError.ts new file mode 100644 index 00000000000..d9159410fe3 --- /dev/null +++ b/packages/thirdweb/src/react/core/errors/mapBridgeError.ts @@ -0,0 +1,25 @@ +import type { ApiError } from "../../../bridge/types/Errors.js"; + +/** + * Maps raw ApiError instances from the Bridge SDK into UI-friendly domain errors. + * Currently returns the same error; will evolve to provide better user-facing messages. + * + * @param e - The raw ApiError from the Bridge SDK + * @returns The mapped ApiError (currently unchanged) + */ +export function mapBridgeError(e: ApiError): ApiError { + // For now, return the same error + // TODO: This will evolve to provide better user-facing error messages + return e; +} + +/** + * Determines if an error code represents a retryable error condition. + * + * @param code - The error code from ApiError + * @returns true if the error is retryable, false otherwise + */ +export function isRetryable(code: ApiError["code"]): boolean { + // Treat INTERNAL_SERVER_ERROR & UNKNOWN_ERROR as retryable + return code === "INTERNAL_SERVER_ERROR" || code === "UNKNOWN_ERROR"; +} diff --git a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts index aa2b40bef3e..56f70df16ca 100644 --- a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts +++ b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts @@ -25,40 +25,42 @@ import type { } from "../../utils/defaultTokens.js"; import type { SiweAuthOptions } from "../auth/useSiweAuth.js"; -export type PaymentInfo = { - /** - * The chain to receive the payment on. - */ - chain: Chain; - /** - * The address of the seller wallet to receive the payment on. - */ - sellerAddress: string; - /** - * Optional ERC20 token to receive the payment on. - * If not provided, the native token will be used. - */ - token?: TokenInfo; - /** - * For direct transfers, specify who will pay the transfer fee. Can be "sender" or "receiver". - */ - feePayer?: "sender" | "receiver"; -} & ( - | { - /** - * The amount of tokens to receive in ETH or tokens. - * ex: 0.1 ETH or 100 USDC - */ - amount: string; - } - | { - /** - * The amount of tokens to receive in wei. - * ex: 1000000000000000000 wei - */ - amountWei: bigint; - } -); +export type PaymentInfo = Prettify< + { + /** + * The chain to receive the payment on. + */ + chain: Chain; + /** + * The address of the seller wallet to receive the payment on. + */ + sellerAddress: string; + /** + * Optional ERC20 token to receive the payment on. + * If not provided, the native token will be used. + */ + token?: Partial & { address: string }; + /** + * For direct transfers, specify who will pay the transfer fee. Can be "sender" or "receiver". + */ + feePayer?: "sender" | "receiver"; + } & ( + | { + /** + * The amount of tokens to receive in ETH or tokens. + * ex: 0.1 ETH or 100 USDC + */ + amount: string; + } + | { + /** + * The amount of tokens to receive in wei. + * ex: 1000000000000000000 wei + */ + amountWei: bigint; + } + ) +>; export type PayUIOptions = Prettify< { @@ -78,7 +80,7 @@ export type PayUIOptions = Prettify< testMode?: boolean; prefillSource?: { chain: Chain; - token?: TokenInfo; + token?: Partial & { address: string }; allowEdits?: { token: boolean; chain: boolean; @@ -115,7 +117,8 @@ export type PayUIOptions = Prettify< * Callback to be called when the user successfully completes the purchase. */ onPurchaseSuccess?: ( - info: + // TODO: remove this type from the callback entirely or adapt it from the new format + info?: | { type: "crypto"; status: BuyWithCryptoStatus; @@ -135,6 +138,7 @@ export type PayUIOptions = Prettify< */ metadata?: { name?: string; + description?: string; image?: string; }; @@ -160,13 +164,14 @@ export type FundWalletOptions = { */ prefillBuy?: { chain: Chain; - token?: TokenInfo; + token?: Partial & { address: string }; amount?: string; allowEdits?: { amount: boolean; token: boolean; chain: boolean; }; + presetOptions?: [number, number, number]; }; }; diff --git a/packages/thirdweb/src/react/core/hooks/others/useChainQuery.ts b/packages/thirdweb/src/react/core/hooks/others/useChainQuery.ts index 4cda3eab0e9..0eb36c2f7f0 100644 --- a/packages/thirdweb/src/react/core/hooks/others/useChainQuery.ts +++ b/packages/thirdweb/src/react/core/hooks/others/useChainQuery.ts @@ -137,7 +137,7 @@ export function useChainExplorers(chain?: Chain) { function getQueryOptions(chain?: Chain) { return { - queryKey: ["chain", chain], + queryKey: ["chain", chain?.id], enabled: !!chain, staleTime: 1000 * 60 * 60, // 1 hour } as const; diff --git a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts new file mode 100644 index 00000000000..78e1b104bd4 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts @@ -0,0 +1,100 @@ +import { type UseQueryOptions, useQueries } from "@tanstack/react-query"; +import { prepare as prepareOnramp } from "../../../../bridge/Onramp.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import { getToken } from "../../../../pay/convert/get-token.js"; +import type { Address } from "../../../../utils/address.js"; +import { toUnits } from "../../../../utils/units.js"; + +/** + * @internal + */ +export type UseBuyWithFiatQuotesForProvidersParams = { + /** + * A client is the entry point to the thirdweb SDK. + */ + client: ThirdwebClient; + /** + * The destination chain ID. + */ + chainId: number; + /** + * The destination token address. + */ + tokenAddress: Address; + /** + * The address that will receive the tokens. + */ + receiver: Address; + /** + * The desired token amount in wei. + */ + amount: string; + /** + * The fiat currency (e.g., "USD"). Defaults to "USD". + */ + currency?: string; +}; + +/** + * @internal + */ +export type OnrampQuoteQueryOptions = Omit< + UseQueryOptions>>, + "queryFn" | "queryKey" | "enabled" +>; + +/** + * @internal + */ +export type UseBuyWithFiatQuotesForProvidersResult = { + data: Awaited> | undefined; + isLoading: boolean; + error: Error | null; + isError: boolean; + isSuccess: boolean; +}[]; + +/** + * @internal + * Hook to get prepared onramp quotes from Coinbase, Stripe, and Transak providers. + */ +export function useBuyWithFiatQuotesForProviders( + params?: UseBuyWithFiatQuotesForProvidersParams, + queryOptions?: OnrampQuoteQueryOptions, +): UseBuyWithFiatQuotesForProvidersResult { + const providers = ["coinbase", "stripe", "transak"] as const; + + const queries = useQueries({ + queries: providers.map((provider) => ({ + ...queryOptions, + queryKey: ["onramp-prepare", provider, params], + queryFn: async () => { + if (!params) { + throw new Error("No params provided"); + } + + const token = await getToken( + params.client, + params.tokenAddress, + params.chainId, + ); + + const amountWei = toUnits(params.amount, token.decimals); + + return prepareOnramp({ + client: params.client, + onramp: provider, + chainId: params.chainId, + tokenAddress: params.tokenAddress, + receiver: params.receiver, + amount: amountWei, + currency: params.currency || "USD", + }); + }, + enabled: !!params, + retry: false, + })), + }); + + return queries; +} diff --git a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts index 78fbcfa3e60..8498ec5ff31 100644 --- a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts +++ b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts @@ -70,7 +70,7 @@ export type SendTransactionPayModalConfig = * Callback to be called when the user successfully completes the purchase. */ onPurchaseSuccess?: ( - info: + info?: | { type: "crypto"; status: BuyWithCryptoStatus; diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts b/packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts new file mode 100644 index 00000000000..b47fa3b8654 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from "vitest"; +import { ApiError } from "../../../bridge/types/Errors.js"; +import { useBridgeError } from "./useBridgeError.js"; + +describe("useBridgeError", () => { + it("should handle null error", () => { + const result = useBridgeError({ error: null }); + + expect(result).toEqual({ + mappedError: null, + isRetryable: false, + userMessage: "", + errorCode: null, + statusCode: null, + isClientError: false, + isServerError: false, + }); + }); + + it("should handle undefined error", () => { + const result = useBridgeError({ error: undefined }); + + expect(result).toEqual({ + mappedError: null, + isRetryable: false, + userMessage: "", + errorCode: null, + statusCode: null, + isClientError: false, + isServerError: false, + }); + }); + + it("should process ApiError correctly", () => { + const apiError = new ApiError({ + code: "INVALID_INPUT", + message: "Invalid parameters provided", + statusCode: 400, + }); + + const result = useBridgeError({ error: apiError }); + + expect(result.mappedError).toBeInstanceOf(ApiError); + expect(result.errorCode).toBe("INVALID_INPUT"); + expect(result.statusCode).toBe(400); + expect(result.isClientError).toBe(true); + expect(result.isServerError).toBe(false); + expect(result.isRetryable).toBe(false); // INVALID_INPUT is not retryable + expect(result.userMessage).toBe( + "Invalid input provided. Please check your parameters and try again.", + ); + }); + + it("should convert generic Error to ApiError", () => { + const genericError = new Error("Network connection failed"); + + const result = useBridgeError({ error: genericError }); + + expect(result.mappedError).toBeInstanceOf(ApiError); + expect(result.errorCode).toBe("UNKNOWN_ERROR"); + expect(result.statusCode).toBe(500); + expect(result.isClientError).toBe(false); + expect(result.isServerError).toBe(true); + expect(result.isRetryable).toBe(true); + expect(result.userMessage).toBe( + "An unexpected error occurred. Please try again.", + ); + }); + + it("should identify server errors correctly", () => { + const serverError = new ApiError({ + code: "INTERNAL_SERVER_ERROR", + message: "Server error", + statusCode: 500, + }); + + const result = useBridgeError({ error: serverError }); + + expect(result.statusCode).toBe(500); + expect(result.isClientError).toBe(false); + expect(result.isServerError).toBe(true); + expect(result.isRetryable).toBe(true); // INTERNAL_SERVER_ERROR is retryable + expect(result.userMessage).toBe( + "A temporary error occurred. Please try again in a moment.", + ); + }); + + it("should provide user-friendly messages for known error codes", () => { + // Test INVALID_INPUT + const invalidInputError = new ApiError({ + code: "INVALID_INPUT", + message: "Technical error message", + statusCode: 400, + }); + const invalidInputResult = useBridgeError({ error: invalidInputError }); + expect(invalidInputResult.userMessage).toBe( + "Invalid input provided. Please check your parameters and try again.", + ); + + // Test INTERNAL_SERVER_ERROR + const serverError = new ApiError({ + code: "INTERNAL_SERVER_ERROR", + message: "Technical error message", + statusCode: 500, + }); + const serverResult = useBridgeError({ error: serverError }); + expect(serverResult.userMessage).toBe( + "A temporary error occurred. Please try again in a moment.", + ); + }); + + it("should use original error message for unknown error codes", () => { + const unknownError = new ApiError({ + code: "UNKNOWN_ERROR", + message: "Custom error message", + statusCode: 418, + }); + + const result = useBridgeError({ error: unknownError }); + + expect(result.userMessage).toBe( + "An unexpected error occurred. Please try again.", + ); + expect(result.errorCode).toBe("UNKNOWN_ERROR"); + }); + + it("should detect client vs server errors correctly", () => { + // Client error (4xx) + const clientError = new ApiError({ + code: "INVALID_INPUT", + message: "Bad request", + statusCode: 400, + }); + + const clientResult = useBridgeError({ error: clientError }); + expect(clientResult.isClientError).toBe(true); + expect(clientResult.isServerError).toBe(false); + + // Server error (5xx) + const serverError = new ApiError({ + code: "INTERNAL_SERVER_ERROR", + message: "Internal error", + statusCode: 503, + }); + + const serverResult = useBridgeError({ error: serverError }); + expect(serverResult.isClientError).toBe(false); + expect(serverResult.isServerError).toBe(true); + + // No status code + const noStatusError = new ApiError({ + code: "UNKNOWN_ERROR", + message: "Unknown error", + statusCode: 500, + }); + + const noStatusResult = useBridgeError({ error: noStatusError }); + expect(noStatusResult.isClientError).toBe(false); + expect(noStatusResult.isServerError).toBe(true); // 500 is a server error + }); + + it("should handle Error without message", () => { + const errorWithoutMessage = new Error(); + + const result = useBridgeError({ error: errorWithoutMessage }); + + expect(result.userMessage).toBe( + "An unexpected error occurred. Please try again.", + ); + expect(result.errorCode).toBe("UNKNOWN_ERROR"); + }); +}); diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeError.ts b/packages/thirdweb/src/react/core/hooks/useBridgeError.ts new file mode 100644 index 00000000000..3857f4d7a16 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgeError.ts @@ -0,0 +1,149 @@ +import { ApiError } from "../../../bridge/types/Errors.js"; +import { isRetryable, mapBridgeError } from "../errors/mapBridgeError.js"; + +/** + * Parameters for the useBridgeError hook + */ +export interface UseBridgeErrorParams { + /** + * The error to process. Can be an ApiError or generic Error. + */ + error: Error | ApiError | null | undefined; +} + +/** + * Result returned by the useBridgeError hook + */ +export interface UseBridgeErrorResult { + /** + * The mapped/normalized error, null if no error provided + */ + mappedError: ApiError | null; + + /** + * Whether this error can be retried + */ + isRetryable: boolean; + + /** + * User-friendly error message + */ + userMessage: string; + + /** + * Technical error code for debugging + */ + errorCode: string | null; + + /** + * HTTP status code if available + */ + statusCode: number | null; + + /** + * Whether this is a client-side error (4xx) + */ + isClientError: boolean; + + /** + * Whether this is a server-side error (5xx) + */ + isServerError: boolean; +} + +/** + * Hook that processes bridge errors using mapBridgeError and isRetryable + * + * @param params - Parameters containing the error to process + * @returns Processed error information with retry logic and user-friendly messages + * + * @example + * ```tsx + * const { data, error } = useBridgeRoutes({ client, originChainId: 1 }); + * const { + * mappedError, + * isRetryable, + * userMessage, + * isClientError + * } = useBridgeError({ error }); + * + * if (error) { + * return ( + *
+ *

{userMessage}

+ * {isRetryable && } + *
+ * ); + * } + * ``` + */ +export function useBridgeError( + params: UseBridgeErrorParams, +): UseBridgeErrorResult { + const { error } = params; + + // No error case + if (!error) { + return { + mappedError: null, + isRetryable: false, + userMessage: "", + errorCode: null, + statusCode: null, + isClientError: false, + isServerError: false, + }; + } + + // Convert to ApiError if it's not already + let apiError: ApiError; + if (error instanceof ApiError) { + apiError = mapBridgeError(error); + } else { + // Create ApiError from generic Error + apiError = new ApiError({ + code: "UNKNOWN_ERROR", + message: error.message || "An unknown error occurred", + statusCode: 500, // Default for generic errors + }); + } + + const statusCode = apiError.statusCode || null; + const isClientError = + statusCode !== null && statusCode >= 400 && statusCode < 500; + const isServerError = statusCode !== null && statusCode >= 500; + + // Generate user-friendly message based on error code + const userMessage = getUserFriendlyMessage(apiError); + + return { + mappedError: apiError, + isRetryable: isRetryable(apiError.code), + userMessage, + errorCode: apiError.code, + statusCode, + isClientError, + isServerError, + }; +} + +/** + * Converts technical error codes to user-friendly messages + */ +function getUserFriendlyMessage(error: ApiError): string { + switch (error.code) { + case "INVALID_INPUT": + return "Invalid input provided. Please check your parameters and try again."; + case "ROUTE_NOT_FOUND": + return "No route found for this transaction. Please try a different token pair or amount."; + case "AMOUNT_TOO_LOW": + return "The amount is too low for this transaction. Please increase the amount."; + case "AMOUNT_TOO_HIGH": + return "The amount is too high for this transaction. Please decrease the amount."; + case "INTERNAL_SERVER_ERROR": + return "A temporary error occurred. Please try again in a moment."; + default: + // Fallback to the original error message if available + return error.message || "An unexpected error occurred. Please try again."; + } +} diff --git a/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts b/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts new file mode 100644 index 00000000000..5293e528879 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts @@ -0,0 +1,161 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ThirdwebClient } from "../../../client/client.js"; +import type { + BridgePrepareRequest, + UseBridgePrepareParams, +} from "./useBridgePrepare.js"; + +// Mock client +const mockClient = { clientId: "test" } as ThirdwebClient; + +describe("useBridgePrepare", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should have correct type structure for buy prepare request", () => { + const buyRequest: BridgePrepareRequest = { + type: "buy", + client: mockClient, + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 1000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + }; + + expect(buyRequest.type).toBe("buy"); + expect(buyRequest.amount).toBe(1000000n); + expect(buyRequest.client).toBe(mockClient); + }); + + it("should have correct type structure for transfer prepare request", () => { + const transferRequest: BridgePrepareRequest = { + type: "transfer", + client: mockClient, + chainId: 1, + tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + amount: 1000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + }; + + expect(transferRequest.type).toBe("transfer"); + expect(transferRequest.amount).toBe(1000000n); + expect(transferRequest.client).toBe(mockClient); + }); + + it("should have correct type structure for sell prepare request", () => { + const sellRequest: BridgePrepareRequest = { + type: "sell", + client: mockClient, + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 1000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + }; + + expect(sellRequest.type).toBe("sell"); + expect(sellRequest.amount).toBe(1000000n); + expect(sellRequest.client).toBe(mockClient); + }); + + it("should have correct type structure for onramp prepare request", () => { + const onrampRequest: BridgePrepareRequest = { + type: "onramp", + client: mockClient, + onramp: "stripe", + chainId: 137, + tokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + receiver: "0x1234567890123456789012345678901234567890", + amount: 1000000n, + }; + + expect(onrampRequest.type).toBe("onramp"); + expect(onrampRequest.amount).toBe(1000000n); + expect(onrampRequest.client).toBe(mockClient); + }); + + it("should handle UseBridgePrepareParams with enabled option", () => { + const params: UseBridgePrepareParams = { + type: "buy", + client: mockClient, + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 1000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + enabled: false, + }; + + expect(params.enabled).toBe(false); + expect(params.type).toBe("buy"); + }); + + it("should have optional enabled parameter", () => { + const paramsWithoutEnabled: UseBridgePrepareParams = { + type: "transfer", + client: mockClient, + chainId: 1, + tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + amount: 1000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + }; + + expect(paramsWithoutEnabled.enabled).toBeUndefined(); // Should be optional + expect(paramsWithoutEnabled.type).toBe("transfer"); + }); + + it("should correctly discriminate between different prepare request types", () => { + const buyRequest: BridgePrepareRequest = { + type: "buy", + client: mockClient, + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 1000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + }; + + // Type narrowing should work + if (buyRequest.type === "buy") { + expect(buyRequest.sender).toBe( + "0x1234567890123456789012345678901234567890", + ); + expect(buyRequest.receiver).toBe( + "0x1234567890123456789012345678901234567890", + ); + } + + const onrampRequest: BridgePrepareRequest = { + type: "onramp", + client: mockClient, + onramp: "stripe", + chainId: 137, + tokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + receiver: "0x1234567890123456789012345678901234567890", + amount: 1000000n, + }; + + // Type narrowing should work for onramp + if (onrampRequest.type === "onramp") { + expect(onrampRequest.receiver).toBe( + "0x1234567890123456789012345678901234567890", + ); + expect(onrampRequest.tokenAddress).toBe( + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + ); + expect(onrampRequest.onramp).toBe("stripe"); + } + }); +}); diff --git a/packages/thirdweb/src/react/core/hooks/useBridgePrepare.ts b/packages/thirdweb/src/react/core/hooks/useBridgePrepare.ts new file mode 100644 index 00000000000..8ee3c592871 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgePrepare.ts @@ -0,0 +1,133 @@ +import { useQuery } from "@tanstack/react-query"; +import type { prepare as BuyPrepare } from "../../../bridge/Buy.js"; +import type { prepare as OnrampPrepare } from "../../../bridge/Onramp.js"; +import type { prepare as SellPrepare } from "../../../bridge/Sell.js"; +import type { prepare as TransferPrepare } from "../../../bridge/Transfer.js"; +import * as Bridge from "../../../bridge/index.js"; +import { ApiError } from "../../../bridge/types/Errors.js"; +import { stringify } from "../../../utils/json.js"; +import { mapBridgeError } from "../errors/mapBridgeError.js"; + +/** + * Union type for different Bridge prepare request types + */ +export type BridgePrepareRequest = + | ({ type: "buy" } & BuyPrepare.Options) + | ({ type: "sell" } & SellPrepare.Options) + | ({ type: "transfer" } & TransferPrepare.Options) + | ({ type: "onramp" } & OnrampPrepare.Options); + +/** + * Union type for different Bridge prepare result types + */ +export type BridgePrepareResult = + | ({ type: "buy" } & BuyPrepare.Result) + | ({ type: "sell" } & SellPrepare.Result) + | ({ type: "transfer" } & TransferPrepare.Result) + | ({ type: "onramp" } & OnrampPrepare.Result); + +/** + * Parameters for the useBridgePrepare hook + */ +export type UseBridgePrepareParams = BridgePrepareRequest & { + /** + * Whether to enable the query. Useful for conditional fetching. + * @default true + */ + enabled?: boolean; +}; + +/** + * Hook that prepares bridge transactions with caching and retry logic + * + * @param params - Parameters for preparing bridge transactions including type and specific options + * @returns React Query result with prepared transaction data, loading state, and error handling + * + * @example + * ```tsx + * // Buy preparation + * const { data: preparedBuy, isLoading, error } = useBridgePrepare({ + * type: "buy", + * client: thirdwebClient, + * originChainId: 1, + * originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + * destinationChainId: 137, + * destinationTokenAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + * amount: parseEther("1"), + * sender: "0x...", + * receiver: "0x..." + * }); + * + * // Transfer preparation + * const { data: preparedTransfer } = useBridgePrepare({ + * type: "transfer", + * client: thirdwebClient, + * originChainId: 1, + * originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + * destinationChainId: 137, + * destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + * amount: 1000000n, + * sender: "0x...", + * receiver: "0x..." + * }); + * ``` + */ +export function useBridgePrepare(params: UseBridgePrepareParams) { + const { enabled = true, type, ...prepareParams } = params; + + return useQuery({ + queryKey: ["bridge-prepare", type, stringify(prepareParams)], + queryFn: async (): Promise => { + switch (type) { + case "buy": { + const result = await Bridge.Buy.prepare( + prepareParams as BuyPrepare.Options, + ); + return { type: "buy", ...result }; + } + case "sell": { + const result = await Bridge.Sell.prepare( + prepareParams as SellPrepare.Options, + ); + return { type: "sell", ...result }; + } + case "transfer": { + const result = await Bridge.Transfer.prepare( + prepareParams as TransferPrepare.Options, + ); + return { type: "transfer", ...result }; + } + case "onramp": { + const result = await Bridge.Onramp.prepare( + prepareParams as OnrampPrepare.Options, + ); + return { type: "onramp", ...result }; + } + default: + throw new Error(`Unsupported bridge prepare type: ${type}`); + } + }, + enabled: enabled && !!prepareParams.client, + staleTime: 2 * 60 * 1000, // 2 minutes - prepared quotes have shorter validity + gcTime: 5 * 60 * 1000, // 5 minutes garbage collection + retry: (failureCount, error) => { + // Handle both ApiError and generic Error instances + if (error instanceof ApiError) { + const bridgeError = mapBridgeError(error); + + // Don't retry on client-side errors (4xx) + if ( + bridgeError.statusCode && + bridgeError.statusCode >= 400 && + bridgeError.statusCode < 500 + ) { + return false; + } + } + + // Retry up to 2 times for prepared quotes (they're more time-sensitive) + return failureCount < 2; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // Exponential backoff, max 10s + }); +} diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts b/packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts new file mode 100644 index 00000000000..278840af8a8 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts @@ -0,0 +1,73 @@ +"use client"; +import { useQuery } from "@tanstack/react-query"; +import * as Buy from "../../../bridge/Buy.js"; +import * as Transfer from "../../../bridge/Transfer.js"; +import type { Token } from "../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { checksumAddress } from "../../../utils/address.js"; + +export interface UseBridgeQuoteParams { + originToken: Token; + destinationToken: Token; + destinationAmount: bigint; + client: ThirdwebClient; + enabled?: boolean; +} + +export type BridgeQuoteResult = NonNullable< + ReturnType["data"] +>; + +export function useBridgeQuote({ + originToken, + destinationToken, + destinationAmount, + client, + enabled = true, +}: UseBridgeQuoteParams) { + return useQuery({ + queryKey: [ + "bridge-quote", + originToken.chainId, + originToken.address, + destinationToken.chainId, + destinationToken.address, + destinationAmount.toString(), + ], + queryFn: async () => { + // if ssame token and chain, use transfer + if ( + checksumAddress(originToken.address) === + checksumAddress(destinationToken.address) && + originToken.chainId === destinationToken.chainId + ) { + const transfer = await Transfer.prepare({ + client, + chainId: originToken.chainId, + tokenAddress: originToken.address, + sender: originToken.address, + receiver: destinationToken.address, + amount: destinationAmount, + }); + return transfer; + } + + console.log("AMOUNT", destinationAmount); + const quote = await Buy.quote({ + originChainId: originToken.chainId, + originTokenAddress: originToken.address, + destinationChainId: destinationToken.chainId, + destinationTokenAddress: destinationToken.address, + amount: destinationAmount, + client, + }); + + return quote; + }, + enabled: + enabled && !!originToken && !!destinationToken && !!destinationAmount, + staleTime: 30000, // 30 seconds + refetchInterval: 60000, // 1 minute + retry: 3, + }); +} diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeRoutes.test.ts b/packages/thirdweb/src/react/core/hooks/useBridgeRoutes.test.ts new file mode 100644 index 00000000000..8a539cbe9ee --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgeRoutes.test.ts @@ -0,0 +1,137 @@ +import { + type MockedFunction, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { routes } from "../../../bridge/Routes.js"; +import { ApiError } from "../../../bridge/types/Errors.js"; +import type { Route } from "../../../bridge/types/Route.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import type { UseBridgeRoutesParams } from "./useBridgeRoutes.js"; + +// Mock the Bridge routes function +vi.mock("../../../bridge/Routes.js", () => ({ + routes: vi.fn(), +})); + +const mockRoutes = routes as MockedFunction; + +// Mock client +const mockClient = { clientId: "test" } as ThirdwebClient; + +// Mock route data +const mockRouteData: Route[] = [ + { + originToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + symbol: "ETH", + name: "Ethereum", + decimals: 18, + priceUsd: 2000.0, + }, + destinationToken: { + chainId: 137, + address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + symbol: "WETH", + name: "Wrapped Ethereum", + decimals: 18, + priceUsd: 2000.0, + }, + }, +]; + +describe("useBridgeRoutes", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should export correct hook parameters type", () => { + // Type-only test to verify UseBridgeRoutesParams interface + const params: UseBridgeRoutesParams = { + client: mockClient, + originChainId: 1, + destinationChainId: 137, + enabled: true, + }; + + expect(params).toBeDefined(); + expect(params.client).toBe(mockClient); + expect(params.originChainId).toBe(1); + expect(params.destinationChainId).toBe(137); + expect(params.enabled).toBe(true); + }); + + it("should handle different parameter combinations", () => { + const fullParams: UseBridgeRoutesParams = { + client: mockClient, + originChainId: 1, + originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + destinationChainId: 137, + destinationTokenAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + maxSteps: 3, + sortBy: "popularity", + limit: 10, + offset: 0, + enabled: false, + }; + + expect(fullParams).toBeDefined(); + expect(fullParams.sortBy).toBe("popularity"); + expect(fullParams.maxSteps).toBe(3); + expect(fullParams.limit).toBe(10); + expect(fullParams.offset).toBe(0); + }); + + it("should have optional enabled parameter defaulting to true", () => { + const paramsWithoutEnabled: UseBridgeRoutesParams = { + client: mockClient, + originChainId: 1, + destinationChainId: 137, + }; + + expect(paramsWithoutEnabled.enabled).toBeUndefined(); // Should be optional + }); + + it("should validate that Bridge.routes would be called with correct parameters", async () => { + const testParams = { + client: mockClient, + originChainId: 1, + destinationChainId: 137, + originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as const, + }; + + // Mock the routes function to return our test data + mockRoutes.mockResolvedValue(mockRouteData); + + // Directly call the routes function to verify it works with our parameters + const result = await routes(testParams); + + expect(mockRoutes).toHaveBeenCalledWith(testParams); + expect(result).toEqual(mockRouteData); + }); + + it("should handle API errors properly", async () => { + const apiError = new ApiError({ + code: "INVALID_INPUT", + message: "Invalid parameters", + statusCode: 400, + }); + + mockRoutes.mockRejectedValue(apiError); + + try { + await routes({ + client: mockClient, + originChainId: 1, + destinationChainId: 137, + }); + } catch (error) { + expect(error).toBe(apiError); + expect(error).toBeInstanceOf(ApiError); + } + }); +}); diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeRoutes.ts b/packages/thirdweb/src/react/core/hooks/useBridgeRoutes.ts new file mode 100644 index 00000000000..39eddea54f8 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgeRoutes.ts @@ -0,0 +1,75 @@ +import { useQuery } from "@tanstack/react-query"; +import { routes } from "../../../bridge/Routes.js"; +import type { routes as RoutesTypes } from "../../../bridge/Routes.js"; +import { ApiError } from "../../../bridge/types/Errors.js"; +import { mapBridgeError } from "../errors/mapBridgeError.js"; + +/** + * Parameters for the useBridgeRoutes hook + */ +export type UseBridgeRoutesParams = RoutesTypes.Options & { + /** + * Whether to enable the query. Useful for conditional fetching. + * @default true + */ + enabled?: boolean; +}; + +/** + * Hook that fetches available bridge routes with caching and retry logic + * + * @param params - Parameters for fetching routes including client and filter options + * @returns React Query result with routes data, loading state, and error handling + * + * @example + * ```tsx + * const { data: routes, isLoading, error } = useBridgeRoutes({ + * client: thirdwebClient, + * originChainId: 1, + * destinationChainId: 137, + * originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + * }); + * ``` + */ +export function useBridgeRoutes(params: UseBridgeRoutesParams) { + const { enabled = true, ...routeParams } = params; + + return useQuery({ + queryKey: [ + "bridge-routes", + { + originChainId: routeParams.originChainId, + originTokenAddress: routeParams.originTokenAddress, + destinationChainId: routeParams.destinationChainId, + destinationTokenAddress: routeParams.destinationTokenAddress, + maxSteps: routeParams.maxSteps, + sortBy: routeParams.sortBy, + limit: routeParams.limit, + offset: routeParams.offset, + }, + ], + queryFn: () => routes(routeParams), + enabled: enabled && !!routeParams.client, + staleTime: 5 * 60 * 1000, // 5 minutes - routes are relatively stable + gcTime: 10 * 60 * 1000, // 10 minutes garbage collection + retry: (failureCount, error) => { + // Handle both ApiError and generic Error instances + if (error instanceof ApiError) { + const bridgeError = mapBridgeError(error); + + // Don't retry on client-side errors (4xx) + if ( + bridgeError.statusCode && + bridgeError.statusCode >= 400 && + bridgeError.statusCode < 500 + ) { + return false; + } + } + + // Retry up to 3 times for server errors or network issues + return failureCount < 3; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff, max 30s + }); +} diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts new file mode 100644 index 00000000000..92a68f5da1e --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts @@ -0,0 +1,336 @@ +/** + * @vitest-environment happy-dom + */ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import { createElement } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { routes } from "../../../bridge/Routes.js"; +import type { Token } from "../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { usePaymentMethods } from "./usePaymentMethods.js"; + +// Mock the routes API +vi.mock("../../../bridge/Routes.js", () => ({ + routes: vi.fn(), +})); + +const mockRoutes = vi.mocked(routes); + +// Mock data +const mockDestinationToken: Token = { + chainId: 1, + address: "0xA0b86a33E6441aA7A6fbEEc9bb27e5e8bc3b8eD7", + decimals: 6, + symbol: "USDC", + name: "USD Coin", + priceUsd: 1.0, +}; + +const mockClient = { + clientId: "test-client-id", +} as ThirdwebClient; + +const mockRouteData = [ + { + originToken: { + chainId: 1, + address: "0xA0b86a33E6441aA7A6fbEEc9bb27e5e8bc3b8eD7", + decimals: 18, + symbol: "ETH", + name: "Ethereum", + priceUsd: 2000, + }, + destinationToken: mockDestinationToken, + steps: [], + }, + { + originToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + decimals: 6, + symbol: "USDC", + name: "USD Coin", + priceUsd: 1.0, + }, + destinationToken: mockDestinationToken, + steps: [], + }, + { + originToken: { + chainId: 42161, + address: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + decimals: 6, + symbol: "USDC", + name: "USD Coin", + priceUsd: 1.0, + }, + destinationToken: mockDestinationToken, + steps: [], + }, +]; + +// Test wrapper component +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return ({ children }: { children: React.ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children); +}; + +describe("usePaymentMethods", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should require destinationToken and client parameters", () => { + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + expect(result.current).toBeDefined(); + expect(result.current.isLoading).toBe(true); + }); + + it("should fetch routes and transform data correctly", async () => { + mockRoutes.mockResolvedValueOnce(mockRouteData); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + // Initially loading + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toEqual([]); + + // Wait for query to resolve + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Should have transformed data + expect(result.current.data).toHaveLength(4); // 3 wallet methods + 1 fiat method + + const walletMethod = result.current.data[0]; + expect(walletMethod?.type).toBe("wallet"); + if (walletMethod?.type === "wallet") { + expect(walletMethod.originToken).toEqual(mockRouteData[0]?.originToken); + } + + const fiatMethod = result.current.data[3]; + expect(fiatMethod?.type).toBe("fiat"); + if (fiatMethod?.type === "fiat") { + expect(fiatMethod.currency).toBe("USD"); + } + }); + + it("should call routes API with correct parameters", async () => { + mockRoutes.mockResolvedValueOnce(mockRouteData); + const wrapper = createWrapper(); + + renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(mockRoutes).toHaveBeenCalledWith({ + client: mockClient, + destinationChainId: mockDestinationToken.chainId, + destinationTokenAddress: mockDestinationToken.address, + sortBy: "popularity", + limit: 50, + }); + }); + }); + + it("should handle empty routes data", async () => { + mockRoutes.mockResolvedValueOnce([]); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Should only have fiat method when no routes + expect(result.current.data).toHaveLength(1); + expect(result.current.data[0]).toEqual({ + type: "fiat", + currency: "USD", + }); + }); + + it("should handle API errors gracefully", async () => { + const mockError = new Error("API Error"); + mockRoutes.mockRejectedValueOnce(mockError); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toBeTruthy(); + expect(result.current.data).toEqual([]); + }); + + it("should deduplicate origin tokens", async () => { + // Mock data with duplicate origin tokens + const firstRoute = mockRouteData[0]; + if (!firstRoute) { + throw new Error("Mock data is invalid"); + } + + const mockDataWithDuplicates = [ + ...mockRouteData, + { + originToken: firstRoute.originToken, // Duplicate ETH + destinationToken: mockDestinationToken, + steps: [], + }, + ]; + + mockRoutes.mockResolvedValueOnce(mockDataWithDuplicates); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Should still have only 4 methods (3 unique wallet + 1 fiat) + expect(result.current.data).toHaveLength(4); + + // Check that ETH only appears once + const walletMethods = result.current.data.filter( + (m) => m.type === "wallet", + ); + const ethMethods = walletMethods.filter( + (m) => m.type === "wallet" && m.originToken?.symbol === "ETH", + ); + expect(ethMethods).toHaveLength(1); + }); + + it("should always include fiat payment option", async () => { + mockRoutes.mockResolvedValueOnce(mockRouteData); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const fiatMethods = result.current.data.filter((m) => m.type === "fiat"); + expect(fiatMethods).toHaveLength(1); + expect(fiatMethods[0]).toEqual({ + type: "fiat", + currency: "USD", + }); + }); + + it("should have correct query key for caching", async () => { + mockRoutes.mockResolvedValueOnce(mockRouteData); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + // The hook should use a query key that includes chain ID and token address + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(mockRoutes).toHaveBeenCalledTimes(1); + }); + + it("should provide refetch functionality", async () => { + mockRoutes.mockResolvedValueOnce(mockRouteData); + const wrapper = createWrapper(); + + const { result } = renderHook( + () => + usePaymentMethods({ + destinationToken: mockDestinationToken, + destinationAmount: "1", + client: mockClient, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(typeof result.current.refetch).toBe("function"); + }); +}); diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts new file mode 100644 index 00000000000..1083259071a --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts @@ -0,0 +1,203 @@ +import { useQuery } from "@tanstack/react-query"; +import { routes } from "../../../bridge/Routes.js"; +import type { Token } from "../../../bridge/types/Token.js"; +import { getCachedChain } from "../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { isInsightEnabled } from "../../../insight/common.js"; +import { getOwnedTokens } from "../../../insight/get-tokens.js"; +import { toTokens } from "../../../utils/units.js"; +import type { Wallet } from "../../../wallets/interfaces/wallet.js"; +import type { PaymentMethod } from "../machines/paymentMachine.js"; +import { useActiveWallet } from "./wallets/useActiveWallet.js"; + +type OwnedTokenWithQuote = { + originToken: Token; + balance: bigint; + originAmount: bigint; +}; + +/** + * Hook that returns available payment methods for BridgeEmbed + * Fetches real routes data based on the destination token + * + * @param options - Configuration options + * @param options.destinationToken - The destination token to find routes for + * @param options.client - ThirdwebClient for API calls + * @returns Available payment methods with route data + * + * @example + * ```tsx + * const { data: paymentMethods, isLoading, error } = usePaymentMethods({ + * destinationToken, + * client + * }); + * ``` + */ +export function usePaymentMethods(options: { + destinationToken: Token; + destinationAmount: string; + client: ThirdwebClient; + payerWallet?: Wallet; + includeDestinationToken?: boolean; +}) { + const { + destinationToken, + destinationAmount, + client, + payerWallet, + includeDestinationToken, + } = options; + const localWallet = useActiveWallet(); // TODO (bridge): get all connected wallets + const wallet = payerWallet || localWallet; + + const routesQuery = useQuery({ + queryKey: [ + "bridge-routes", + destinationToken.chainId, + destinationToken.address, + destinationAmount, + payerWallet?.getAccount()?.address, + includeDestinationToken, + ], + queryFn: async (): Promise => { + if (!wallet) { + throw new Error("No wallet connected"); + } + const allRoutes = await routes({ + client, + destinationChainId: destinationToken.chainId, + destinationTokenAddress: destinationToken.address, + sortBy: "popularity", + includePrices: true, + maxSteps: 3, + limit: 100, // Get top 100 most popular routes + }); + + const allOriginTokens = includeDestinationToken + ? [destinationToken, ...allRoutes.map((route) => route.originToken)] + : allRoutes.map((route) => route.originToken); + + // 1. Resolve all unique chains in the supported token map + const uniqueChains = Array.from( + new Set(allOriginTokens.map((t) => t.chainId)), + ); + + // 2. Check insight availability once per chain + const insightSupport = await Promise.all( + uniqueChains.map(async (c) => ({ + chain: getCachedChain(c), + enabled: await isInsightEnabled(getCachedChain(c)), + })), + ); + const insightEnabledChains = insightSupport.filter((c) => c.enabled); + + // 3. ERC-20 balances for insight-enabled chains (batched 5 chains / call) + let owned: OwnedTokenWithQuote[] = []; + let page = 0; + const limit = 100; + + while (true) { + const batch = await getOwnedTokens({ + ownerAddress: wallet.getAccount()?.address || "", + chains: insightEnabledChains.map((c) => c.chain), + client, + queryOptions: { + limit, + page, + metadata: "false", + }, + }); + + if (batch.length === 0) { + break; + } + + // find matching origin token in allRoutes + const tokensWithBalance = batch + .map((b) => ({ + originToken: allOriginTokens.find( + (t) => + t.address.toLowerCase() === b.tokenAddress.toLowerCase() && + t.chainId === b.chainId, + ), + balance: b.value, + originAmount: 0n, + })) + .filter((t) => !!t.originToken) as OwnedTokenWithQuote[]; + + owned = [...owned, ...tokensWithBalance]; + page += 1; + } + + const requiredDollarAmount = + Number.parseFloat(destinationAmount) * destinationToken.priceUsd; + + // sort by dollar balance descending + owned.sort((a, b) => { + const aDollarBalance = + Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) * + a.originToken.priceUsd; + const bDollarBalance = + Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) * + b.originToken.priceUsd; + return bDollarBalance - aDollarBalance; + }); + + const suitableOriginTokens: OwnedTokenWithQuote[] = []; + + for (const b of owned) { + if (b.originToken && b.balance > 0n) { + const dollarBalance = + Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) * + b.originToken.priceUsd; + if (b.originToken.priceUsd && dollarBalance < requiredDollarAmount) { + continue; + } + + if ( + includeDestinationToken && + b.originToken.address.toLowerCase() === + destinationToken.address.toLowerCase() && + b.originToken.chainId === destinationToken.chainId + ) { + // add same token to the front of the list + suitableOriginTokens.unshift({ + balance: b.balance, + originAmount: 0n, + originToken: b.originToken, + }); + continue; + } + + suitableOriginTokens.push({ + balance: b.balance, + originAmount: 0n, + originToken: b.originToken, + }); + } + } + + const transformedRoutes = [ + ...suitableOriginTokens.map((s) => ({ + type: "wallet" as const, + payerWallet: wallet, + originToken: s.originToken, + balance: s.balance, + })), + ]; + return transformedRoutes; + }, + staleTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: false, + enabled: !!wallet, + }); + + return { + data: routesQuery.data || [], + isLoading: routesQuery.isLoading, + error: routesQuery.error, + isError: routesQuery.isError, + isSuccess: routesQuery.isSuccess, + refetch: routesQuery.refetch, + }; +} diff --git a/packages/thirdweb/src/react/core/hooks/useStepExecutor.test.ts b/packages/thirdweb/src/react/core/hooks/useStepExecutor.test.ts new file mode 100644 index 00000000000..feff04b3115 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useStepExecutor.test.ts @@ -0,0 +1,806 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Action } from "../../../bridge/types/BridgeAction.js"; +import type { RouteStep } from "../../../bridge/types/Route.js"; +import { defineChain } from "../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { + buyWithApprovalQuote, + complexBuyQuote, + onrampWithSwapsQuote, + simpleBuyQuote, + simpleOnrampQuote, +} from "../../../stories/Bridge/fixtures.js"; +import type { Wallet } from "../../../wallets/interfaces/wallet.js"; +import type { WindowAdapter } from "../adapters/WindowAdapter.js"; +import type { BridgePrepareResult } from "./useBridgePrepare.js"; +import type { StepExecutorOptions } from "./useStepExecutor.js"; +import { flattenRouteSteps, useStepExecutor } from "./useStepExecutor.js"; + +// Mock React hooks +vi.mock("react", async () => { + const actual = await vi.importActual("react"); + return { + ...actual, + useState: vi.fn((initial) => { + let state = initial; + return [ + state, + (newState: typeof initial) => { + state = typeof newState === "function" ? newState(state) : newState; + }, + ]; + }), + useCallback: vi.fn((fn) => fn), + useMemo: vi.fn((fn) => fn()), + useRef: vi.fn((initial) => ({ current: initial })), + useEffect: vi.fn((fn) => fn()), + }; +}); + +// Mock modules +vi.mock("../../../transaction/prepare-transaction.js", () => ({ + prepareTransaction: vi.fn((options) => ({ + ...options, + type: "prepared", + })), +})); + +vi.mock("../../../transaction/actions/send-transaction.js", () => ({ + sendTransaction: vi.fn(), +})); + +vi.mock("../../../transaction/actions/send-batch-transaction.js", () => ({ + sendBatchTransaction: vi.fn(), +})); + +vi.mock("../../../transaction/actions/wait-for-tx-receipt.js", () => ({ + waitForReceipt: vi.fn(), +})); + +vi.mock("../../../bridge/Status.js", () => ({ + status: vi.fn(), +})); + +vi.mock("../../../bridge/index.js", () => ({ + Onramp: { + status: vi.fn(), + }, +})); + +vi.mock("../errors/mapBridgeError.js", () => ({ + isRetryable: vi.fn( + (code: string) => + code === "INTERNAL_SERVER_ERROR" || code === "UNKNOWN_ERROR", + ), +})); + +// Test helpers +const mockClient: ThirdwebClient = { + clientId: "test-client", + secretKey: undefined, +} as ThirdwebClient; + +const mockWindowAdapter: WindowAdapter = { + open: vi.fn(), +}; + +const createMockWallet = (hasAccount = true, supportsBatch = false): Wallet => { + const mockAccount = hasAccount + ? { + address: "0x1234567890123456789012345678901234567890", + sendTransaction: vi.fn(), + sendBatchTransaction: supportsBatch ? vi.fn() : undefined, + signMessage: vi.fn(), + signTypedData: vi.fn(), + } + : undefined; + + return { + id: "test-wallet", + getAccount: () => mockAccount, + getChain: vi.fn(), + autoConnect: vi.fn(), + connect: vi.fn(), + disconnect: vi.fn(), + switchChain: vi.fn(), + subscribe: vi.fn(), + getConfig: () => ({}), + } as unknown as Wallet; +}; + +const createMockRouteSteps = ( + stepCount = 2, + txPerStep = 2, + includeApproval = true, +): RouteStep[] => { + const steps: RouteStep[] = []; + + for (let i = 0; i < stepCount; i++) { + const transactions = []; + for (let j = 0; j < txPerStep; j++) { + transactions.push({ + id: `0x${i}${j}` as `0x${string}`, + action: (includeApproval && i === 0 && j === 0 + ? "approval" + : "transfer") as Action, + to: "0xabcdef1234567890123456789012345678901234" as `0x${string}`, + data: `0x${i}${j}data` as `0x${string}`, + value: j === 0 ? 1000000000000000000n : undefined, + chainId: i === 0 ? 1 : 137, // Different chains for different steps + chain: i === 0 ? defineChain(1) : defineChain(137), + client: mockClient, + }); + } + + steps.push({ + originToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + destinationToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + originAmount: 1000000n, + destinationAmount: 999000n, + estimatedExecutionTimeMs: 60000, + transactions, + }); + } + + // Modify steps to have all transactions on the same chain + if (steps[0]?.transactions) { + for (const tx of steps[0].transactions) { + tx.chainId = 1; + tx.chain = defineChain(1); + } + } + if (steps[1]?.transactions) { + for (const tx of steps[1].transactions) { + tx.chainId = 1; + tx.chain = defineChain(1); + } + } + + return steps; +}; + +const createMockBuyQuote = (steps: RouteStep[]): BridgePrepareResult => ({ + type: "buy", + originAmount: 1000000000000000000n, + destinationAmount: 999000000000000000n, + timestamp: Date.now(), + estimatedExecutionTimeMs: 120000, + steps, + intent: { + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 999000000000000000n, + sender: "0x1234567890123456789012345678901234567890", + receiver: "0x1234567890123456789012345678901234567890", + }, +}); + +describe("useStepExecutor", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("flattenRouteSteps", () => { + it("should flatten route steps into linear transaction array", () => { + const steps = createMockRouteSteps(2, 2); + const flattened = flattenRouteSteps(steps); + + expect(flattened).toHaveLength(4); + expect(flattened[0]?._index).toBe(0); + expect(flattened[0]?._stepIndex).toBe(0); + expect(flattened[2]?._index).toBe(2); + expect(flattened[2]?._stepIndex).toBe(1); + }); + + it("should handle empty steps array", () => { + const flattened = flattenRouteSteps([]); + expect(flattened).toHaveLength(0); + }); + + it("should handle steps with no transactions", () => { + const steps: RouteStep[] = [ + { + originToken: { + chainId: 1, + address: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + destinationToken: { + chainId: 137, + address: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + originAmount: 1000000n, + destinationAmount: 999000n, + estimatedExecutionTimeMs: 60000, + transactions: [], + }, + ]; + + const flattened = flattenRouteSteps(steps); + expect(flattened).toHaveLength(0); + }); + }); + + describe("Simple Buy Quote", () => { + it("should execute simple buy quote successfully", async () => { + const { sendTransaction } = await import( + "../../../transaction/actions/send-transaction.js" + ); + const { status } = await import("../../../bridge/Status.js"); + + const mockSendTransaction = vi.mocked(sendTransaction); + const mockStatus = vi.mocked(status); + + // Setup mocks + mockSendTransaction.mockResolvedValue({ + transactionHash: "0xhash123", + chain: defineChain(1), + client: mockClient, + }); + + mockStatus.mockResolvedValue({ + status: "COMPLETED", + paymentId: "payment-simple", + originAmount: 1000000000000000000n, + destinationAmount: 100000000n, + originChainId: 1, + destinationChainId: 1, + originTokenAddress: + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as `0x${string}`, + destinationTokenAddress: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + originToken: { + chainId: 1, + address: + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as `0x${string}`, + symbol: "ETH", + name: "Ethereum", + decimals: 18, + priceUsd: 2500, + }, + destinationToken: { + chainId: 1, + address: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + sender: "0x1234567890123456789012345678901234567890" as `0x${string}`, + receiver: "0x1234567890123456789012345678901234567890" as `0x${string}`, + transactions: [ + { + chainId: 1, + transactionHash: "0xhash123" as `0x${string}`, + }, + ], + }); + + const wallet = createMockWallet(true, false); + const onComplete = vi.fn(); + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + onComplete, + }; + + const result = useStepExecutor(options); + + // Verify the hook returns the expected structure + expect(result).toHaveProperty("isExecuting"); + expect(result).toHaveProperty("progress"); + expect(result).toHaveProperty("currentStep"); + expect(result).toHaveProperty("start"); + expect(result).toHaveProperty("cancel"); + expect(result).toHaveProperty("retry"); + expect(result).toHaveProperty("error"); + + // Verify initial state + expect(result.isExecuting).toBe(false); + expect(result.progress).toBe(0); + expect(result.currentStep).toBeUndefined(); + expect(result).toHaveProperty("onrampStatus"); + + // The hook should have a start function + expect(typeof result.start).toBe("function"); + }); + }); + + describe("Buy Quote with Approval", () => { + it("should execute buy quote with approval step", async () => { + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: buyWithApprovalQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify the hook handles approval transactions correctly + const flatTxs = flattenRouteSteps(buyWithApprovalQuote.steps); + expect(flatTxs).toHaveLength(2); + expect(flatTxs[0]?.action).toBe("approval"); + expect(flatTxs[1]?.action).toBe("buy"); + + // Verify hook structure + expect(result).toHaveProperty("isExecuting"); + expect(result).toHaveProperty("progress"); + expect(result).toHaveProperty("start"); + expect(result.isExecuting).toBe(false); + expect(result.progress).toBe(0); + }); + }); + + describe("Complex Multi-Step Buy Quote", () => { + it("should handle complex buy quote with multiple steps", async () => { + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: complexBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify the hook can handle complex multi-step quotes + const flatTxs = flattenRouteSteps(complexBuyQuote.steps); + expect(flatTxs).toHaveLength(6); // 3 steps * 2 transactions each + expect(complexBuyQuote.steps).toHaveLength(3); + + // Verify initial state for complex quote + expect(result.progress).toBe(0); + expect(result.isExecuting).toBe(false); + }); + }); + + describe("Batching path", () => { + it("should batch transactions on the same chain when sendBatchTransaction is available", async () => { + const { sendBatchTransaction } = await import( + "../../../transaction/actions/send-batch-transaction.js" + ); + const { status } = await import("../../../bridge/Status.js"); + + const mockSendBatchTransaction = vi.mocked(sendBatchTransaction); + const mockStatus = vi.mocked(status); + + // Setup mocks + mockSendBatchTransaction.mockResolvedValue({ + transactionHash: "0xbatchhash123", + chain: defineChain(1), + client: mockClient, + }); + + mockStatus + .mockResolvedValueOnce({ + status: "PENDING", + paymentId: "payment-batch", + originAmount: 100000000n, + originChainId: 1, + destinationChainId: 137, + originTokenAddress: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + destinationTokenAddress: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as `0x${string}`, + originToken: { + chainId: 1, + address: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + destinationToken: { + chainId: 137, + address: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + sender: "0x1234567890123456789012345678901234567890" as `0x${string}`, + receiver: + "0x1234567890123456789012345678901234567890" as `0x${string}`, + transactions: [], + }) + .mockResolvedValueOnce({ + status: "COMPLETED", + paymentId: "payment-batch", + originAmount: 100000000n, + destinationAmount: 100000000n, + originChainId: 1, + destinationChainId: 137, + originTokenAddress: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + destinationTokenAddress: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as `0x${string}`, + originToken: { + chainId: 1, + address: + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + destinationToken: { + chainId: 137, + address: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as `0x${string}`, + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + sender: "0x1234567890123456789012345678901234567890" as `0x${string}`, + receiver: + "0x1234567890123456789012345678901234567890" as `0x${string}`, + transactions: [ + { + chainId: 1, + transactionHash: "0xbatchhash123" as `0x${string}`, + }, + ], + }); + + const wallet = createMockWallet(true, true); // Supports batch + + const options: StepExecutorOptions = { + preparedQuote: buyWithApprovalQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + await result.start(); + + // Verify batching would be used for same-chain transactions + const account = wallet.getAccount(); + expect(account?.sendBatchTransaction).toBeDefined(); + }); + }); + + describe("Simple Onramp Flow", () => { + it("should handle simple onramp without additional steps", async () => { + const { Onramp } = await import("../../../bridge/index.js"); + + const mockOnrampStatus = vi.mocked(Onramp.status); + + mockOnrampStatus + .mockResolvedValueOnce({ + status: "PENDING", + transactions: [], + }) + .mockResolvedValueOnce({ + status: "COMPLETED", + transactions: [], + }); + + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: simpleOnrampQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify onramp setup + expect(result.onrampStatus).toBe("pending"); + expect(simpleOnrampQuote.type).toBe("onramp"); + expect(simpleOnrampQuote.steps).toHaveLength(0); + + // Verify window adapter is available for opening URLs + expect(mockWindowAdapter.open).toBeDefined(); + }); + }); + + describe("Onramp with Additional Steps", () => { + it("should execute onramp flow before transactions and poll until complete", async () => { + const { Onramp } = await import("../../../bridge/index.js"); + + const mockOnrampStatus = vi.mocked(Onramp.status); + + mockOnrampStatus + .mockResolvedValueOnce({ + status: "PENDING", + transactions: [], + }) + .mockResolvedValueOnce({ + status: "COMPLETED", + transactions: [ + { + chainId: 137, + transactionHash: "0xonramphash123" as `0x${string}`, + }, + ], + }); + + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: onrampWithSwapsQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify onramp with additional steps + expect(result.onrampStatus).toBe("pending"); + expect(onrampWithSwapsQuote.type).toBe("onramp"); + expect(onrampWithSwapsQuote.steps).toHaveLength(2); + + // Verify the transactions in the steps + const flatTxs = flattenRouteSteps(onrampWithSwapsQuote.steps); + expect(flatTxs).toHaveLength(4); // 2 steps * 2 transactions each + + // Verify window adapter will be used for opening onramp URL + expect(mockWindowAdapter.open).toBeDefined(); + }); + }); + + describe("Auto-start execution", () => { + it("should auto-start execution when autoStart is true", async () => { + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + autoStart: true, + }; + + const result = useStepExecutor(options); + + // Verify the hook structure with autoStart option + expect(options.autoStart).toBe(true); + expect(result).toHaveProperty("start"); + expect(result).toHaveProperty("isExecuting"); + + // The hook should handle autoStart internally + // We can't test the actual execution without a real React environment + }); + }); + + describe("Error handling and retries", () => { + it("should handle retryable errors and allow retry", async () => { + const { isRetryable } = await import("../errors/mapBridgeError.js"); + const mockIsRetryable = vi.mocked(isRetryable); + + mockIsRetryable.mockReturnValue(true); + + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify retry function exists and isRetryable is properly mocked + expect(result).toHaveProperty("retry"); + expect(typeof result.retry).toBe("function"); + expect(mockIsRetryable("INTERNAL_SERVER_ERROR")).toBe(true); + }); + + it("should not allow retry for non-retryable errors", async () => { + const { isRetryable } = await import("../errors/mapBridgeError.js"); + const mockIsRetryable = vi.mocked(isRetryable); + + mockIsRetryable.mockReturnValue(false); + + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify non-retryable errors are handled correctly + expect(mockIsRetryable("INVALID_INPUT")).toBe(false); + expect(result).toHaveProperty("retry"); + }); + }); + + describe("Cancellation", () => { + it("should stop polling when cancelled", async () => { + const wallet = createMockWallet(true, false); + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify cancel function exists + expect(result).toHaveProperty("cancel"); + expect(typeof result.cancel).toBe("function"); + }); + + it("should not call onComplete when cancelled", async () => { + const wallet = createMockWallet(true, false); + const onComplete = vi.fn(); + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + onComplete, + }; + + useStepExecutor(options); + + // Verify onComplete callback is configured and accepts completed statuses array + expect(options.onComplete).toBeDefined(); + expect(onComplete).not.toHaveBeenCalled(); + }); + }); + + describe("Edge cases", () => { + it("should handle wallet not connected", async () => { + const wallet = createMockWallet(false); // No account + + const options: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const result = useStepExecutor(options); + + // Verify wallet has no account + expect(wallet.getAccount()).toBeUndefined(); + expect(result).toHaveProperty("error"); + }); + + it("should handle empty steps array", async () => { + const wallet = createMockWallet(true); + const emptyBuyQuote = createMockBuyQuote([]); + const onComplete = vi.fn(); + + const options: StepExecutorOptions = { + preparedQuote: emptyBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + onComplete, + }; + + const result = useStepExecutor(options); + + expect(result.progress).toBe(0); + expect(emptyBuyQuote.steps).toHaveLength(0); + + // Empty steps should result in immediate completion + const flattened = flattenRouteSteps(emptyBuyQuote.steps); + expect(flattened).toHaveLength(0); + }); + + it("should handle progress calculation correctly", async () => { + const wallet = createMockWallet(true); + + // Test with buy quote (no onramp) + const buyOptions: StepExecutorOptions = { + preparedQuote: complexBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const buyResult = useStepExecutor(buyOptions); + expect(buyResult.progress).toBe(0); // No transactions completed yet + expect(buyResult.onrampStatus).toBeUndefined(); + + // Test with onramp quote + const onrampOptions: StepExecutorOptions = { + preparedQuote: simpleOnrampQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const onrampResult = useStepExecutor(onrampOptions); + expect(onrampResult.progress).toBe(0); // No steps completed yet + expect(onrampResult.onrampStatus).toBe("pending"); + }); + }); + + describe("Progress tracking", () => { + it("should calculate progress correctly for different quote types", () => { + const wallet = createMockWallet(true); + + // Test simple buy quote progress + const simpleBuyOptions: StepExecutorOptions = { + preparedQuote: simpleBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const simpleBuyResult = useStepExecutor(simpleBuyOptions); + const simpleBuyFlatTxs = flattenRouteSteps(simpleBuyQuote.steps); + expect(simpleBuyResult.progress).toBe(0); + expect(simpleBuyFlatTxs).toHaveLength(1); + + // Test complex buy quote progress + const complexBuyOptions: StepExecutorOptions = { + preparedQuote: complexBuyQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const complexBuyResult = useStepExecutor(complexBuyOptions); + const complexBuyFlatTxs = flattenRouteSteps(complexBuyQuote.steps); + expect(complexBuyResult.progress).toBe(0); + expect(complexBuyFlatTxs).toHaveLength(6); + + // Test onramp with swaps progress + const onrampSwapsOptions: StepExecutorOptions = { + preparedQuote: onrampWithSwapsQuote, + wallet, + windowAdapter: mockWindowAdapter, + client: mockClient, + }; + + const onrampSwapsResult = useStepExecutor(onrampSwapsOptions); + const onrampSwapsFlatTxs = flattenRouteSteps(onrampWithSwapsQuote.steps); + expect(onrampSwapsResult.progress).toBe(0); + expect(onrampSwapsFlatTxs).toHaveLength(4); + expect(onrampSwapsResult.onrampStatus).toBe("pending"); + }); + }); +}); diff --git a/packages/thirdweb/src/react/core/hooks/useStepExecutor.ts b/packages/thirdweb/src/react/core/hooks/useStepExecutor.ts new file mode 100644 index 00000000000..82b3829f466 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useStepExecutor.ts @@ -0,0 +1,606 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { status as OnrampStatus } from "../../../bridge/OnrampStatus.js"; +import { ApiError } from "../../../bridge/types/Errors.js"; +import type { + RouteStep, + RouteTransaction, +} from "../../../bridge/types/Route.js"; +import type { Status } from "../../../bridge/types/Status.js"; +import { getCachedChain } from "../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { waitForReceipt } from "../../../transaction/actions/wait-for-tx-receipt.js"; +import type { Account, Wallet } from "../../../wallets/interfaces/wallet.js"; +import type { WindowAdapter } from "../adapters/WindowAdapter.js"; +import { + useBridgePrepare, + type BridgePrepareRequest, + type BridgePrepareResult, +} from "./useBridgePrepare.js"; +import { useQuery } from "@tanstack/react-query"; +import { stringify } from "../../../utils/json.js"; + +/** + * Type for completed status results from Bridge.status and Onramp.status + */ +export type CompletedStatusResult = + | ({ type: "buy" } & Extract) + | ({ type: "sell" } & Extract) + | ({ type: "transfer" } & Extract) + | ({ type: "onramp" } & Extract< + OnrampStatus.Result, + { status: "COMPLETED" } + >); + +/** + * Options for the step executor hook + */ +export interface StepExecutorOptions { + /** Prepared quote returned by Bridge.prepare */ + request: BridgePrepareRequest; + /** Wallet instance providing getAccount() & sendTransaction */ + wallet: Wallet; + /** Window adapter for opening on-ramp URLs (web / RN) */ + windowAdapter: WindowAdapter; + /** Thirdweb client for API calls */ + client: ThirdwebClient; + /** Auto start execution as soon as hook mounts */ + autoStart?: boolean; + /** Callback when all steps complete successfully - receives array of all completed status results */ + onComplete?: (completedStatuses: CompletedStatusResult[]) => void; +} + +/** + * Internal flattened transaction type + */ +export interface FlattenedTx extends RouteTransaction { + /** Index in flat array */ + _index: number; + /** Parent step index */ + _stepIndex: number; +} + +/** + * Public return type of useStepExecutor + */ +export interface StepExecutorResult { + currentStep?: RouteStep; + currentTxIndex?: number; + progress: number; // 0–100 + onrampStatus?: "pending" | "executing" | "completed" | "failed"; + executionState: "fetching" | "idle" | "executing" | "auto-starting"; + steps?: RouteStep[]; + error?: ApiError; + start: () => void; + cancel: () => void; + retry: () => void; +} + +/** + * Flatten RouteStep[] into a linear list of transactions preserving ordering & indices. + */ +export function flattenRouteSteps(steps: RouteStep[]): FlattenedTx[] { + const out: FlattenedTx[] = []; + steps.forEach((step, stepIdx) => { + step.transactions?.forEach((tx, _txIdx) => { + out.push({ + ...(tx as RouteTransaction), + _index: out.length, + _stepIndex: stepIdx, + }); + }); + }); + return out; +} + +/** + * Hook that sequentially executes prepared steps. + * NOTE: initial implementation only exposes progress + basic state machine. Actual execution logic will follow in later subtasks. + */ +export function useStepExecutor( + options: StepExecutorOptions, +): StepExecutorResult { + const { + request, + wallet, + windowAdapter, + client, + autoStart = false, + onComplete, + } = options; + + const { data: preparedQuote, isLoading } = useBridgePrepare(request); + + // Flatten all transactions upfront + const flatTxs = useMemo( + () => (preparedQuote?.steps ? flattenRouteSteps(preparedQuote.steps) : []), + [preparedQuote?.steps], + ); + + // State management + const [currentTxIndex, setCurrentTxIndex] = useState( + undefined, + ); + const [executionState, setExecutionState] = useState< + "fetching" | "idle" | "executing" | "auto-starting" + >("idle"); + const [error, setError] = useState(undefined); + const [completedTxs, setCompletedTxs] = useState>(new Set()); + const [onrampStatus, setOnrampStatus] = useState< + "pending" | "executing" | "completed" | "failed" | undefined + >(preparedQuote?.type === "onramp" ? "pending" : undefined); + + useQuery({ + queryKey: [ + "bridge-quote-execution-state", + stringify(preparedQuote?.steps), + isLoading, + ], + queryFn: async () => { + if (!isLoading) { + setExecutionState("idle"); + } else { + setExecutionState("fetching"); + } + return executionState; + }, + }); + + // Cancellation tracking + const abortControllerRef = useRef(null); + + // Get current step based on current tx index + const currentStep = useMemo(() => { + if (typeof preparedQuote?.steps === "undefined") return undefined; + if (currentTxIndex === undefined) { + return undefined; + } + const tx = flatTxs[currentTxIndex]; + return tx ? preparedQuote.steps[tx._stepIndex] : undefined; + }, [currentTxIndex, flatTxs, preparedQuote?.steps]); + + // Calculate progress including onramp step + const progress = useMemo(() => { + if (typeof preparedQuote?.type === "undefined") return 0; + const totalSteps = + flatTxs.length + (preparedQuote.type === "onramp" ? 1 : 0); + if (totalSteps === 0) { + return 0; + } + const completedSteps = + completedTxs.size + (onrampStatus === "completed" ? 1 : 0); + return Math.round((completedSteps / totalSteps) * 100); + }, [completedTxs.size, flatTxs.length, preparedQuote?.type, onrampStatus]); + + // Exponential backoff polling utility + const poller = useCallback( + async ( + pollFn: () => Promise<{ + completed: boolean; + }>, + abortSignal: AbortSignal, + ) => { + const delay = 2000; // 2 second poll interval + + while (!abortSignal.aborted) { + const result = await pollFn(); + if (result.completed) { + return; + } + + await new Promise((resolve) => { + const timeout = setTimeout(resolve, delay); + abortSignal.addEventListener("abort", () => clearTimeout(timeout), { + once: true, + }); + }); + } + + throw new Error("Polling aborted"); + }, + [], + ); + + // Execute a single transaction + const executeSingleTx = useCallback( + async ( + tx: FlattenedTx, + account: Account, + completedStatusResults: CompletedStatusResult[], + abortSignal: AbortSignal, + ) => { + if (typeof preparedQuote?.type === "undefined") { + throw new Error("No quote generated. This is unexpected."); + } + const { prepareTransaction } = await import( + "../../../transaction/prepare-transaction.js" + ); + const { sendTransaction } = await import( + "../../../transaction/actions/send-transaction.js" + ); + + // Prepare the transaction + const preparedTx = prepareTransaction({ + chain: tx.chain, + client: tx.client, + to: tx.to, + data: tx.data, + value: tx.value, + }); + + // Send the transaction + const result = await sendTransaction({ + account, + transaction: preparedTx, + }); + const hash = result.transactionHash; + + if (tx.action === "approval" || tx.action === "fee") { + // don't poll status for approval transactions, just wait for confirmation + await waitForReceipt(result); + return; + } + + // Poll for completion + const { status } = await import("../../../bridge/Status.js"); + await poller(async () => { + const statusResult = await status({ + transactionHash: hash, + chainId: tx.chainId, + client: tx.client, + }); + + if (statusResult.status === "COMPLETED") { + // Add type field from preparedQuote for discriminated union + const typedStatusResult = { + type: preparedQuote.type, + ...statusResult, + }; + completedStatusResults.push(typedStatusResult); + return { completed: true }; + } + + if (statusResult.status === "FAILED") { + throw new Error("Payment failed"); + } + + return { completed: false }; + }, abortSignal); + }, + [poller, preparedQuote?.type], + ); + + // Execute batch transactions + const executeBatch = useCallback( + async ( + txs: FlattenedTx[], + account: Account, + completedStatusResults: CompletedStatusResult[], + abortSignal: AbortSignal, + ) => { + if (typeof preparedQuote?.type === "undefined") { + throw new Error("No quote generated. This is unexpected."); + } + if (!account.sendBatchTransaction) { + throw new Error("Account does not support batch transactions"); + } + + const { prepareTransaction } = await import( + "../../../transaction/prepare-transaction.js" + ); + const { sendBatchTransaction } = await import( + "../../../transaction/actions/send-batch-transaction.js" + ); + + // Prepare and convert all transactions + const serializableTxs = await Promise.all( + txs.map(async (tx) => { + const preparedTx = prepareTransaction({ + chain: tx.chain, + client: tx.client, + to: tx.to, + data: tx.data, + value: tx.value, + }); + return preparedTx; + }), + ); + + // Send batch + const result = await sendBatchTransaction({ + account, + transactions: serializableTxs, + }); + // Batch transactions return a single receipt, we need to handle this differently + // For now, we'll assume all transactions in the batch succeed together + + // Poll for the first transaction's completion (representative of the batch) + if (txs.length === 0) { + throw new Error("No transactions to batch"); + } + const firstTx = txs[0]; + if (!firstTx) { + throw new Error("Invalid batch transaction"); + } + + const { status } = await import("../../../bridge/Status.js"); + await poller(async () => { + const statusResult = await status({ + transactionHash: result.transactionHash, + chainId: firstTx.chainId, + client: firstTx.client, + }); + + if (statusResult.status === "COMPLETED") { + // Add type field from preparedQuote for discriminated union + const typedStatusResult = { + type: preparedQuote.type, + ...statusResult, + }; + completedStatusResults.push(typedStatusResult); + return { completed: true }; + } + + if (statusResult.status === "FAILED") { + throw new Error("Payment failed"); + } + + return { completed: false }; + }, abortSignal); + }, + [poller, preparedQuote?.type], + ); + + // Execute onramp step + const executeOnramp = useCallback( + async ( + onrampQuote: Extract, + completedStatusResults: CompletedStatusResult[], + abortSignal: AbortSignal, + ) => { + setOnrampStatus("executing"); + // Open the payment URL + windowAdapter.open(onrampQuote.link); + + // Poll for completion using the session ID + const { Onramp } = await import("../../../bridge/index.js"); + await poller(async () => { + const statusResult = await Onramp.status({ + id: onrampQuote.id, + client: client, + }); + + const status = statusResult.status; + if (status === "COMPLETED") { + setOnrampStatus("completed"); + // Add type field for discriminated union + const typedStatusResult = { + type: "onramp" as const, + ...statusResult, + }; + completedStatusResults.push(typedStatusResult); + return { completed: true }; + } else if (status === "FAILED") { + setOnrampStatus("failed"); + } + + return { completed: false }; + }, abortSignal); + }, + [poller, client, windowAdapter], + ); + + // Main execution function + const execute = useCallback(async () => { + if (typeof preparedQuote?.type === "undefined") { + throw new Error("No quote generated. This is unexpected."); + } + if (executionState !== "idle") { + return; + } + + setExecutionState("executing"); + setError(undefined); + const completedStatusResults: CompletedStatusResult[] = []; + + // Create new abort controller + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + try { + // Execute onramp first if configured and not already completed + if (preparedQuote.type === "onramp" && onrampStatus === "pending") { + await executeOnramp( + preparedQuote, + completedStatusResults, + abortController.signal, + ); + } + + // Then execute transactions + const account = wallet.getAccount(); + if (!account) { + throw new ApiError({ + code: "INVALID_INPUT", + message: "Wallet not connected", + statusCode: 400, + }); + } + + // Start from where we left off, or from the beginning + const startIndex = currentTxIndex ?? 0; + + for (let i = startIndex; i < flatTxs.length; i++) { + if (abortController.signal.aborted) { + break; + } + + const currentTx = flatTxs[i]; + if (!currentTx) { + continue; // Skip invalid index + } + + setCurrentTxIndex(i); + const currentStepData = preparedQuote.steps[currentTx._stepIndex]; + if (!currentStepData) { + throw new Error(`Invalid step index: ${currentTx._stepIndex}`); + } + + // switch chain if needed + if (currentTx.chainId !== wallet.getChain()?.id) { + await wallet.switchChain(getCachedChain(currentTx.chainId)); + } + + // Check if we can batch transactions + const canBatch = + account.sendBatchTransaction !== undefined && i < flatTxs.length - 1; // Not the last transaction + + if (canBatch) { + // Find consecutive transactions on the same chain + const batchTxs: FlattenedTx[] = [currentTx]; + let j = i + 1; + while (j < flatTxs.length) { + const nextTx = flatTxs[j]; + if (!nextTx || nextTx.chainId !== currentTx.chainId) { + break; + } + batchTxs.push(nextTx); + j++; + } + + // Execute batch if we have multiple transactions + if (batchTxs.length > 1) { + await executeBatch( + batchTxs, + account, + completedStatusResults, + abortController.signal, + ); + + // Mark all batched transactions as completed + for (const tx of batchTxs) { + setCompletedTxs((prev) => new Set(prev).add(tx._index)); + } + + // Skip ahead + i = j - 1; + continue; + } + } + + // Execute single transaction + await executeSingleTx( + currentTx, + account, + completedStatusResults, + abortController.signal, + ); + + // Mark transaction as completed + setCompletedTxs((prev) => new Set(prev).add(currentTx._index)); + } + + // All done - check if we actually completed everything + if (!abortController.signal.aborted) { + setCurrentTxIndex(undefined); + + // Call completion callback with all completed status results + if (onComplete) { + onComplete(completedStatusResults); + } + } + } catch (err) { + console.error("Error executing payment", err); + if (err instanceof ApiError) { + setError(err); + } else { + setError( + new ApiError({ + code: "UNKNOWN_ERROR", + message: (err as Error)?.message || "An unknown error occurred", + statusCode: 500, + }), + ); + } + } finally { + setExecutionState("idle"); + abortControllerRef.current = null; + } + }, [ + executionState, + wallet, + currentTxIndex, + flatTxs, + executeSingleTx, + executeBatch, + onrampStatus, + executeOnramp, + onComplete, + preparedQuote, + ]); + + // Start execution + const start = useCallback(() => { + if (executionState === "idle") { + execute(); + } + }, [execute, executionState]); + + // Cancel execution + const cancel = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + setExecutionState("idle"); + if (onrampStatus === "executing") { + setOnrampStatus("pending"); + } + }, [onrampStatus]); + + // Retry from failed transaction + const retry = useCallback(() => { + if (error) { + setError(undefined); + execute(); + } + }, [error, execute]); + + const hasInitialized = useRef(false); + + useEffect(() => { + if ( + autoStart && + executionState === "idle" && + currentTxIndex === undefined && + !hasInitialized.current + ) { + hasInitialized.current = true; + setExecutionState("auto-starting"); + // add a delay to ensure the UI is ready + setTimeout(() => { + start(); + }, 500); + } + }, [autoStart, executionState, currentTxIndex, start]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + + return { + currentStep, + currentTxIndex, + progress, + executionState, + steps: preparedQuote?.steps, + onrampStatus, + error, + start, + cancel, + retry, + }; +} diff --git a/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts b/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts new file mode 100644 index 00000000000..33d18ec6075 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts @@ -0,0 +1,177 @@ +import { useQuery } from "@tanstack/react-query"; +import type { AbiFunction } from "abitype"; +import { toFunctionSelector } from "viem"; +import type { Token } from "../../../bridge/index.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js"; +import type { CompilerMetadata } from "../../../contract/actions/compiler-metadata.js"; +import { getCompilerMetadata } from "../../../contract/actions/get-compiler-metadata.js"; +import { getContract } from "../../../contract/contract.js"; +import { decimals } from "../../../extensions/erc20/read/decimals.js"; +import { getToken } from "../../../pay/convert/get-token.js"; +import { encode } from "../../../transaction/actions/encode.js"; +import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js"; +import { getTransactionGasCost } from "../../../transaction/utils.js"; +import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js"; +import { toTokens } from "../../../utils/units.js"; +import { + formatCurrencyAmount, + formatTokenAmount, +} from "../../web/ui/ConnectWallet/screens/formatTokenBalance.js"; +import { useChainMetadata } from "./others/useChainQuery.js"; + +export interface TransactionDetails { + contractMetadata: CompilerMetadata | null; + functionInfo: { + functionName: string; + selector: string; + description?: string; + }; + usdValueDisplay: string | null; + txCostDisplay: string; + gasCostDisplay: string | null; + tokenInfo: Token | null; + costWei: bigint; + gasCostWei: bigint | null; + totalCost: string; + totalCostWei: bigint; +} + +export interface UseTransactionDetailsOptions { + transaction: PreparedTransaction; + client: ThirdwebClient; +} + +/** + * Hook to fetch comprehensive transaction details including contract metadata, + * function information, cost calculations, and gas estimates. + */ +export function useTransactionDetails({ + transaction, + client, +}: UseTransactionDetailsOptions) { + const chainMetadata = useChainMetadata(transaction.chain); + + return useQuery({ + queryKey: [ + "transaction-details", + transaction.to, + transaction.chain.id, + transaction.erc20Value, + ], + queryFn: async (): Promise => { + // Create contract instance for metadata fetching + const contract = getContract({ + client, + chain: transaction.chain, + address: transaction.to as string, + }); + + const [contractMetadata, value, erc20Value, transactionData] = + await Promise.all([ + getCompilerMetadata(contract).catch(() => null), + resolvePromisedValue(transaction.value), + resolvePromisedValue(transaction.erc20Value), + encode(transaction).catch(() => "0x"), + ]); + + const [tokenInfo, gasCostWei] = await Promise.all([ + getToken( + client, + erc20Value ? erc20Value.tokenAddress : NATIVE_TOKEN_ADDRESS, + transaction.chain.id, + ).catch(() => null), + getTransactionGasCost(transaction).catch(() => null), + ]); + + // Process function info from ABI if available + let functionInfo = { + functionName: "Contract Call", + selector: "0x", + description: undefined, + }; + + if (contractMetadata?.abi && transactionData.length >= 10) { + try { + const selector = transactionData.slice(0, 10) as `0x${string}`; + const abi = contractMetadata.abi; + + // Find matching function in ABI + const abiItems = Array.isArray(abi) ? abi : []; + const functions = abiItems + .filter( + (item) => + item && + typeof item === "object" && + "type" in item && + (item as { type: string }).type === "function", + ) + .map((item) => item as AbiFunction); + + const matchingFunction = functions.find((fn) => { + return toFunctionSelector(fn) === selector; + }); + + if (matchingFunction) { + functionInfo = { + functionName: matchingFunction.name, + selector, + description: undefined, // Skip devdoc for now + }; + } + } catch { + // Keep default values + } + } + + const resolveDecimals = async () => { + if (tokenInfo) { + return tokenInfo.decimals; + } + if (erc20Value) { + return decimals({ + contract: getContract({ + client, + chain: transaction.chain, + address: erc20Value.tokenAddress, + }), + }); + } + return 18; + }; + + const decimal = await resolveDecimals(); + const costWei = erc20Value ? erc20Value.amountWei : value || 0n; + const nativeTokenSymbol = + chainMetadata.data?.nativeCurrency?.symbol || "ETH"; + const tokenSymbol = tokenInfo?.symbol || nativeTokenSymbol; + + const totalCostWei = erc20Value + ? erc20Value.amountWei + : (value || 0n) + (gasCostWei || 0n); + const totalCost = toTokens(totalCostWei, decimal); + + const usdValue = tokenInfo?.priceUsd + ? Number(totalCost) * tokenInfo.priceUsd + : null; + + return { + contractMetadata, + functionInfo, + usdValueDisplay: usdValue + ? formatCurrencyAmount("USD", usdValue) + : null, + txCostDisplay: `${formatTokenAmount(costWei, decimal)} ${tokenSymbol}`, + gasCostDisplay: gasCostWei + ? `${formatTokenAmount(gasCostWei, 18)} ${nativeTokenSymbol}` + : null, + tokenInfo, + costWei, + gasCostWei, + totalCost, + totalCostWei, + }; + }, + enabled: !!transaction.to && !!chainMetadata.data, + }); +} diff --git a/packages/thirdweb/src/react/core/machines/.keep b/packages/thirdweb/src/react/core/machines/.keep new file mode 100644 index 00000000000..f5a7aa3fffa --- /dev/null +++ b/packages/thirdweb/src/react/core/machines/.keep @@ -0,0 +1,2 @@ +# Placeholder file to maintain directory structure +# This directory will contain XState machine definitions for payment flows \ No newline at end of file diff --git a/packages/thirdweb/src/react/core/machines/paymentMachine.test.ts b/packages/thirdweb/src/react/core/machines/paymentMachine.test.ts new file mode 100644 index 00000000000..f720770eaa6 --- /dev/null +++ b/packages/thirdweb/src/react/core/machines/paymentMachine.test.ts @@ -0,0 +1,691 @@ +/** + * @vitest-environment happy-dom + */ +import { act, renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { TEST_CLIENT } from "../../../../test/src/test-clients.js"; +import { TEST_IN_APP_WALLET_A } from "../../../../test/src/test-wallets.js"; +import type { Token } from "../../../bridge/types/Token.js"; +import { defineChain } from "../../../chains/utils.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js"; +import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js"; +import type { WindowAdapter } from "../adapters/WindowAdapter.js"; +import type { BridgePrepareResult } from "../hooks/useBridgePrepare.js"; +import { + type PaymentMachineContext, + type PaymentMethod, + usePaymentMachine, +} from "./paymentMachine.js"; + +// Mock adapters +const mockWindowAdapter: WindowAdapter = { + open: vi.fn().mockResolvedValue(undefined), +}; + +const mockStorage: AsyncStorage = { + getItem: vi.fn().mockResolvedValue(null), + setItem: vi.fn().mockResolvedValue(undefined), + removeItem: vi.fn().mockResolvedValue(undefined), +}; + +// Test token objects +const testUSDCToken: Token = { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + name: "USD Coin (PoS)", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, +}; + +const testETHToken: Token = { + chainId: 1, + address: NATIVE_TOKEN_ADDRESS, + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, +}; + +const testTokenForPayment: Token = { + chainId: 1, + address: "0xA0b86a33E6425c03e54c4b45DCb6d75b6B72E2AA", + name: "Test Token", + symbol: "TT", + decimals: 18, + priceUsd: 1.0, +}; + +const mockBuyQuote: BridgePrepareResult = { + type: "buy", + originAmount: 1000000000000000000n, // 1 ETH + destinationAmount: 100000000n, // 100 USDC + timestamp: Date.now(), + estimatedExecutionTimeMs: 120000, // 2 minutes + steps: [ + { + originToken: testETHToken, + destinationToken: testUSDCToken, + originAmount: 1000000000000000000n, + destinationAmount: 100000000n, + estimatedExecutionTimeMs: 120000, + transactions: [ + { + action: "approval" as const, + id: "0x123" as const, + to: "0x456" as const, + data: "0x789" as const, + chainId: 1, + client: TEST_CLIENT, + chain: defineChain(1), + }, + { + action: "buy" as const, + id: "0xabc" as const, + to: "0xdef" as const, + data: "0x012" as const, + value: 1000000000000000000n, + chainId: 1, + client: TEST_CLIENT, + chain: defineChain(1), + }, + ], + }, + ], + intent: { + originChainId: 1, + originTokenAddress: NATIVE_TOKEN_ADDRESS, + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 100000000n, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }, +}; + +describe("PaymentMachine", () => { + let adapters: PaymentMachineContext["adapters"]; + + beforeEach(() => { + adapters = { + window: mockWindowAdapter, + storage: mockStorage, + }; + }); + + it("should initialize in init state", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + const [state] = result.current; + + expect(state.value).toBe("init"); + expect(state.context.mode).toBe("fund_wallet"); + expect(state.context.adapters).toBe(adapters); + }); + + it("should transition through happy path with wallet payment method", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + // Confirm destination + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + let [state] = result.current; + expect(state.value).toBe("methodSelection"); + expect(state.context.destinationToken).toEqual(testTokenForPayment); + expect(state.context.destinationAmount).toBe("100"); + expect(state.context.receiverAddress).toBe( + "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + ); + + // Select wallet payment method + const walletPaymentMethod: PaymentMethod = { + type: "wallet", + originToken: testUSDCToken, + payerWallet: TEST_IN_APP_WALLET_A, + balance: 1000000000000000000n, + }; + + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: walletPaymentMethod, + }); + }); + + [state] = result.current; + expect(state.value).toBe("quote"); + expect(state.context.selectedPaymentMethod).toEqual(walletPaymentMethod); + + // Receive quote + act(() => { + const [, send] = result.current; + send({ + type: "QUOTE_RECEIVED", + preparedQuote: mockBuyQuote, + }); + }); + + [state] = result.current; + expect(state.value).toBe("preview"); + expect(state.context.preparedQuote).toBe(mockBuyQuote); + + // Confirm route + act(() => { + const [, send] = result.current; + send({ + type: "ROUTE_CONFIRMED", + }); + }); + + [state] = result.current; + expect(state.value).toBe("execute"); + expect(state.context.selectedPaymentMethod).toBe(walletPaymentMethod); + + // Complete execution + act(() => { + const [, send] = result.current; + send({ + type: "EXECUTION_COMPLETE", + completedStatuses: [ + { + type: "buy", + status: "COMPLETED", + paymentId: "test-payment-id", + originAmount: 1000000000000000000n, + destinationAmount: 100000000n, + originChainId: 1, + destinationChainId: 137, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationTokenAddress: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + originToken: testETHToken, + destinationToken: testUSDCToken, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + transactions: [ + { + chainId: 1, + transactionHash: "0xtest123", + }, + ], + }, + ], + }); + }); + + [state] = result.current; + expect(state.value).toBe("success"); + expect(state.context.completedStatuses).toBeDefined(); + expect(state.context.completedStatuses).toHaveLength(1); + expect(state.context.completedStatuses?.[0]?.status).toBe("COMPLETED"); + }); + + it("should handle errors and allow retry", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + const testError = new Error("Network error"); + act(() => { + const [, send] = result.current; + send({ + type: "ERROR_OCCURRED", + error: testError, + }); + }); + + let [state] = result.current; + expect(state.value).toBe("error"); + expect(state.context.currentError).toBe(testError); + expect(state.context.retryState).toBe("init"); + + // Retry should clear error and return to beginning + act(() => { + const [, send] = result.current; + send({ + type: "RETRY", + }); + }); + + [state] = result.current; + expect(state.value).toBe("init"); + expect(state.context.currentError).toBeUndefined(); + expect(state.context.retryState).toBeUndefined(); + }); + + it("should preserve context data through transitions", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + const testToken: Token = { + chainId: 42, + address: "0xtest", + name: "Test Token", + symbol: "TEST", + decimals: 18, + priceUsd: 1.0, + }; + + // Confirm destination + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testToken, + destinationAmount: "50", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + // Select payment method + const paymentMethod: PaymentMethod = { + type: "wallet", + payerWallet: TEST_IN_APP_WALLET_A, + originToken: testUSDCToken, + balance: 1000000000000000000n, + }; + + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod, + }); + }); + + const [state] = result.current; + // All context should be preserved + expect(state.context.destinationToken).toEqual(testToken); + expect(state.context.destinationAmount).toBe("50"); + expect(state.context.selectedPaymentMethod).toEqual(paymentMethod); + expect(state.context.mode).toBe("fund_wallet"); + expect(state.context.adapters).toBe(adapters); + }); + + it("should handle state transitions correctly", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + const [initialState] = result.current; + expect(initialState.value).toBe("init"); + + // Only DESTINATION_CONFIRMED should be valid from initial state + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: { + type: "fiat", + currency: "USD", + payerWallet: TEST_IN_APP_WALLET_A, + onramp: "stripe", + }, + }); + }); + + let [state] = result.current; + expect(state.value).toBe("init"); // Should stay in same state for invalid transition + + // Valid transition + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + [state] = result.current; + expect(state.value).toBe("methodSelection"); + }); + + it("should reset to initial state", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + // Go through some states + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: { + type: "fiat", + currency: "USD", + payerWallet: TEST_IN_APP_WALLET_A, + onramp: "stripe", + }, + }); + }); + + let [state] = result.current; + expect(state.value).toBe("quote"); + + // Trigger error + act(() => { + const [, send] = result.current; + send({ + type: "ERROR_OCCURRED", + error: new Error("Test error"), + }); + }); + + [state] = result.current; + expect(state.value).toBe("error"); + + // Reset + act(() => { + const [, send] = result.current; + send({ + type: "RESET", + }); + }); + + [state] = result.current; + expect(state.value).toBe("init"); + // Context should still have adapters and mode but other data should be cleared + expect(state.context.adapters).toBe(adapters); + expect(state.context.mode).toBe("fund_wallet"); + }); + + it("should handle error states from all major states", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + // Test error from init + act(() => { + const [, send] = result.current; + send({ + type: "ERROR_OCCURRED", + error: new Error("Init error"), + }); + }); + + let [state] = result.current; + expect(state.value).toBe("error"); + expect(state.context.retryState).toBe("init"); + + // Reset and test error from methodSelection + act(() => { + const [, send] = result.current; + send({ type: "RESET" }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "ERROR_OCCURRED", + error: new Error("Method selection error"), + }); + }); + + [state] = result.current; + expect(state.value).toBe("error"); + expect(state.context.retryState).toBe("methodSelection"); + }); + + it("should handle back navigation", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + // Go to methodSelection + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + // Go to quote + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: { + type: "fiat", + currency: "USD", + payerWallet: TEST_IN_APP_WALLET_A, + onramp: "stripe", + }, + }); + }); + + let [state] = result.current; + expect(state.value).toBe("quote"); + + // Navigate back to methodSelection + act(() => { + const [, send] = result.current; + send({ + type: "BACK", + }); + }); + + [state] = result.current; + expect(state.value).toBe("methodSelection"); + + // Navigate back to init + act(() => { + const [, send] = result.current; + send({ + type: "BACK", + }); + }); + + [state] = result.current; + expect(state.value).toBe("init"); + }); + + it("should clear prepared quote when payment method changes", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + // Go to methodSelection + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + // Select first payment method and get quote + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: { + type: "fiat", + currency: "USD", + payerWallet: TEST_IN_APP_WALLET_A, + onramp: "stripe", + }, + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "QUOTE_RECEIVED", + preparedQuote: mockBuyQuote, + }); + }); + + let [state] = result.current; + expect(state.context.preparedQuote).toBe(mockBuyQuote); + + // Go back and select different payment method + act(() => { + const [, send] = result.current; + send({ type: "BACK" }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: { + type: "wallet", + payerWallet: TEST_IN_APP_WALLET_A, + originToken: testUSDCToken, + balance: 1000000000000000000n, + }, + }); + }); + + [state] = result.current; + expect(state.context.preparedQuote).toBeUndefined(); // Should be cleared + }); + + it("should handle post-buy-transaction state flow", () => { + const { result } = renderHook(() => + usePaymentMachine(adapters, "fund_wallet"), + ); + + // Go through the complete happy path to reach success state + act(() => { + const [, send] = result.current; + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: testTokenForPayment, + destinationAmount: "100", + receiverAddress: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "PAYMENT_METHOD_SELECTED", + paymentMethod: { + type: "wallet", + payerWallet: TEST_IN_APP_WALLET_A, + originToken: testUSDCToken, + balance: 1000000000000000000n, + }, + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "QUOTE_RECEIVED", + preparedQuote: mockBuyQuote, + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "ROUTE_CONFIRMED", + }); + }); + + act(() => { + const [, send] = result.current; + send({ + type: "EXECUTION_COMPLETE", + completedStatuses: [ + { + type: "buy", + status: "COMPLETED", + paymentId: "test-payment-id", + originAmount: 1000000000000000000n, + destinationAmount: 100000000n, + originChainId: 1, + destinationChainId: 137, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationTokenAddress: + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + originToken: testETHToken, + destinationToken: testUSDCToken, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + transactions: [ + { + chainId: 1, + transactionHash: "0xtest123", + }, + ], + }, + ], + }); + }); + + let [state] = result.current; + expect(state.value).toBe("success"); + + // Continue to post-buy transaction + act(() => { + const [, send] = result.current; + send({ + type: "CONTINUE_TO_TRANSACTION", + }); + }); + + [state] = result.current; + expect(state.value).toBe("post-buy-transaction"); + + // Reset from post-buy-transaction should go back to init + act(() => { + const [, send] = result.current; + send({ + type: "RESET", + }); + }); + + [state] = result.current; + expect(state.value).toBe("init"); + // Context should be reset to initial state with only adapters and mode + expect(state.context.adapters).toBe(adapters); + expect(state.context.mode).toBe("fund_wallet"); + expect(state.context.destinationToken).toBeUndefined(); + expect(state.context.selectedPaymentMethod).toBeUndefined(); + expect(state.context.preparedQuote).toBeUndefined(); + expect(state.context.completedStatuses).toBeUndefined(); + }); +}); diff --git a/packages/thirdweb/src/react/core/machines/paymentMachine.ts b/packages/thirdweb/src/react/core/machines/paymentMachine.ts new file mode 100644 index 00000000000..f79078783ad --- /dev/null +++ b/packages/thirdweb/src/react/core/machines/paymentMachine.ts @@ -0,0 +1,290 @@ +import { useCallback, useState } from "react"; +import type { Token } from "../../../bridge/types/Token.js"; +import type { Address } from "../../../utils/address.js"; +import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js"; +import type { Wallet } from "../../../wallets/interfaces/wallet.js"; +import type { WindowAdapter } from "../adapters/WindowAdapter.js"; +import type { CompletedStatusResult } from "../hooks/useStepExecutor.js"; +import type { + BridgePrepareRequest, + BridgePrepareResult, +} from "../hooks/useBridgePrepare.js"; + +/** + * Payment modes supported by BridgeEmbed + */ +export type PaymentMode = "fund_wallet" | "direct_payment" | "transaction"; + +/** + * Payment method types with their required data + */ +export type PaymentMethod = + | { + type: "wallet"; + payerWallet: Wallet; + originToken: Token; + balance: bigint; + } + | { + type: "fiat"; + payerWallet: Wallet; + currency: string; + onramp: "stripe" | "coinbase" | "transak"; + }; + +/** + * Payment machine context - holds all flow state data + */ +export interface PaymentMachineContext { + // Flow configuration + mode: PaymentMode; + + // Target requirements (resolved in init state) + destinationAmount?: string; + destinationToken?: Token; + receiverAddress?: Address; + + // User selections (set in methodSelection state) + selectedPaymentMethod?: PaymentMethod; + + // Prepared quote data (set in quote state) + quote?: BridgePrepareResult; + request?: BridgePrepareRequest; + + // Execution results (set in execute state on completion) + completedStatuses?: CompletedStatusResult[]; + + // Error handling + currentError?: Error; + retryState?: PaymentMachineState; // State to retry from + + // Dependency injection + adapters: { + window: WindowAdapter; + storage: AsyncStorage; + }; +} + +/** + * Events that can be sent to the payment machine + */ +export type PaymentMachineEvent = + | { + type: "DESTINATION_CONFIRMED"; + destinationToken: Token; + destinationAmount: string; + receiverAddress: Address; + } + | { type: "PAYMENT_METHOD_SELECTED"; paymentMethod: PaymentMethod } + | { + type: "QUOTE_RECEIVED"; + quote: BridgePrepareResult; + request: BridgePrepareRequest; + } + | { type: "ROUTE_CONFIRMED" } + | { type: "EXECUTION_COMPLETE"; completedStatuses: CompletedStatusResult[] } + | { type: "ERROR_OCCURRED"; error: Error } + | { type: "CONTINUE_TO_TRANSACTION" } + | { type: "RETRY" } + | { type: "RESET" } + | { type: "BACK" }; + +type PaymentMachineState = + | "init" + | "methodSelection" + | "quote" + | "preview" + | "execute" + | "success" + | "post-buy-transaction" + | "error"; + +/** + * Hook to create and use the payment machine + */ +export function usePaymentMachine( + adapters: PaymentMachineContext["adapters"], + mode: PaymentMode = "fund_wallet", +) { + const [currentState, setCurrentState] = useState("init"); + const [context, setContext] = useState({ + mode, + adapters, + }); + + const send = useCallback( + (event: PaymentMachineEvent) => { + setCurrentState((state) => { + setContext((ctx) => { + switch (state) { + case "init": + if (event.type === "DESTINATION_CONFIRMED") { + return { + ...ctx, + destinationToken: event.destinationToken, + destinationAmount: event.destinationAmount, + receiverAddress: event.receiverAddress, + }; + } else if (event.type === "ERROR_OCCURRED") { + return { + ...ctx, + currentError: event.error, + retryState: "init", + }; + } + break; + + case "methodSelection": + if (event.type === "PAYMENT_METHOD_SELECTED") { + return { + ...ctx, + quote: undefined, // reset quote when method changes + selectedPaymentMethod: event.paymentMethod, + }; + } else if (event.type === "ERROR_OCCURRED") { + return { + ...ctx, + currentError: event.error, + retryState: "methodSelection", + }; + } + break; + + case "quote": + if (event.type === "QUOTE_RECEIVED") { + return { + ...ctx, + quote: event.quote, + request: event.request, + }; + } else if (event.type === "ERROR_OCCURRED") { + return { + ...ctx, + currentError: event.error, + retryState: "quote", + }; + } + break; + + case "preview": + if (event.type === "ERROR_OCCURRED") { + return { + ...ctx, + currentError: event.error, + retryState: "preview", + }; + } + break; + + case "execute": + if (event.type === "EXECUTION_COMPLETE") { + return { + ...ctx, + completedStatuses: event.completedStatuses, + }; + } else if (event.type === "ERROR_OCCURRED") { + return { + ...ctx, + currentError: event.error, + retryState: "execute", + }; + } + break; + + case "error": + if (event.type === "RETRY" || event.type === "RESET") { + return { + ...ctx, + currentError: undefined, + retryState: undefined, + }; + } + break; + + case "success": + if (event.type === "RESET") { + return { + mode: ctx.mode, + adapters: ctx.adapters, + }; + } + break; + + case "post-buy-transaction": + if (event.type === "RESET") { + return { + mode: ctx.mode, + adapters: ctx.adapters, + }; + } + break; + } + return ctx; + }); + + // State transitions + switch (state) { + case "init": + if (event.type === "DESTINATION_CONFIRMED") + return "methodSelection"; + if (event.type === "ERROR_OCCURRED") return "error"; + break; + + case "methodSelection": + if (event.type === "PAYMENT_METHOD_SELECTED") return "quote"; + if (event.type === "BACK") return "init"; + if (event.type === "ERROR_OCCURRED") return "error"; + break; + + case "quote": + if (event.type === "QUOTE_RECEIVED") return "preview"; + if (event.type === "BACK") return "methodSelection"; + if (event.type === "ERROR_OCCURRED") return "error"; + break; + + case "preview": + if (event.type === "ROUTE_CONFIRMED") return "execute"; + if (event.type === "BACK") return "methodSelection"; + if (event.type === "ERROR_OCCURRED") return "error"; + break; + + case "execute": + if (event.type === "EXECUTION_COMPLETE") return "success"; + if (event.type === "BACK") return "preview"; + if (event.type === "ERROR_OCCURRED") return "error"; + break; + + case "success": + if (event.type === "CONTINUE_TO_TRANSACTION") + return "post-buy-transaction"; + if (event.type === "RESET") return "init"; + break; + + case "post-buy-transaction": + if (event.type === "RESET") return "init"; + break; + + case "error": + if (event.type === "RETRY") { + return context.retryState ?? "init"; + } + if (event.type === "RESET") { + return "init"; + } + break; + } + + return state; + }); + }, + [context.retryState], + ); + + return [ + { + value: currentState, + context, + }, + send, + ] as const; +} diff --git a/packages/thirdweb/src/react/core/types/.keep b/packages/thirdweb/src/react/core/types/.keep new file mode 100644 index 00000000000..aacf1fca1d1 --- /dev/null +++ b/packages/thirdweb/src/react/core/types/.keep @@ -0,0 +1,2 @@ +# Placeholder file to maintain directory structure +# This directory will contain shared type definitions and interfaces \ No newline at end of file diff --git a/packages/thirdweb/src/react/core/utils/persist.ts b/packages/thirdweb/src/react/core/utils/persist.ts new file mode 100644 index 00000000000..169f012470c --- /dev/null +++ b/packages/thirdweb/src/react/core/utils/persist.ts @@ -0,0 +1,129 @@ +import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js"; +import type { PaymentMachineContext } from "../machines/paymentMachine.js"; + +/** + * Storage key for payment machine snapshots + */ +const PAYMENT_SNAPSHOT_KEY = "thirdweb:bridge-embed:payment-snapshot"; + +/** + * Serializable snapshot of the payment machine state + */ +export interface PaymentSnapshot { + value: string; // Current state name + context: Omit; // Context without adapters (not serializable) + timestamp: number; // When snapshot was saved +} + +/** + * Saves a payment machine snapshot to storage + * + * @param storage - AsyncStorage instance for persistence + * @param state - Current machine state name + * @param context - Current machine context (adapters will be excluded) + * @returns Promise that resolves when snapshot is saved + */ +export async function saveSnapshot( + storage: AsyncStorage, + state: string, + context: PaymentMachineContext, +): Promise { + try { + // Create serializable snapshot excluding adapters + const snapshot: PaymentSnapshot = { + value: state, + context: { + mode: context.mode, + destinationToken: context.destinationToken, + destinationAmount: context.destinationAmount, + selectedPaymentMethod: context.selectedPaymentMethod, + quote: context.quote, + request: context.request, + completedStatuses: context.completedStatuses, + currentError: context.currentError + ? { + name: context.currentError.name, + message: context.currentError.message, + stack: context.currentError.stack, + } + : undefined, + retryState: context.retryState, + }, + timestamp: Date.now(), + }; + + // Serialize and save to storage + const serializedSnapshot = JSON.stringify(snapshot); + await storage.setItem(PAYMENT_SNAPSHOT_KEY, serializedSnapshot); + } catch (error) { + // Log error but don't throw - persistence failure shouldn't break the flow + console.warn("Failed to save payment snapshot:", error); + } +} + +/** + * Loads a payment machine snapshot from storage + * + * @param storage - AsyncStorage instance for persistence + * @returns Promise that resolves to the loaded snapshot or null if not found/invalid + */ +export async function loadSnapshot( + storage: AsyncStorage, +): Promise { + try { + const serializedSnapshot = await storage.getItem(PAYMENT_SNAPSHOT_KEY); + + if (!serializedSnapshot) { + return null; + } + + const snapshot = JSON.parse(serializedSnapshot) as PaymentSnapshot; + + // Validate snapshot structure + if (!snapshot.value || !snapshot.context || !snapshot.timestamp) { + console.warn("Invalid payment snapshot structure, ignoring"); + await clearSnapshot(storage); + return null; + } + + // Check if snapshot is too old (24 hours) + const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if (Date.now() - snapshot.timestamp > maxAge) { + console.warn("Payment snapshot expired, clearing"); + await clearSnapshot(storage); + return null; + } + + return snapshot; + } catch (error) { + console.warn("Failed to load payment snapshot:", error); + // Clear corrupted snapshot + await clearSnapshot(storage); + return null; + } +} + +/** + * Clears the payment machine snapshot from storage + * + * @param storage - AsyncStorage instance for persistence + * @returns Promise that resolves when snapshot is cleared + */ +export async function clearSnapshot(storage: AsyncStorage): Promise { + try { + await storage.removeItem(PAYMENT_SNAPSHOT_KEY); + } catch (error) { + console.warn("Failed to clear payment snapshot:", error); + } +} + +/** + * Checks if a valid payment snapshot exists in storage + * + * @param storage - AsyncStorage instance for persistence + * @returns Promise that resolves to true if valid snapshot exists + */ +export async function hasSnapshot(storage: AsyncStorage): Promise { + const snapshot = await loadSnapshot(storage); + return snapshot !== null; +} diff --git a/packages/thirdweb/src/react/core/utils/wallet.test.ts b/packages/thirdweb/src/react/core/utils/wallet.test.ts new file mode 100644 index 00000000000..37d8d5af4b0 --- /dev/null +++ b/packages/thirdweb/src/react/core/utils/wallet.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import type { Wallet } from "../../../wallets/interfaces/wallet.js"; +import { hasSponsoredTransactionsEnabled } from "../../../wallets/smart/is-smart-wallet.js"; + +describe("hasSponsoredTransactionsEnabled", () => { + it("should return false for undefined wallet", () => { + expect(hasSponsoredTransactionsEnabled(undefined)).toBe(false); + }); + + it("should handle smart wallet with sponsorGas config", () => { + const mockSmartWallet = { + id: "smart", + getConfig: () => ({ sponsorGas: true }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockSmartWallet)).toBe(true); + + const mockSmartWalletDisabled = { + id: "smart", + getConfig: () => ({ sponsorGas: false }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockSmartWalletDisabled)).toBe( + false, + ); + }); + + it("should handle smart wallet with gasless config", () => { + const mockSmartWallet = { + id: "smart", + getConfig: () => ({ gasless: true }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockSmartWallet)).toBe(true); + }); + + it("should handle inApp wallet with smartAccount config", () => { + const mockInAppWallet = { + id: "inApp", + getConfig: () => ({ + smartAccount: { + sponsorGas: true, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); + + const mockInAppWalletDisabled = { + id: "inApp", + getConfig: () => ({ + smartAccount: { + sponsorGas: false, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWalletDisabled)).toBe( + false, + ); + }); + + it("should handle inApp wallet with gasless config", () => { + const mockInAppWallet = { + id: "inApp", + getConfig: () => ({ + smartAccount: { + gasless: true, + }, + }), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockInAppWallet)).toBe(true); + }); + + it("should return false for regular wallet without smart account config", () => { + const mockRegularWallet = { + id: "inApp", + getConfig: () => ({}), + } as Wallet; + expect(hasSponsoredTransactionsEnabled(mockRegularWallet)).toBe(false); + }); +}); diff --git a/packages/thirdweb/src/react/native/adapters/WindowAdapter.ts b/packages/thirdweb/src/react/native/adapters/WindowAdapter.ts new file mode 100644 index 00000000000..dbf8e4e311a --- /dev/null +++ b/packages/thirdweb/src/react/native/adapters/WindowAdapter.ts @@ -0,0 +1,36 @@ +import { Linking } from "react-native"; +import type { WindowAdapter } from "../../core/adapters/WindowAdapter.js"; + +/** + * React Native implementation of WindowAdapter using Linking.openURL. + * Opens URLs in the default browser or appropriate app. + */ +export class NativeWindowAdapter implements WindowAdapter { + /** + * Opens a URL using React Native's Linking API. + * + * @param url - The URL to open + * @returns Promise that resolves when the operation is initiated + */ + async open(url: string): Promise { + try { + // Check if the URL can be opened + const canOpen = await Linking.canOpenURL(url); + + if (!canOpen) { + throw new Error(`Cannot open URL: ${url}`); + } + + // Open the URL + await Linking.openURL(url); + } catch (error) { + console.warn("Failed to open URL:", error); + throw new Error(`Failed to open URL: ${url}`); + } + } +} + +/** + * Default instance of the Native WindowAdapter. + */ +export const nativeWindowAdapter = new NativeWindowAdapter(); diff --git a/packages/thirdweb/src/react/native/flows/.keep b/packages/thirdweb/src/react/native/flows/.keep new file mode 100644 index 00000000000..9a920ca00f5 --- /dev/null +++ b/packages/thirdweb/src/react/native/flows/.keep @@ -0,0 +1,2 @@ +# Placeholder file to maintain directory structure +# This directory will contain React Native composite and flow components \ No newline at end of file diff --git a/packages/thirdweb/src/react/web/adapters/WindowAdapter.ts b/packages/thirdweb/src/react/web/adapters/WindowAdapter.ts new file mode 100644 index 00000000000..db6347bdc5a --- /dev/null +++ b/packages/thirdweb/src/react/web/adapters/WindowAdapter.ts @@ -0,0 +1,23 @@ +import type { WindowAdapter } from "../../core/adapters/WindowAdapter.js"; + +/** + * Web implementation of WindowAdapter using the browser's window.open API. + * Opens URLs in a new tab/window. + */ +export class WebWindowAdapter implements WindowAdapter { + /** + * Opens a URL in a new browser tab/window. + * + * @param url - The URL to open + * @returns Promise that resolves when the operation is initiated + */ + async open(url: string): Promise { + // Use window.open to open URL in new tab + window.open(url, "_blank", "noopener,noreferrer"); + } +} + +/** + * Default instance of the Web WindowAdapter. + */ +export const webWindowAdapter = new WebWindowAdapter(); diff --git a/packages/thirdweb/src/react/web/adapters/adapters.test.ts b/packages/thirdweb/src/react/web/adapters/adapters.test.ts new file mode 100644 index 00000000000..612fcf34bd1 --- /dev/null +++ b/packages/thirdweb/src/react/web/adapters/adapters.test.ts @@ -0,0 +1,38 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { WebWindowAdapter } from "./WindowAdapter.js"; + +describe("WebWindowAdapter", () => { + let windowAdapter: WebWindowAdapter; + let mockOpen: ReturnType; + + beforeEach(() => { + windowAdapter = new WebWindowAdapter(); + + // Mock window.open using vi.stubGlobal + mockOpen = vi.fn(); + vi.stubGlobal("window", { + open: mockOpen, + }); + }); + + it("should open URL in new tab with correct parameters", async () => { + const mockWindow = {} as Partial; + mockOpen.mockReturnValue(mockWindow); + + await windowAdapter.open("https://example.com"); + + expect(mockOpen).toHaveBeenCalledWith( + "https://example.com", + "_blank", + "noopener,noreferrer", + ); + }); + + it("should throw error when popup is blocked", async () => { + mockOpen.mockReturnValue(null); + + await expect(windowAdapter.open("https://example.com")).rejects.toThrow( + "Failed to open URL - popup may be blocked", + ); + }); +}); diff --git a/packages/thirdweb/src/react/web/flows/.keep b/packages/thirdweb/src/react/web/flows/.keep new file mode 100644 index 00000000000..c8a98fb3bdc --- /dev/null +++ b/packages/thirdweb/src/react/web/flows/.keep @@ -0,0 +1,2 @@ +# Placeholder file to maintain directory structure +# This directory will contain web-specific composite and flow components \ No newline at end of file diff --git a/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx b/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx index 6348602db0c..16117a6cdd5 100644 --- a/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx +++ b/packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx @@ -83,6 +83,11 @@ import { TransactionModal } from "../../ui/TransactionButton/TransactionModal.js * value: toWei("0.1"), * chain: sepolia, * client: thirdwebClient, + * // Specify a token required for the transaction + * erc20Value: { + * amountWei: toWei("0.1"), + * tokenAddress: "0x...", + * }, * }); * sendTx(transaction); * }; diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx new file mode 100644 index 00000000000..90877b8c827 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx @@ -0,0 +1,357 @@ +"use client"; +import { useCallback, useMemo } from "react"; +import type { Token } from "../../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js"; +import type { Address } from "../../../../utils/address.js"; +import { webLocalStorage } from "../../../../utils/storage/webStorage.js"; +import type { Prettify } from "../../../../utils/type-utils.js"; +import type { + BridgePrepareRequest, + BridgePrepareResult, +} from "../../../core/hooks/useBridgePrepare.js"; +import type { CompletedStatusResult } from "../../../core/hooks/useStepExecutor.js"; +import { + type PaymentMethod, + usePaymentMachine, +} from "../../../core/machines/paymentMachine.js"; +import { webWindowAdapter } from "../../adapters/WindowAdapter.js"; +import en from "../ConnectWallet/locale/en.js"; +import type { ConnectLocale } from "../ConnectWallet/locale/types.js"; +import type { PayEmbedConnectOptions } from "../PayEmbed.js"; +import { ExecutingTxScreen } from "../TransactionButton/ExecutingScreen.js"; +import { Container } from "../components/basic.js"; +import { DirectPayment } from "./DirectPayment.js"; +import { ErrorBanner } from "./ErrorBanner.js"; +import { FundWallet } from "./FundWallet.js"; +import { QuoteLoader } from "./QuoteLoader.js"; +import { StepRunner } from "./StepRunner.js"; +import { TransactionPayment } from "./TransactionPayment.js"; +import { PaymentDetails } from "./payment-details/PaymentDetails.js"; +import { PaymentSelection } from "./payment-selection/PaymentSelection.js"; +import { SuccessScreen } from "./payment-success/SuccessScreen.js"; + +export type UIOptions = Prettify< + { + metadata?: { + title?: string; + description?: string; + image?: string; + }; + } & ( + | { + mode: "fund_wallet"; + destinationToken: Token; + initialAmount?: string; + presetOptions?: [number, number, number]; + } + | { + mode: "direct_payment"; + paymentInfo: { + sellerAddress: Address; + token: Token; + amount: string; + feePayer?: "sender" | "receiver"; + }; + } + | { mode: "transaction"; transaction: PreparedTransaction } + ) +>; + +export interface BridgeOrchestratorProps { + /** + * UI configuration and mode + */ + uiOptions: UIOptions; + + /** + * The receiver address, defaults to the connected wallet address + */ + receiverAddress?: Address; + + /** + * ThirdwebClient for blockchain interactions + */ + client: ThirdwebClient; + + /** + * Called when the flow is completed successfully + */ + onComplete?: () => void; + + /** + * Called when the flow encounters an error + */ + onError?: (error: Error) => void; + + /** + * Called when the user cancels the flow + */ + onCancel?: () => void; + + /** + * Connect options for wallet connection + */ + connectOptions?: PayEmbedConnectOptions; + + /** + * Locale for connect UI + */ + connectLocale?: ConnectLocale; + + /** + * Optional purchase data for the payment + */ + purchaseData?: object; + + /** + * Optional payment link ID for the payment + */ + paymentLinkId?: string; + + /** + * Quick buy amounts + */ + presetOptions?: [number, number, number]; +} + +export function BridgeOrchestrator({ + client, + uiOptions, + receiverAddress, + onComplete, + onError, + onCancel, + connectOptions, + connectLocale, + purchaseData, + paymentLinkId, + presetOptions, +}: BridgeOrchestratorProps) { + // Initialize adapters + const adapters = useMemo( + () => ({ + window: webWindowAdapter, + storage: webLocalStorage, + }), + [], + ); + + // Use the payment machine hook + const [state, send] = usePaymentMachine(adapters, uiOptions.mode); + + // Handle buy completion + const handleBuyComplete = useCallback(() => { + if (uiOptions.mode === "transaction") { + send({ type: "CONTINUE_TO_TRANSACTION" }); + } else { + onComplete?.(); + send({ type: "RESET" }); + } + }, [onComplete, send, uiOptions.mode]); + + // Handle post-buy transaction completion + const handlePostBuyTransactionComplete = useCallback(() => { + onComplete?.(); + send({ type: "RESET" }); + }, [onComplete, send]); + + // Handle errors + const handleError = useCallback( + (error: Error) => { + onError?.(error); + send({ type: "ERROR_OCCURRED", error }); + }, + [onError, send], + ); + + // Handle payment method selection + const handlePaymentMethodSelected = useCallback( + (paymentMethod: PaymentMethod) => { + send({ type: "PAYMENT_METHOD_SELECTED", paymentMethod }); + }, + [send], + ); + + // Handle quote received + const handleQuoteReceived = useCallback( + (quote: BridgePrepareResult, request: BridgePrepareRequest) => { + send({ type: "QUOTE_RECEIVED", quote, request }); + }, + [send], + ); + + // Handle route confirmation + const handleRouteConfirmed = useCallback(() => { + send({ type: "ROUTE_CONFIRMED" }); + }, [send]); + + // Handle execution complete + const handleExecutionComplete = useCallback( + (completedStatuses: CompletedStatusResult[]) => { + send({ type: "EXECUTION_COMPLETE", completedStatuses }); + }, + [send], + ); + + // Handle retry + const handleRetry = useCallback(() => { + send({ type: "RETRY" }); + }, [send]); + + // Handle requirements resolved from FundWallet and DirectPayment + const handleRequirementsResolved = useCallback( + (amount: string, token: Token, receiverAddress: Address) => { + send({ + type: "DESTINATION_CONFIRMED", + destinationToken: token, + receiverAddress, + destinationAmount: amount, + }); + }, + [send], + ); + + return ( + + {/* Error Banner */} + {state.value === "error" && state.context.currentError && ( + + )} + + {/* Render current screen based on state */} + {state.value === "init" && uiOptions.mode === "fund_wallet" && ( + + )} + + {state.value === "init" && uiOptions.mode === "direct_payment" && ( + + )} + + {state.value === "init" && uiOptions.mode === "transaction" && ( + + )} + + {state.value === "methodSelection" && + state.context.destinationToken && + state.context.destinationAmount && + state.context.receiverAddress && ( + { + send({ type: "BACK" }); + }} + connectOptions={connectOptions} + connectLocale={connectLocale || en} + includeDestinationToken={uiOptions.mode !== "fund_wallet"} + /> + )} + + {state.value === "quote" && + state.context.selectedPaymentMethod && + state.context.receiverAddress && + state.context.destinationToken && + state.context.destinationAmount && ( + { + send({ type: "BACK" }); + }} + /> + )} + + {state.value === "preview" && + state.context.selectedPaymentMethod && + state.context.quote && ( + { + send({ type: "BACK" }); + }} + onError={handleError} + /> + )} + + {state.value === "execute" && + state.context.quote && + state.context.request && + state.context.selectedPaymentMethod?.payerWallet && ( + { + send({ type: "BACK" }); + }} + /> + )} + + {state.value === "success" && + state.context.quote && + state.context.completedStatuses && ( + + )} + + {state.value === "post-buy-transaction" && + uiOptions.mode === "transaction" && + uiOptions.transaction && ( + { + // Do nothing + }} + /> + )} + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx b/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx new file mode 100644 index 00000000000..427c0573463 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx @@ -0,0 +1,234 @@ +"use client"; +import type { Token } from "../../../../bridge/types/Token.js"; +import { defineChain } from "../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import { type Address, shortenAddress } from "../../../../utils/address.js"; +import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; +import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; +import { useEnsName } from "../../../core/utils/wallet.js"; +import { ConnectButton } from "../ConnectWallet/ConnectButton.js"; +import { PoweredByThirdweb } from "../ConnectWallet/PoweredByTW.js"; +import { FiatValue } from "../ConnectWallet/screens/Buy/swap/FiatValue.js"; +import type { PayEmbedConnectOptions } from "../PayEmbed.js"; +import { ChainName } from "../components/ChainName.js"; +import { Spacer } from "../components/Spacer.js"; +import { Container, Line } from "../components/basic.js"; +import { Button } from "../components/buttons.js"; +import { Text } from "../components/text.js"; +import type { UIOptions } from "./BridgeOrchestrator.js"; +import { ChainIcon } from "./common/TokenAndChain.js"; +import { WithHeader } from "./common/WithHeader.js"; + +export interface DirectPaymentProps { + /** + * Payment information for the direct payment + */ + uiOptions: Extract; + + /** + * ThirdwebClient for blockchain interactions + */ + client: ThirdwebClient; + + /** + * Called when user continues with the payment + */ + onContinue: (amount: string, token: Token, receiverAddress: Address) => void; + + /** + * Connect options for wallet connection + */ + connectOptions?: PayEmbedConnectOptions; +} + +export function DirectPayment({ + uiOptions, + client, + onContinue, + connectOptions, +}: DirectPaymentProps) { + const activeAccount = useActiveAccount(); + const chain = defineChain(uiOptions.paymentInfo.token.chainId); + const theme = useCustomTheme(); + const handleContinue = () => { + onContinue( + uiOptions.paymentInfo.amount, + uiOptions.paymentInfo.token, + uiOptions.paymentInfo.sellerAddress, + ); + }; + const ensName = useEnsName({ + address: uiOptions.paymentInfo.sellerAddress, + client, + }); + const sellerAddress = + ensName.data || shortenAddress(uiOptions.paymentInfo.sellerAddress); + + const buyNow = ( + + + Buy Now · + + + + ); + + return ( + + {/* Price section */} + + + + + One-time payment + + + + + + + + + + + {/* Seller section */} + + + Sold by + + + {sellerAddress} + + + + + + + + Price + + + {`${uiOptions.paymentInfo.amount} ${uiOptions.paymentInfo.token.symbol}`} + + + + + + {/* Network section */} + + + Network + + + + + + + + + + + + + + {/* Action button */} + + {activeAccount ? ( + + ) : ( + + )} + + + + + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/ErrorBanner.tsx b/packages/thirdweb/src/react/web/ui/Bridge/ErrorBanner.tsx new file mode 100644 index 00000000000..6c13f6186a3 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/ErrorBanner.tsx @@ -0,0 +1,86 @@ +"use client"; +import { CrossCircledIcon } from "@radix-ui/react-icons"; +import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; +import { iconSize } from "../../../core/design-system/index.js"; +import { useBridgeError } from "../../../core/hooks/useBridgeError.js"; +import { Container } from "../components/basic.js"; +import { Button } from "../components/buttons.js"; +import { Text } from "../components/text.js"; + +export interface ErrorBannerProps { + /** + * The error to display + */ + error: Error; + + /** + * Called when user wants to retry + */ + onRetry: () => void; + + /** + * Called when user wants to cancel + */ + onCancel?: () => void; +} + +export function ErrorBanner({ error, onRetry, onCancel }: ErrorBannerProps) { + const theme = useCustomTheme(); + + const { userMessage } = useBridgeError({ error }); + + return ( + + {/* Error Icon and Message */} + + + + + + + + Error + + + + + {userMessage} + + + + + {/* Action Buttons */} + + + {onCancel && ( + + )} + + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx b/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx new file mode 100644 index 00000000000..5591e316f17 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx @@ -0,0 +1,341 @@ +"use client"; +import { useState } from "react"; +import type { Token } from "../../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import { type Address, getAddress } from "../../../../utils/address.js"; +import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; +import { + fontSize, + iconSize, + radius, + spacing, +} from "../../../core/design-system/index.js"; +import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; +import { ConnectButton } from "../ConnectWallet/ConnectButton.js"; +import { PoweredByThirdweb } from "../ConnectWallet/PoweredByTW.js"; +import { OutlineWalletIcon } from "../ConnectWallet/icons/OutlineWalletIcon.js"; +import { WalletRow } from "../ConnectWallet/screens/Buy/swap/WalletRow.js"; +import type { PayEmbedConnectOptions } from "../PayEmbed.js"; +import { Spacer } from "../components/Spacer.js"; +import { Container } from "../components/basic.js"; +import { Button } from "../components/buttons.js"; +import { Input } from "../components/formElements.js"; +import { Text } from "../components/text.js"; +import type { UIOptions } from "./BridgeOrchestrator.js"; +import { TokenAndChain } from "./common/TokenAndChain.js"; +import { WithHeader } from "./common/WithHeader.js"; + +export interface FundWalletProps { + /** + * UI configuration and mode + */ + uiOptions: Extract; + + /** + * The receiver address, defaults to the connected wallet address + */ + receiverAddress?: Address; + /** + * ThirdwebClient for price fetching + */ + client: ThirdwebClient; + + /** + * Called when continue is clicked with the resolved requirements + */ + onContinue: (amount: string, token: Token, receiverAddress: Address) => void; + + /** + * Quick buy amounts + */ + presetOptions?: [number, number, number]; + + /** + * Connect options for wallet connection + */ + connectOptions?: PayEmbedConnectOptions; +} + +export function FundWallet({ + client, + receiverAddress, + uiOptions, + onContinue, + presetOptions = [5, 10, 20], + connectOptions, +}: FundWalletProps) { + const [amount, setAmount] = useState(uiOptions.initialAmount ?? ""); + const theme = useCustomTheme(); + const account = useActiveAccount(); + const receiver = receiverAddress ?? account?.address; + const handleAmountChange = (inputValue: string) => { + let processedValue = inputValue; + + // Replace comma with period if it exists + processedValue = processedValue.replace(",", "."); + + if (processedValue.startsWith(".")) { + processedValue = `0${processedValue}`; + } + + const numValue = Number(processedValue); + if (Number.isNaN(numValue)) { + return; + } + + if (processedValue.startsWith("0") && !processedValue.startsWith("0.")) { + setAmount(processedValue.slice(1)); + } else { + setAmount(processedValue); + } + }; + + const getAmountFontSize = () => { + const length = amount.length; + if (length > 12) return fontSize.md; + if (length > 8) return fontSize.lg; + return fontSize.xl; + }; + + const isValidAmount = amount && Number(amount) > 0; + + const focusInput = () => { + const input = document.querySelector("#amount-input") as HTMLInputElement; + input?.focus(); + }; + + const handleQuickAmount = (usdAmount: number) => { + if (uiOptions.destinationToken.priceUsd === 0) { + return; + } + // Convert USD amount to token amount using token price + const tokenAmount = usdAmount / uiOptions.destinationToken.priceUsd; + // Format to reasonable decimal places (up to 6 decimals, remove trailing zeros) + const formattedAmount = Number.parseFloat( + tokenAmount.toFixed(6), + ).toString(); + setAmount(formattedAmount); + }; + + return ( + + + {/* Token Info */} + + + {/* Amount Input */} + +
) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + focusInput(); + } + }} + role="button" + tabIndex={0} + > + + { + // put cursor at the end of the input + if (amount === "") { + e.currentTarget.setSelectionRange( + e.currentTarget.value.length, + e.currentTarget.value.length, + ); + } + }} + onChange={(e) => { + handleAmountChange(e.target.value); + }} + style={{ + fontSize: getAmountFontSize(), + fontWeight: 600, + textAlign: "right", + padding: "0", + border: "none", + boxShadow: "none", + }} + /> + +
+ + {/* Fiat Value */} + + + ≈ $ + {(Number(amount) * uiOptions.destinationToken.priceUsd).toFixed( + 2, + )} + + +
+
+ + {/* Quick Amount Buttons */} + {presetOptions && ( + <> + + + {presetOptions?.map((amount) => ( + + ))} + + + )} + + + + + {receiver ? ( + + ) : ( + <> + + + No Wallet Connected + + + )} + +
+ + + + {/* Continue Button */} + {receiver ? ( + + ) : ( + + )} + + + + + +
+ ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/QuoteLoader.tsx b/packages/thirdweb/src/react/web/ui/Bridge/QuoteLoader.tsx new file mode 100644 index 00000000000..20a7c6b9331 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/QuoteLoader.tsx @@ -0,0 +1,219 @@ +"use client"; +import { useEffect } from "react"; +import type { Token } from "../../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../../../constants/addresses.js"; +import { toUnits } from "../../../../utils/units.js"; +import { + type BridgePrepareRequest, + type BridgePrepareResult, + type UseBridgePrepareParams, + useBridgePrepare, +} from "../../../core/hooks/useBridgePrepare.js"; +import type { PaymentMethod } from "../../../core/machines/paymentMachine.js"; +import { Spacer } from "../components/Spacer.js"; +import { Spinner } from "../components/Spinner.js"; +import { Container } from "../components/basic.js"; +import { Text } from "../components/text.js"; + +export interface QuoteLoaderProps { + /** + * The destination token to bridge to + */ + destinationToken: Token; + + /** + * The payment method to use + */ + paymentMethod: PaymentMethod; + + /** + * The amount to bridge (as string) + */ + amount: string; + + /** + * The sender address + */ + sender?: string; + + /** + * The receiver address (defaults to sender for fund_wallet mode) + */ + receiver: string; + + /** + * ThirdwebClient for API calls + */ + client: ThirdwebClient; + + /** + * Called when a quote is successfully received + */ + onQuoteReceived: ( + preparedQuote: BridgePrepareResult, + request: BridgePrepareRequest, + ) => void; + + /** + * Called when an error occurs + */ + onError: (error: Error) => void; + + /** + * Called when user wants to go back + */ + onBack?: () => void; + + /** + * Optional purchase data for the payment + */ + purchaseData?: object; + + /** + * Optional payment link ID for the payment + */ + paymentLinkId?: string; + + /** + * Fee payer for direct transfers (defaults to sender) + */ + feePayer?: "sender" | "receiver"; +} + +export function QuoteLoader({ + destinationToken, + paymentMethod, + amount, + sender, + receiver, + client, + onQuoteReceived, + onError, + purchaseData, + paymentLinkId, + feePayer, +}: QuoteLoaderProps) { + // For now, we'll use a simple buy operation + // This will be expanded to handle different bridge types based on the payment method + const request: BridgePrepareRequest = getBridgeParams({ + paymentMethod, + amount, + destinationToken, + receiver, + sender, + client, + purchaseData, + paymentLinkId, + feePayer, + }); + const prepareQuery = useBridgePrepare(request); + + // Handle successful quote + useEffect(() => { + if (prepareQuery.data) { + onQuoteReceived(prepareQuery.data, request); + } + }, [prepareQuery.data, onQuoteReceived, request]); + + // Handle errors + useEffect(() => { + if (prepareQuery.error) { + onError(prepareQuery.error as Error); + } + }, [prepareQuery.error, onError]); + + return ( + + + + + Finding the best route... + + + + We're searching for the most efficient path for this payment. + + + ); +} + +function getBridgeParams(args: { + paymentMethod: PaymentMethod; + amount: string; + destinationToken: Token; + receiver: string; + client: ThirdwebClient; + sender?: string; + feePayer?: "sender" | "receiver"; + purchaseData?: object; + paymentLinkId?: string; +}): UseBridgePrepareParams { + const { paymentMethod, amount, destinationToken, receiver, client, sender } = + args; + + switch (paymentMethod.type) { + case "fiat": + return { + type: "onramp", + client, + amount: toUnits(amount, destinationToken.decimals), + receiver, + sender, + chainId: destinationToken.chainId, + tokenAddress: destinationToken.address, + onramp: paymentMethod.onramp || "coinbase", + purchaseData: args.purchaseData, + currency: paymentMethod.currency, + onrampTokenAddress: NATIVE_TOKEN_ADDRESS, // always onramp to native token + paymentLinkId: args.paymentLinkId, + enabled: !!(destinationToken && amount && client), + }; + case "wallet": + // if the origin token is the same as the destination token, use transfer type + if ( + paymentMethod.originToken.chainId === destinationToken.chainId && + paymentMethod.originToken.address.toLowerCase() === + destinationToken.address.toLowerCase() + ) { + return { + type: "transfer", + client, + chainId: destinationToken.chainId, + tokenAddress: destinationToken.address, + feePayer: args.feePayer || "sender", + amount: toUnits(amount, destinationToken.decimals), + sender: + sender || + paymentMethod.payerWallet.getAccount()?.address || + receiver, + receiver, + purchaseData: args.purchaseData, + paymentLinkId: args.paymentLinkId, + enabled: !!(destinationToken && amount && client), + }; + } + + return { + type: "buy", + client, + originChainId: paymentMethod.originToken.chainId, + originTokenAddress: paymentMethod.originToken.address, + destinationChainId: destinationToken.chainId, + destinationTokenAddress: destinationToken.address, + amount: toUnits(amount, destinationToken.decimals), + sender: + sender || paymentMethod.payerWallet.getAccount()?.address || receiver, + receiver, + purchaseData: args.purchaseData, + paymentLinkId: args.paymentLinkId, + enabled: !!(destinationToken && amount && client), + }; + } +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/StepRunner.tsx b/packages/thirdweb/src/react/web/ui/Bridge/StepRunner.tsx new file mode 100644 index 00000000000..c510bd12469 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/StepRunner.tsx @@ -0,0 +1,421 @@ +"use client"; +import { CheckIcon, ClockIcon, Cross1Icon } from "@radix-ui/react-icons"; +import type { RouteStep } from "../../../../bridge/types/Route.js"; +import type { Chain } from "../../../../chains/types.js"; +import { defineChain } from "../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; +import type { WindowAdapter } from "../../../core/adapters/WindowAdapter.js"; +import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; +import { + iconSize, + radius, + spacing, +} from "../../../core/design-system/index.js"; +import { + type CompletedStatusResult, + useStepExecutor, +} from "../../../core/hooks/useStepExecutor.js"; +import { ChainName } from "../components/ChainName.js"; +import { Spacer } from "../components/Spacer.js"; +import { Spinner } from "../components/Spinner.js"; +import { Container, ModalHeader } from "../components/basic.js"; +import { Button } from "../components/buttons.js"; +import { Text } from "../components/text.js"; +import type { BridgePrepareRequest } from "../../../core/hooks/useBridgePrepare.js"; + +export interface StepRunnerProps { + request: BridgePrepareRequest; + + /** + * Wallet instance for executing transactions + */ + wallet: Wallet; + + /** + * Thirdweb client for API calls + */ + client: ThirdwebClient; + + /** + * Window adapter for opening URLs (web/RN) + */ + windowAdapter: WindowAdapter; + + /** + * Whether to automatically start the transaction process + */ + autoStart?: boolean; + + /** + * Called when all steps are completed - receives array of completed status results + */ + onComplete: (completedStatuses: CompletedStatusResult[]) => void; + + /** + * Called when user cancels the flow + */ + onCancel?: () => void; + + /** + * Called when user clicks the back button + */ + onBack?: () => void; +} + +export function StepRunner({ + request, + wallet, + client, + windowAdapter, + onComplete, + onCancel, + onBack, + autoStart, +}: StepRunnerProps) { + const theme = useCustomTheme(); + + // Use the real step executor hook + const { + currentStep, + progress, + executionState, + onrampStatus, + steps, + error, + start, + cancel, + retry, + } = useStepExecutor({ + request, + wallet, + client, + windowAdapter, + autoStart, + onComplete: (completedStatuses: CompletedStatusResult[]) => { + onComplete(completedStatuses); + }, + }); + + const handleCancel = () => { + cancel(); + if (onCancel) { + onCancel(); + } + }; + + const handleRetry = () => { + retry(); + }; + + const getStepStatus = ( + stepIndex: number, + ): "pending" | "executing" | "completed" | "failed" => { + if (!currentStep || !steps) { + // Not started yet + return stepIndex === 0 ? (error ? "failed" : "pending") : "pending"; + } + + const currentStepIndex = steps.findIndex((step) => step === currentStep); + + if (stepIndex < currentStepIndex) return "completed"; + if (stepIndex === currentStepIndex && executionState === "executing") + return "executing"; + if (stepIndex === currentStepIndex && error) return "failed"; + if ( + stepIndex === currentStepIndex && + executionState === "idle" && + progress === 100 + ) + return "completed"; + + return "pending"; + }; + + const getStatusIcon = ( + status: "pending" | "executing" | "completed" | "failed", + ) => { + switch (status) { + case "completed": + return ( + + ); + case "executing": + return ; + case "failed": + return ( + + ); + default: + return ( + + ); + } + }; + + const getStepBackgroundColor = ( + status: "pending" | "executing" | "completed" | "failed", + ) => { + switch (status) { + case "completed": + return theme.colors.tertiaryBg; + case "executing": + return theme.colors.tertiaryBg; + case "failed": + return theme.colors.tertiaryBg; + default: + return theme.colors.tertiaryBg; + } + }; + + const getIconBackgroundColor = ( + status: "pending" | "executing" | "completed" | "failed", + ) => { + switch (status) { + case "completed": + return theme.colors.success; + case "executing": + return theme.colors.accentButtonBg; + case "failed": + return theme.colors.danger; + default: + return theme.colors.borderColor; + } + }; + + const getStepDescription = (step: RouteStep) => { + const { originToken, destinationToken } = step; + + // If tokens are the same, it's likely a bridge operation + if (originToken.chainId !== destinationToken.chainId) { + return ( + + + Bridge {originToken.symbol} to{" "} + + + + ); + } + + // If different tokens on same chain, it's a swap + if (originToken.symbol !== destinationToken.symbol) { + return ( + + Swap {originToken.symbol} to {destinationToken.symbol} + + ); + } + + // Fallback to step number + return ( + + Process transaction + + ); + }; + + const getStepStatusText = ( + status: "pending" | "executing" | "completed" | "failed", + ) => { + switch (status) { + case "executing": + return "Processing..."; + case "completed": + return "Completed"; + case "pending": + return "Waiting..."; + case "failed": + return "Failed"; + default: + return "Unknown"; + } + }; + + return ( + + + + + + + {/* Progress Bar */} + + + + Progress + + + {progress}% + + + + + + + + + + + + + {/* Steps List */} + + {request.type === "onramp" && onrampStatus ? ( + + + {getStatusIcon(onrampStatus)} + + + + + TEST + + + {getStepStatusText(onrampStatus)} + + + + ) : null} + {steps?.map((step, index) => { + const status = getStepStatus(index); + + return ( + + + {getStatusIcon(status)} + + + + {getStepDescription(step)} + + {getStepStatusText(status)} + + + + ); + })} + + + + + Keep this window open until all +
transactions are complete. +
+ + + + {/* Action Buttons */} + {error ? ( + + + + ) : executionState === "idle" && progress === 0 ? ( + + ) : executionState === "executing" || + executionState === "auto-starting" ? ( + + ) : null} +
+
+ ); +} + +function getDestinationChain(request: BridgePrepareRequest): Chain { + switch (request.type) { + case "onramp": + return defineChain(request.chainId); + case "buy": + case "sell": + return defineChain(request.destinationChainId); + case "transfer": + return defineChain(request.chainId); + default: + throw new Error("Invalid quote type"); + } +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/TransactionPayment.tsx b/packages/thirdweb/src/react/web/ui/Bridge/TransactionPayment.tsx new file mode 100644 index 00000000000..f847b1ec3b5 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/TransactionPayment.tsx @@ -0,0 +1,403 @@ +"use client"; +import type { Token } from "../../../../bridge/index.js"; +import type { ThirdwebClient } from "../../../../client/client.js"; +import { + type Address, + getAddress, + shortenAddress, +} from "../../../../utils/address.js"; +import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; +import { fontSize, spacing } from "../../../core/design-system/index.js"; +import { useChainMetadata } from "../../../core/hooks/others/useChainQuery.js"; +import { useTransactionDetails } from "../../../core/hooks/useTransactionDetails.js"; +import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; +import { ConnectButton } from "../ConnectWallet/ConnectButton.js"; +import { PoweredByThirdweb } from "../ConnectWallet/PoweredByTW.js"; +import type { PayEmbedConnectOptions } from "../PayEmbed.js"; +import { ChainName } from "../components/ChainName.js"; +import { Spacer } from "../components/Spacer.js"; +import { Container, Line } from "../components/basic.js"; +import { Button } from "../components/buttons.js"; +import { Text } from "../components/text.js"; +import type { UIOptions } from "./BridgeOrchestrator.js"; +import { ChainIcon } from "./common/TokenAndChain.js"; +import { WithHeader } from "./common/WithHeader.js"; + +export interface TransactionPaymentProps { + /** + * UI configuration and mode + */ + uiOptions: Extract; + + /** + * ThirdwebClient for blockchain interactions + */ + client: ThirdwebClient; + + /** + * Called when user confirms transaction execution + */ + onContinue: (amount: string, token: Token, receiverAddress: Address) => void; + + /** + * Connect options for wallet connection + */ + connectOptions?: PayEmbedConnectOptions; +} + +export function TransactionPayment({ + uiOptions, + client, + onContinue, + connectOptions, +}: TransactionPaymentProps) { + const theme = useCustomTheme(); + const activeAccount = useActiveAccount(); + + // Get chain metadata for native currency symbol + const chainMetadata = useChainMetadata(uiOptions.transaction.chain); + + // Use the extracted hook for transaction details + const transactionDataQuery = useTransactionDetails({ + transaction: uiOptions.transaction, + client, + }); + + const contractName = + transactionDataQuery.data?.contractMetadata?.name || "Unknown Contract"; + const functionName = + transactionDataQuery.data?.functionInfo?.functionName || "Contract Call"; + const isLoading = transactionDataQuery.isLoading || chainMetadata.isLoading; + + const buttonLabel = `Execute ${functionName}`; + + // Skeleton component for loading state + const SkeletonRow = ({ width = "100%" }: { width?: string }) => ( + +
+
+ + ); + + const SkeletonHeader = () => ( + + {/* USD Value Skeleton */} +
+ + {/* Function Name Skeleton */} +
+ + ); + + if (isLoading) { + return ( + + {/* Loading Header */} + + + + + + + + + {/* Loading Rows */} + + + + + + + + + + + + + + + + + {/* Loading Button */} +
+ + + + + + + ); + } + + return ( + + {/* Cost and Function Name section */} + + {/* USD Value */} + + {transactionDataQuery.data?.usdValueDisplay || + transactionDataQuery.data?.txCostDisplay} + + + {/* Function Name */} + + {functionName} + + + + + + + + + + {/* Contract Info */} + + + Contract + + + {contractName} + + + + + + {/* Address */} + + + Address + + + {shortenAddress(uiOptions.transaction.to as string)} + + + + + + {/* Network */} + + + Network + + + + + + + + + + {/* Cost */} + {transactionDataQuery.data?.txCostDisplay && ( + <> + + + Cost + + + {transactionDataQuery.data?.txCostDisplay} + + + + + + )} + + {/* Network Fees */} + {transactionDataQuery.data?.gasCostDisplay && ( + <> + + + Network fees + + + {transactionDataQuery.data?.gasCostDisplay} + + + + + + )} + + + + + + {/* Action Button */} + {activeAccount ? ( + + ) : ( + + )} + + + + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/UnsupportedTokenScreen.tsx b/packages/thirdweb/src/react/web/ui/Bridge/UnsupportedTokenScreen.tsx new file mode 100644 index 00000000000..98067d91332 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/UnsupportedTokenScreen.tsx @@ -0,0 +1,84 @@ +import type { Chain } from "../../../../chains/types.js"; +import { iconSize } from "../../../core/design-system/index.js"; +import { useChainMetadata } from "../../../core/hooks/others/useChainQuery.js"; +import { AccentFailIcon } from "../ConnectWallet/icons/AccentFailIcon.js"; +import { Spacer } from "../components/Spacer.js"; +import { Container } from "../components/basic.js"; +import { Text } from "../components/text.js"; + +export interface UnsupportedTokenScreenProps { + /** + * The chain the token is on + */ + chain: Chain; +} + +/** + * Screen displayed when a specified token is not supported by the Bridge API + * @internal + */ +export function UnsupportedTokenScreen(props: UnsupportedTokenScreenProps) { + const { chain } = props; + + const { data: chainMetadata } = useChainMetadata(chain); + + if (chainMetadata?.testnet) { + return ( + + {/* Error Icon */} + + + + {/* Title */} + + Testnet Not Supported + + + + {/* Description */} + + The Universal Bridge does not support testnets at this time. + + + ); + } + + return ( + + {/* Error Icon */} + + + + {/* Title */} + + Token Not Supported + + + + {/* Description */} + + This token or chain is not supported by the Universal Bridge. + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/TokenAndChain.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/TokenAndChain.tsx new file mode 100644 index 00000000000..f303060e5c1 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/common/TokenAndChain.tsx @@ -0,0 +1,203 @@ +import { useMemo } from "react"; +import type { Token } from "../../../../../bridge/index.js"; +import type { Chain } from "../../../../../chains/types.js"; +import { getCachedChain } from "../../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js"; +import { resolveScheme } from "../../../../../utils/ipfs.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { iconSize, spacing } from "../../../../core/design-system/index.js"; +import { + useChainIconUrl, + useChainMetadata, +} from "../../../../core/hooks/others/useChainQuery.js"; +import { genericTokenIcon } from "../../../../core/utils/walletIcon.js"; +import { isNativeToken } from "../../ConnectWallet/screens/nativeToken.js"; +import { ChainName } from "../../components/ChainName.js"; +import { Img } from "../../components/Img.js"; +import { Container } from "../../components/basic.js"; +import { fallbackChainIcon } from "../../components/fallbackChainIcon.js"; +import { Text } from "../../components/text.js"; + +export function TokenAndChain({ + token, + client, + size, + style, +}: { + token: Omit; + client: ThirdwebClient; + size: keyof typeof iconSize; + style?: React.CSSProperties; +}) { + const theme = useCustomTheme(); + const chain = getCachedChain(token.chainId); + return ( + + + + {chain.id !== 1 && ( + + + + )} + + + + + {token.name} + + + + + ); +} + +export function TokenIconWithFallback(props: { + token: Omit; + size: keyof typeof iconSize; + client: ThirdwebClient; +}) { + const chain = getCachedChain(props.token.chainId); + const chainMeta = useChainMetadata(chain).data; + const theme = useCustomTheme(); + + const tokenImage = useMemo(() => { + if ( + isNativeToken(props.token) || + props.token.address === NATIVE_TOKEN_ADDRESS + ) { + if (chainMeta?.nativeCurrency.symbol === "ETH") { + return "ipfs://QmcxZHpyJa8T4i63xqjPYrZ6tKrt55tZJpbXcjSDKuKaf9/ethereum/512.png"; // ETH icon + } + return chainMeta?.icon?.url; + } + return props.token.iconUri; + }, [props.token, chainMeta?.icon?.url, chainMeta?.nativeCurrency.symbol]); + + return tokenImage ? ( + + ) : ( + + + {props.token.symbol.slice(0, 1)} + + + ); +} + +export const ChainIcon: React.FC<{ + chain: Chain; + size: keyof typeof iconSize; + client: ThirdwebClient; +}> = (props) => { + const { url } = useChainIconUrl(props.chain); + return ( + + + + ); +}; + +const getSrcChainIcon = (props: { + client: ThirdwebClient; + chainIconUrl?: string; +}) => { + const url = props.chainIconUrl; + if (!url) { + return fallbackChainIcon; + } + try { + return resolveScheme({ + uri: url, + client: props.client, + }); + } catch { + return fallbackChainIcon; + } +}; diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx new file mode 100644 index 00000000000..6f5ba2a3312 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx @@ -0,0 +1,112 @@ +import styled from "@emotion/styled"; +import type { Token } from "../../../../../bridge/index.js"; +import { getCachedChain } from "../../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { spacing } from "../../../../core/design-system/index.js"; +import { FiatValue } from "../../ConnectWallet/screens/Buy/swap/FiatValue.js"; +import { Container } from "../../components/basic.js"; +import { Button } from "../../components/buttons.js"; +import { Text } from "../../components/text.js"; +import { TokenAndChain } from "./TokenAndChain.js"; + +export function TokenBalanceRow({ + client, + token, + amount, + onClick, + style, +}: { + client: ThirdwebClient; + token: Token; + amount: string; + onClick: (token: Token) => void; + style?: React.CSSProperties; +}) { + const chain = getCachedChain(token.chainId); + return ( + onClick(token)} + variant="secondary" + style={{ + display: "flex", + justifyContent: "space-between", + padding: `${spacing.sm} ${spacing.md}`, + ...style, + }} + > + + + + + + + {`${Number(amount).toLocaleString(undefined, { + maximumFractionDigits: 6, + minimumFractionDigits: 0, + })} ${token.symbol}`} + + + + + ); +} + +const StyledButton = /* @__PURE__ */ styled(Button)((props) => { + const theme = useCustomTheme(); + return { + background: "transparent", + justifyContent: "space-between", + flexWrap: "nowrap", + flexDirection: "row", + padding: spacing.sm, + paddingRight: spacing.xs, + gap: spacing.sm, + "&:hover": { + background: theme.colors.secondaryButtonBg, + }, + transition: "background 200ms ease, transform 150ms ease", + ...props.style, + }; +}); diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/WithHeader.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/WithHeader.tsx new file mode 100644 index 00000000000..359006e00ba --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/common/WithHeader.tsx @@ -0,0 +1,65 @@ +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { resolveScheme } from "../../../../../utils/ipfs.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { radius } from "../../../../core/design-system/index.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container } from "../../components/basic.js"; +import { Text } from "../../components/text.js"; +import type { UIOptions } from "../BridgeOrchestrator.js"; + +export function WithHeader({ + children, + uiOptions, + defaultTitle, + client, +}: { + children: React.ReactNode; + uiOptions: UIOptions; + defaultTitle: string; + client: ThirdwebClient; +}) { + const theme = useCustomTheme(); + return ( + + {/* image */} + {uiOptions.metadata?.image && ( +
+ )} + + + + {/* title */} + + {uiOptions.metadata?.title || defaultTitle} + + + {/* Description */} + {uiOptions.metadata?.description && ( + <> + + + {uiOptions.metadata?.description} + + + )} + + + {children} + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentDetails.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentDetails.tsx new file mode 100644 index 00000000000..13e1fba6f20 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentDetails.tsx @@ -0,0 +1,318 @@ +"use client"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { radius, spacing } from "../../../../core/design-system/index.js"; +import type { BridgePrepareResult } from "../../../../core/hooks/useBridgePrepare.js"; +import type { PaymentMethod } from "../../../../core/machines/paymentMachine.js"; +import {} from "../../ConnectWallet/screens/Buy/fiat/currencies.js"; +import { + formatCurrencyAmount, + formatTokenAmount, +} from "../../ConnectWallet/screens/formatTokenBalance.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container, ModalHeader } from "../../components/basic.js"; +import { Button } from "../../components/buttons.js"; +import { Text } from "../../components/text.js"; +import type { UIOptions } from "../BridgeOrchestrator.js"; +import { PaymentOverview } from "./PaymentOverview.js"; + +export interface PaymentDetailsProps { + /** + * The UI mode to use + */ + uiOptions: UIOptions; + /** + * The client to use + */ + client: ThirdwebClient; + /** + * The payment method to use + */ + paymentMethod: PaymentMethod; + /** + * The prepared quote to preview + */ + preparedQuote: BridgePrepareResult; + + /** + * Called when user confirms the route + */ + onConfirm: () => void; + + /** + * Called when user wants to go back + */ + onBack: () => void; + + /** + * Called when an error occurs + */ + onError: (error: Error) => void; +} + +export function PaymentDetails({ + uiOptions, + client, + paymentMethod, + preparedQuote, + onConfirm, + onBack, + onError, +}: PaymentDetailsProps) { + const theme = useCustomTheme(); + + const handleConfirm = () => { + try { + onConfirm(); + } catch (error) { + onError(error as Error); + } + }; + + // Extract common data based on quote type + const getDisplayData = () => { + switch (preparedQuote.type) { + case "transfer": { + const token = + paymentMethod.type === "wallet" + ? paymentMethod.originToken + : undefined; + if (!token) { + // can never happen + onError(new Error("Invalid payment method")); + return { + originToken: undefined, + destinationToken: undefined, + originAmount: "0", + destinationAmount: "0", + estimatedTime: 0, + }; + } + return { + originToken: token, + destinationToken: token, + originAmount: formatTokenAmount( + preparedQuote.originAmount, + token.decimals, + ), + destinationAmount: formatTokenAmount( + preparedQuote.destinationAmount, + token.decimals, + ), + estimatedTime: preparedQuote.estimatedExecutionTimeMs, + }; + } + case "buy": { + const method = + paymentMethod.type === "wallet" ? paymentMethod : undefined; + if (!method) { + // can never happen + onError(new Error("Invalid payment method")); + return { + originToken: undefined, + destinationToken: undefined, + originAmount: "0", + destinationAmount: "0", + estimatedTime: 0, + }; + } + return { + originToken: + paymentMethod.type === "wallet" + ? paymentMethod.originToken + : undefined, + destinationToken: + preparedQuote.steps[preparedQuote.steps.length - 1] + ?.destinationToken, + originAmount: formatTokenAmount( + preparedQuote.originAmount, + method.originToken.decimals, + ), + destinationAmount: formatTokenAmount( + preparedQuote.destinationAmount, + preparedQuote.steps[preparedQuote.steps.length - 1] + ?.destinationToken?.decimals ?? 18, + ), + estimatedTime: preparedQuote.estimatedExecutionTimeMs, + }; + } + case "onramp": { + const method = + paymentMethod.type === "fiat" ? paymentMethod : undefined; + if (!method) { + // can never happen + onError(new Error("Invalid payment method")); + return { + originToken: undefined, + destinationToken: undefined, + originAmount: "0", + destinationAmount: "0", + estimatedTime: 0, + }; + } + return { + originToken: undefined, // Onramp starts with fiat + destinationToken: preparedQuote.destinationToken, + originAmount: formatCurrencyAmount( + method.currency, + Number(preparedQuote.currencyAmount), + ), + destinationAmount: formatTokenAmount( + preparedQuote.destinationAmount, + preparedQuote.destinationToken.decimals, + ), + estimatedTime: undefined, + }; + } + default: { + throw new Error( + `Unsupported bridge prepare type: ${preparedQuote.type}`, + ); + } + } + }; + + const displayData = getDisplayData(); + + return ( + + + + + + + {/* Quote Summary */} + + {displayData.destinationToken && ( + + )} + + + + + + Estimated Time + + + {displayData.estimatedTime + ? `~${Math.ceil(displayData.estimatedTime / 60000)} min` + : "~2 min"} + + + + {preparedQuote.steps.length > 1 ? ( + + + Route Length + + + {preparedQuote.steps.length} step + {preparedQuote.steps.length !== 1 ? "s" : ""} + + + ) : null} + + + + {/* Route Steps */} + {preparedQuote.steps.length > 1 && ( + + + + + {preparedQuote.steps.map((step, stepIndex) => ( + + {/* Step Header */} + + + + {stepIndex + 1} + + + + + + + {step.originToken.symbol} →{" "} + {step.destinationToken.symbol} + + + {step.originToken.name} to{" "} + {step.destinationToken.name} + + + + + + ))} + + + )} + + + + {/* Action Buttons */} + + + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentOverview.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentOverview.tsx new file mode 100644 index 00000000000..d51918c5380 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentOverview.tsx @@ -0,0 +1,301 @@ +import type { Token } from "../../../../../bridge/index.js"; +import { defineChain } from "../../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { radius, spacing } from "../../../../core/design-system/index.js"; +import { useTransactionDetails } from "../../../../core/hooks/useTransactionDetails.js"; +import type { PaymentMethod } from "../../../../core/machines/paymentMachine.js"; +import { getFiatCurrencyIcon } from "../../ConnectWallet/screens/Buy/fiat/currencies.js"; +import { FiatValue } from "../../ConnectWallet/screens/Buy/swap/FiatValue.js"; +import { StepConnectorArrow } from "../../ConnectWallet/screens/Buy/swap/StepConnector.js"; +import { WalletRow } from "../../ConnectWallet/screens/Buy/swap/WalletRow.js"; +import { Container } from "../../components/basic.js"; +import { Text } from "../../components/text.js"; +import type { UIOptions } from "../BridgeOrchestrator.js"; +import { TokenBalanceRow } from "../common/TokenBalanceRow.js"; + +export function PaymentOverview(props: { + uiOptions: UIOptions; + receiver: string; + sender?: string; + client: ThirdwebClient; + paymentMethod: PaymentMethod; + toToken: Token; + fromAmount: string; + toAmount: string; +}) { + const theme = useCustomTheme(); + const sender = + props.sender || + (props.paymentMethod.type === "wallet" + ? props.paymentMethod.payerWallet.getAccount()?.address + : undefined); + const isDifferentRecipient = + props.receiver.toLowerCase() !== sender?.toLowerCase(); + return ( + + {/* Sell */} + + {sender && ( + + + + )} + {props.paymentMethod.type === "wallet" && ( + {}} + style={{ + background: "transparent", + borderRadius: 0, + border: "none", + }} + /> + )} + {props.paymentMethod.type === "fiat" && ( + + + {getFiatCurrencyIcon({ + currency: props.paymentMethod.currency, + size: "lg", + })} + + + {props.paymentMethod.currency} + + + {props.paymentMethod.onramp.charAt(0).toUpperCase() + + props.paymentMethod.onramp.slice(1)} + + + + + {props.fromAmount} + + + )} + + {/* Connector Icon */} + + {/* Buy */} + + {isDifferentRecipient && ( + + + + )} + {props.uiOptions.mode === "direct_payment" && ( + + + + {props.uiOptions.metadata?.title || "Payment"} + + {props.uiOptions.metadata?.description && ( + + {props.uiOptions.metadata.description} + + )} + + + + + {props.uiOptions.paymentInfo.amount} {props.toToken.symbol} + + + + )} + {props.uiOptions.mode === "fund_wallet" && ( + {}} + style={{ + background: "transparent", + borderRadius: 0, + border: "none", + }} + /> + )} + {props.uiOptions.mode === "transaction" && ( + + )} + + + ); +} + +const TransactionOverViewCompact = (props: { + uiOptions: Extract; + client: ThirdwebClient; +}) => { + const theme = useCustomTheme(); + const txInfo = useTransactionDetails({ + transaction: props.uiOptions.transaction, + client: props.client, + }); + + if (!txInfo.data) { + // Skeleton loading state + return ( + + + {/* Title skeleton */} +
+ {/* Description skeleton - only if metadata exists */} + {props.uiOptions.metadata?.description && ( +
+ )} + + + {/* Function name skeleton */} +
+ + + ); + } + + return ( + + + + {props.uiOptions.metadata?.title || "Transaction"} + + {props.uiOptions.metadata?.description && ( + + {props.uiOptions.metadata.description} + + )} + + + + {txInfo.data.functionInfo.functionName} + + + + ); +}; diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx new file mode 100644 index 00000000000..5342e21ee77 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx @@ -0,0 +1,186 @@ +"use client"; +import { useMemo } from "react"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { checksumAddress } from "../../../../../utils/address.js"; +import { toTokens } from "../../../../../utils/units.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { + iconSize, + radius, + spacing, +} from "../../../../core/design-system/index.js"; +import { useBuyWithFiatQuotesForProviders } from "../../../../core/hooks/pay/useBuyWithFiatQuotesForProviders.js"; +import { Img } from "../../components/Img.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Spinner } from "../../components/Spinner.js"; +import { Container } from "../../components/basic.js"; +import { Button } from "../../components/buttons.js"; +import { Text } from "../../components/text.js"; + +export interface FiatProviderSelectionProps { + client: ThirdwebClient; + onProviderSelected: (provider: "coinbase" | "stripe" | "transak") => void; + toChainId: number; + toTokenAddress: string; + toAddress: string; + toAmount?: string; +} + +const PROVIDERS = [ + { + id: "coinbase" as const, + name: "Coinbase", + description: "Fast and secure payments", + iconUri: "https://i.ibb.co/LDJ3Rk2t/Frame-5.png", + }, + { + id: "stripe" as const, + name: "Stripe", + description: "Trusted payment processing", + iconUri: "https://i.ibb.co/CpgQC2Lf/images-3.png", + }, + { + id: "transak" as const, + name: "Transak", + description: "Global payment solution", + iconUri: "https://i.ibb.co/Xx2r882p/Transak-official-symbol-1.png", + }, +]; + +export function FiatProviderSelection({ + onProviderSelected, + client, + toChainId, + toTokenAddress, + toAddress, + toAmount, +}: FiatProviderSelectionProps) { + const theme = useCustomTheme(); + + // Fetch quotes for all providers + const quoteQueries = useBuyWithFiatQuotesForProviders({ + client, + chainId: toChainId, + tokenAddress: checksumAddress(toTokenAddress), + receiver: checksumAddress(toAddress), + amount: toAmount || "0", + currency: "USD", + }); + + const quotes = useMemo(() => { + return quoteQueries.map((q) => q.data).filter((q) => !!q); + }, [quoteQueries]); + + // TODO: add a "remember my choice" checkbox + + return ( + <> + + {quotes.length > 0 ? ( + quotes + .sort((a, b) => a.currencyAmount - b.currencyAmount) + .map((quote, index) => { + const provider = PROVIDERS.find( + (p) => p.id === quote.intent.onramp, + ); + if (!provider) { + return null; + } + + return ( + + + + ); + }) + ) : ( + + + + + Generating quotes... + + + )} + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx new file mode 100644 index 00000000000..a8ed9595913 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx @@ -0,0 +1,268 @@ +"use client"; +import { useEffect, useState } from "react"; +import type { Token } from "../../../../../bridge/types/Token.js"; +import { defineChain } from "../../../../../chains/utils.js"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { Address } from "../../../../../utils/address.js"; +import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; +import { usePaymentMethods } from "../../../../core/hooks/usePaymentMethods.js"; +import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js"; +import { useConnectedWallets } from "../../../../core/hooks/wallets/useConnectedWallets.js"; +import type { PaymentMethod } from "../../../../core/machines/paymentMachine.js"; +import type { ConnectLocale } from "../../ConnectWallet/locale/types.js"; +import { WalletSwitcherConnectionScreen } from "../../ConnectWallet/screens/WalletSwitcherConnectionScreen.js"; +import type { PayEmbedConnectOptions } from "../../PayEmbed.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container, ModalHeader } from "../../components/basic.js"; +import { FiatProviderSelection } from "./FiatProviderSelection.js"; +import { TokenSelection } from "./TokenSelection.js"; +import { WalletFiatSelection } from "./WalletFiatSelection.js"; +import { toUnits } from "../../../../../utils/units.js"; + +export interface PaymentSelectionProps { + /** + * The destination token to bridge to + */ + destinationToken: Token; + + /** + * The destination amount to bridge + */ + destinationAmount: string; + + /** + * The receiver address + */ + receiverAddress?: Address; + + /** + * ThirdwebClient for API calls + */ + client: ThirdwebClient; + + /** + * Called when user selects a payment method + */ + onPaymentMethodSelected: (paymentMethod: PaymentMethod) => void; + + /** + * Called when an error occurs + */ + onError: (error: Error) => void; + + /** + * Called when user wants to go back + */ + onBack?: () => void; + + /** + * Connect options for wallet connection + */ + connectOptions?: PayEmbedConnectOptions; + + /** + * Locale for connect UI + */ + connectLocale: ConnectLocale; + + /** + * Whether to include the destination token in the payment methods + */ + includeDestinationToken?: boolean; +} + +type Step = + | { type: "walletSelection" } + | { type: "tokenSelection"; selectedWallet: Wallet } + | { type: "fiatProviderSelection" } + | { type: "walletConnection" }; + +export function PaymentSelection({ + destinationToken, + client, + destinationAmount, + receiverAddress, + onPaymentMethodSelected, + onError, + onBack, + connectOptions, + connectLocale, + includeDestinationToken, +}: PaymentSelectionProps) { + const connectedWallets = useConnectedWallets(); + const activeWallet = useActiveWallet(); + + const [currentStep, setCurrentStep] = useState({ + type: "walletSelection", + }); + + const payerWallet = + currentStep.type === "tokenSelection" + ? currentStep.selectedWallet + : activeWallet; + const { + data: paymentMethods, + isLoading: paymentMethodsLoading, + error: paymentMethodsError, + } = usePaymentMethods({ + destinationToken, + destinationAmount, + client, + includeDestinationToken: + includeDestinationToken || + receiverAddress?.toLowerCase() !== + payerWallet?.getAccount()?.address?.toLowerCase(), + payerWallet, + }); + + // Handle error from usePaymentMethods + useEffect(() => { + if (paymentMethodsError) { + onError(paymentMethodsError as Error); + } + }, [paymentMethodsError, onError]); + + const handlePaymentMethodSelected = (paymentMethod: PaymentMethod) => { + try { + onPaymentMethodSelected(paymentMethod); + } catch (error) { + onError(error as Error); + } + }; + + const handleWalletSelected = (wallet: Wallet) => { + setCurrentStep({ type: "tokenSelection", selectedWallet: wallet }); + }; + + const handleConnectWallet = async () => { + setCurrentStep({ type: "walletConnection" }); + }; + + const handleFiatSelected = () => { + setCurrentStep({ type: "fiatProviderSelection" }); + }; + + const handleBackToWalletSelection = () => { + setCurrentStep({ type: "walletSelection" }); + }; + + const handleOnrampProviderSelected = ( + provider: "coinbase" | "stripe" | "transak", + ) => { + if (!payerWallet) { + onError(new Error("No wallet available for fiat payment")); + return; + } + + const fiatPaymentMethod: PaymentMethod = { + type: "fiat", + payerWallet, + currency: "USD", // Default to USD for now + onramp: provider, + }; + handlePaymentMethodSelected(fiatPaymentMethod); + }; + + const getStepTitle = () => { + switch (currentStep.type) { + case "walletSelection": + return "Choose Payment Method"; + case "tokenSelection": + return "Select Token"; + case "fiatProviderSelection": + return "Select Payment Provider"; + case "walletConnection": + return "Connect Wallet"; + } + }; + + const getBackHandler = () => { + switch (currentStep.type) { + case "walletSelection": + return onBack; + case "tokenSelection": + case "fiatProviderSelection": + case "walletConnection": + return handleBackToWalletSelection; + } + }; + + // Handle rendering WalletSwitcherConnectionScreen + if (currentStep.type === "walletConnection") { + const destinationChain = destinationToken + ? defineChain(destinationToken.chainId) + : undefined; + const chains = destinationChain + ? [destinationChain, ...(connectOptions?.chains || [])] + : connectOptions?.chains; + + return ( + w.id !== "inApp")} + /> + ); + } + + return ( + + + + + + + {currentStep.type === "walletSelection" && ( + + )} + + {currentStep.type === "tokenSelection" && ( + + )} + + {currentStep.type === "fiatProviderSelection" && ( + + )} + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx new file mode 100644 index 00000000000..1080508a4f0 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx @@ -0,0 +1,282 @@ +"use client"; +import type { Token } from "../../../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { radius, spacing } from "../../../../core/design-system/index.js"; +import { useBridgeQuote } from "../../../../core/hooks/useBridgeQuote.js"; +import type { PaymentMethod } from "../../../../core/machines/paymentMachine.js"; +import { formatTokenAmount } from "../../ConnectWallet/screens/formatTokenBalance.js"; +import { Skeleton } from "../../components/Skeleton.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container } from "../../components/basic.js"; +import { Button } from "../../components/buttons.js"; +import { Text } from "../../components/text.js"; +import { TokenAndChain } from "../common/TokenAndChain.js"; + +export interface TokenSelectionProps { + paymentMethods: PaymentMethod[]; + paymentMethodsLoading: boolean; + client: ThirdwebClient; + onPaymentMethodSelected: (paymentMethod: PaymentMethod) => void; + onBack: () => void; + destinationToken: Token; + destinationAmount: bigint; +} + +// Individual payment method token row component +interface PaymentMethodTokenRowProps { + paymentMethod: PaymentMethod & { type: "wallet" }; + destinationToken: Token; + destinationAmount: bigint; + client: ThirdwebClient; + onPaymentMethodSelected: (paymentMethod: PaymentMethod) => void; +} + +function PaymentMethodTokenRow({ + paymentMethod, + destinationToken, + destinationAmount, + client, + onPaymentMethodSelected, +}: PaymentMethodTokenRowProps) { + const theme = useCustomTheme(); + + // Fetch individual quote for this specific token pair + const { + data: quote, + isLoading: quoteLoading, + error: quoteError, + } = useBridgeQuote({ + originToken: paymentMethod.originToken, + destinationToken, + destinationAmount, + client, + }); + + // Use the fetched originAmount if available, otherwise fall back to the one from paymentMethod + const displayOriginAmount = quote?.originAmount; + const hasEnoughBalance = displayOriginAmount + ? paymentMethod.balance >= displayOriginAmount + : false; + + return ( + + ); +} + +export function TokenSelection({ + paymentMethods, + paymentMethodsLoading, + client, + onPaymentMethodSelected, + onBack, + destinationToken, + destinationAmount, +}: TokenSelectionProps) { + const theme = useCustomTheme(); + + if (paymentMethodsLoading) { + return ( + <> + + Loading your tokens + + + + {/* Skeleton rows matching PaymentMethodTokenRow structure */} + {[1, 2, 3].map((i) => ( + + + {/* Left side: Token icon and name skeleton */} + + {/* Token icon skeleton */} +
+ + {/* Token name skeleton */} + + {/* Chain name skeleton */} + + + + + {/* Right side: Price and balance skeleton */} + + {/* Price amount skeleton */} + + {/* Balance skeleton */} + + + + + + + + ))} + + + ); + } + + if (paymentMethods.length === 0) { + return ( + + + No available tokens found for this wallet + + + + Try connecting a different wallet or pay with card + + + + + ); + } + + return ( + <> + + Select payment token + + + + {paymentMethods + .filter((method) => method.type === "wallet") + .map((method) => ( + + ))} + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/WalletFiatSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/WalletFiatSelection.tsx new file mode 100644 index 00000000000..9f0c41d3396 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/WalletFiatSelection.tsx @@ -0,0 +1,172 @@ +"use client"; +import { ChevronRightIcon, PlusIcon } from "@radix-ui/react-icons"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { + iconSize, + radius, + spacing, +} from "../../../../core/design-system/index.js"; +import { CreditCardIcon } from "../../ConnectWallet/icons/CreditCardIcon.js"; +import { WalletRow } from "../../ConnectWallet/screens/Buy/swap/WalletRow.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container } from "../../components/basic.js"; +import { Button } from "../../components/buttons.js"; +import { Text } from "../../components/text.js"; + +export interface WalletFiatSelectionProps { + connectedWallets: Wallet[]; + client: ThirdwebClient; + onWalletSelected: (wallet: Wallet) => void; + onFiatSelected: () => void; + onConnectWallet: () => void; +} + +export function WalletFiatSelection({ + connectedWallets, + client, + onWalletSelected, + onFiatSelected, + onConnectWallet, +}: WalletFiatSelectionProps) { + const theme = useCustomTheme(); + + return ( + <> + + Pay with Crypto + + + {/* Connected Wallets */} + {connectedWallets.length > 0 && ( + <> + + {connectedWallets.map((wallet) => { + const account = wallet.getAccount(); + if (!account?.address) { + return null; + } + return ( + + ); + })} + + + + )} + + {/* Connect Another Wallet */} + + + + + {/* Pay with Debit Card */} + + Pay with Fiat + + + + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-success/PaymentReceipt.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-success/PaymentReceipt.tsx new file mode 100644 index 00000000000..b2c9ac52d30 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-success/PaymentReceipt.tsx @@ -0,0 +1,392 @@ +"use client"; +import { CopyIcon } from "@radix-ui/react-icons"; +import { useQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; +import type { Token } from "../../../../../bridge/types/Token.js"; +import type { ChainMetadata } from "../../../../../chains/types.js"; +import { + defineChain, + getCachedChain, + getChainMetadata, +} from "../../../../../chains/utils.js"; +import { shortenHex } from "../../../../../utils/address.js"; +import { formatExplorerTxUrl } from "../../../../../utils/url.js"; +import type { WindowAdapter } from "../../../../core/adapters/WindowAdapter.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { + iconSize, + radius, + spacing, +} from "../../../../core/design-system/index.js"; +import type { BridgePrepareResult } from "../../../../core/hooks/useBridgePrepare.js"; +import type { CompletedStatusResult } from "../../../../core/hooks/useStepExecutor.js"; +import { formatTokenAmount } from "../../ConnectWallet/screens/formatTokenBalance.js"; +import { shorterChainName } from "../../components/ChainName.js"; +import { Skeleton } from "../../components/Skeleton.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container, ModalHeader } from "../../components/basic.js"; +import { Text } from "../../components/text.js"; + +interface TransactionInfo { + type: "paymentId" | "transactionHash"; + id: string; + label: string; + chain: ChainMetadata; + destinationToken?: Token; + originToken?: Token; + originChain?: ChainMetadata; + amountPaid?: string; + amountReceived?: string; +} + +function getPaymentId( + preparedQuote: BridgePrepareResult, + status: CompletedStatusResult, +) { + if (preparedQuote.type === "onramp") { + return preparedQuote.id; + } + return status.transactions[status.transactions.length - 1]?.transactionHash; +} + +/** + * Hook to fetch transaction info for a completed status + */ +function useTransactionInfo( + status: CompletedStatusResult, + preparedQuote: BridgePrepareResult, +) { + return useQuery({ + queryKey: [ + "transaction-info", + status.type, + getPaymentId(preparedQuote, status), + ], + queryFn: async (): Promise => { + const isOnramp = status.type === "onramp"; + + if (isOnramp && preparedQuote.type === "onramp") { + // For onramp, create a display ID since OnrampStatus doesn't have paymentId + return { + type: "paymentId" as const, + id: preparedQuote.id, + label: "Onramp Payment", + destinationToken: preparedQuote.destinationToken, + chain: await getChainMetadata( + defineChain(preparedQuote.destinationToken.chainId), + ), + amountPaid: `${preparedQuote.currencyAmount} ${preparedQuote.currency}`, + amountReceived: `${formatTokenAmount( + preparedQuote.destinationAmount, + preparedQuote.destinationToken.decimals, + )} ${preparedQuote.destinationToken.symbol}`, + }; + } else if ( + status.type === "buy" || + status.type === "sell" || + status.type === "transfer" + ) { + if (status.transactions.length > 0) { + // get the last transaction hash + const tx = status.transactions[status.transactions.length - 1]; + if (tx) { + const [destinationChain, originChain] = await Promise.all([ + getChainMetadata(getCachedChain(status.destinationToken.chainId)), + getChainMetadata(getCachedChain(status.originToken.chainId)), + ]); + return { + type: "transactionHash" as const, + id: tx.transactionHash, + label: "Transaction", + chain: destinationChain, + originToken: status.originToken, + originChain, + destinationToken: status.destinationToken, + amountReceived: `${formatTokenAmount( + status.destinationAmount, + status.destinationToken.decimals, + )} ${status.destinationToken.symbol}`, + amountPaid: `${formatTokenAmount( + status.originAmount, + status.originToken.decimals, + )} ${status.originToken.symbol}`, + }; + } + } + } + + return null; + }, + enabled: true, + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +interface CompletedStepDetailCardProps { + status: CompletedStatusResult; + preparedQuote: BridgePrepareResult; + windowAdapter: WindowAdapter; + onCopyToClipboard: (text: string) => Promise; +} + +/** + * Component to display details for a completed transaction step + */ +function CompletedStepDetailCard({ + status, + preparedQuote, + windowAdapter, + onCopyToClipboard, +}: CompletedStepDetailCardProps) { + const theme = useCustomTheme(); + const { data: txInfo, isLoading } = useTransactionInfo(status, preparedQuote); + + if (isLoading) { + return ( + + + + + + ); + } + + if (!txInfo) { + return null; + } + + return ( + + {/* Status Badge */} + + + {txInfo.label} + + + + COMPLETED + + + + + {/* Amount Paid */} + {txInfo.amountPaid && ( + + + Amount Paid + + + {txInfo.amountPaid} + + + )} + + {/* Origin Chain */} + {txInfo.originChain && ( + + + Origin Chain + + + {shorterChainName(txInfo.chain.name)} + + + )} + + {/* Amount Received */} + {txInfo.amountReceived && ( + + + Amount Received + + + {txInfo.amountReceived} + + + )} + + {/* Chain */} + + + Chain + + + {shorterChainName(txInfo.chain.name)} + + + + {/* Transaction Info */} + + + {txInfo.type === "paymentId" ? "Payment ID" : "Transaction Hash"} + + + onCopyToClipboard(txInfo.id) + : () => { + const explorer = txInfo.chain.explorers?.[0]; + if (explorer) { + windowAdapter.open( + formatExplorerTxUrl(explorer.url, txInfo.id), + ); + } + } + } + > + {shortenHex(txInfo.id)} + + + {txInfo.type === "paymentId" ? ( + + ) : null} + + + + ); +} + +export interface PaymentReceitProps { + /** + * Prepared quote from Bridge.prepare + */ + preparedQuote: BridgePrepareResult; + + /** + * Completed status results from step execution + */ + completedStatuses: CompletedStatusResult[]; + + /** + * Called when user goes back to success screen + */ + onBack: () => void; + + /** + * Window adapter for opening URLs + */ + windowAdapter: WindowAdapter; +} + +export function PaymentReceipt({ + preparedQuote, + completedStatuses, + onBack, + windowAdapter, +}: PaymentReceitProps) { + // Copy to clipboard + const copyToClipboard = useCallback(async (text: string) => { + try { + await navigator.clipboard.writeText(text); + // Could add a toast notification here + } catch (error) { + console.warn("Failed to copy to clipboard:", error); + } + }, []); + + return ( + + + + + + + {/* Status Results */} + + + Transactions + + + {completedStatuses.map((status, index) => ( + + ))} + + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-success/SuccessScreen.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-success/SuccessScreen.tsx new file mode 100644 index 00000000000..54b72c25e60 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-success/SuccessScreen.tsx @@ -0,0 +1,155 @@ +"use client"; +import { CheckIcon } from "@radix-ui/react-icons"; +import { useState } from "react"; +import type { WindowAdapter } from "../../../../core/adapters/WindowAdapter.js"; +import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; +import { iconSize } from "../../../../core/design-system/index.js"; +import type { BridgePrepareResult } from "../../../../core/hooks/useBridgePrepare.js"; +import type { CompletedStatusResult } from "../../../../core/hooks/useStepExecutor.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container, ModalHeader } from "../../components/basic.js"; +import { Button } from "../../components/buttons.js"; +import { Text } from "../../components/text.js"; +import type { UIOptions } from "../BridgeOrchestrator.js"; +import { PaymentReceipt } from "./PaymentReceipt.js"; + +export interface SuccessScreenProps { + /** + * UI options + */ + uiOptions: UIOptions; + /** + * Prepared quote from Bridge.prepare + */ + preparedQuote: BridgePrepareResult; + + /** + * Completed status results from step execution + */ + completedStatuses: CompletedStatusResult[]; + + /** + * Called when user closes the success screen + */ + onDone: () => void; + + /** + * Window adapter for opening URLs + */ + windowAdapter: WindowAdapter; +} + +type ViewState = "success" | "detail"; + +export function SuccessScreen({ + uiOptions, + preparedQuote, + completedStatuses, + onDone, + windowAdapter, +}: SuccessScreenProps) { + const theme = useCustomTheme(); + const [viewState, setViewState] = useState("success"); + + if (viewState === "detail") { + return ( + setViewState("success")} + /> + ); + } + + return ( + + + + + + + {/* Success Icon with Animation */} + + + + + + Payment Successful! + + + + Your cross-chain payment has been completed successfully. + + + + + {/* Action Buttons */} + + + + + + + {/* CSS Animations */} + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx index e0c485c69c7..8fa694d2a91 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx @@ -699,8 +699,8 @@ const ButtonContainer = /* @__PURE__ */ StyledDiv(() => { const ShowAllWalletsIcon = /* @__PURE__ */ StyledDiv(() => { const theme = useCustomTheme(); return { - width: `${iconSize.xl}px`, - height: `${iconSize.xl}px`, + width: `${iconSize.lg}px`, + height: `${iconSize.lg}px`, backgroundColor: theme.colors.tertiaryBg, border: `2px solid ${theme.colors.borderColor}`, borderRadius: radius.md, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/constants.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/constants.ts index aa8386cf093..739d518f3df 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/constants.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/constants.ts @@ -5,7 +5,7 @@ export const reservedScreens = { showAll: "showAll", }; -export const modalMaxWidthCompact = "360px"; +export const modalMaxWidthCompact = "400px"; const wideModalWidth = 730; export const modalMaxWidthWide = `${wideModalWidth}px`; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/CreditCardIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/CreditCardIcon.tsx new file mode 100644 index 00000000000..c7a7a38e291 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/CreditCardIcon.tsx @@ -0,0 +1,24 @@ +import type { IconFC } from "./types.js"; + +/** + * @internal + */ +export const CreditCardIcon: IconFC = (props) => { + return ( + + + + + ); +}; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx index 61567c33c3b..b132713e40e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx @@ -24,7 +24,10 @@ import type { } from "../../../../../core/hooks/connection/ConnectButtonProps.js"; import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js"; import { invalidateWalletBalance } from "../../../../../core/providers/invalidateWalletBalance.js"; -import type { SupportedTokens } from "../../../../../core/utils/defaultTokens.js"; +import type { + SupportedTokens, + TokenInfo, +} from "../../../../../core/utils/defaultTokens.js"; import { ErrorState } from "../../../../wallets/shared/ErrorState.js"; import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js"; import type { PayEmbedConnectOptions } from "../../../PayEmbed.js"; @@ -539,7 +542,7 @@ function BuyScreenContent(props: BuyScreenContentProps) { toChain={toChain} toToken={toToken} fromChain={fromChain} - fromToken={fromToken} + fromToken={fromToken as TokenInfo} showFromTokenSelector={() => { setScreen({ id: "select-from-token", diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/DirectPaymentModeScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/DirectPaymentModeScreen.tsx index 348d460e974..c92395f7e7e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/DirectPaymentModeScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/DirectPaymentModeScreen.tsx @@ -90,7 +90,9 @@ export function DirectPaymentModeScreen(props: { const token: TokenInfo = paymentInfo.token ? { - ...paymentInfo.token, + name: paymentInfo.token.name || chainData.nativeCurrency.name, + symbol: paymentInfo.token.symbol || chainData.nativeCurrency.symbol, + address: paymentInfo.token.address || NATIVE_TOKEN_ADDRESS, icon: paymentInfo.token?.icon || supportedDestinations diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx index 694da52dbda..996937f421b 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.tsx @@ -99,6 +99,14 @@ export function getFiatIcon( /> ); } + +export function getFiatCurrencyIcon(props: { + currency: string; + size: keyof typeof iconSize; +}): React.ReactNode { + return getFiatIcon(getCurrencyMeta(props.currency), props.size); +} + const UnknownCurrencyIcon: IconFC = (props) => { return ; }; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts index 0bdc0dc71fb..caa7aa62403 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useUISelectionStates.ts @@ -6,6 +6,7 @@ import type { PayUIOptions, } from "../../../../../../core/hooks/connection/ConnectButtonProps.js"; import { useActiveWalletChain } from "../../../../../../core/hooks/wallets/useActiveWalletChain.js"; +import type { TokenInfo } from "../../../../../../core/utils/defaultTokens.js"; import { useDebouncedValue } from "../../../../hooks/useDebouncedValue.js"; import { type ERC20OrNativeToken, NATIVE_TOKEN } from "../../nativeToken.js"; import { @@ -49,7 +50,7 @@ export function useToTokenSelectionStates(options: { setToChain(prefillBuy.chain); } if (prefillBuy?.token) { - setToToken(prefillBuy.token); + setToToken(prefillBuy.token as TokenInfo); } }, [prefillBuy?.amount, prefillBuy?.chain, prefillBuy?.token]); @@ -68,7 +69,7 @@ export function useToTokenSelectionStates(options: { ); const [toToken, setToToken] = useState( - prefillBuy?.token || + (prefillBuy?.token as TokenInfo) || (payOptions.mode === "direct_payment" && payOptions.paymentInfo.token) || NATIVE_TOKEN, ); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx index 366b6ae54cc..75fc29ca35f 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx @@ -43,7 +43,13 @@ export function FiatValue( return cryptoToFiatQuery.data?.result ? ( - ${formatNumber(cryptoToFiatQuery.data.result, 2).toFixed(2)} + $ + {Number( + formatNumber(cryptoToFiatQuery.data.result, 2).toFixed(2), + ).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} ) : null; } diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx index 16b1a2601c1..8648c9b4dd8 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/StepConnector.tsx @@ -19,6 +19,7 @@ export function StepConnectorArrow() { !!p.details.email)?.details.email : undefined; const walletInfo = useWalletInfo(wallet?.id); @@ -48,7 +48,7 @@ export function WalletRow(props: { : ""; return ( - + {wallet ? ( )} - + {props.label ? ( {props.label} ) : null} - + {addressOrENS || shortenAddress(props.address)} {profile.isLoading ? ( diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts index c29fcbe9ecc..d5a9cca35d2 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts @@ -1,4 +1,6 @@ import { formatNumber } from "../../../../../utils/formatNumber.js"; +import { toTokens } from "../../../../../utils/units.js"; +import { getCurrencyMeta } from "./Buy/fiat/currencies.js"; /** * @internal @@ -20,3 +22,23 @@ export function formatTokenBalance( (showSymbol ? ` ${balanceData.symbol}` : "") ); } + +export function formatTokenAmount( + amount: bigint, + decimals: number, + decimalsToShow = 5, +) { + return formatNumber( + Number.parseFloat(toTokens(amount, decimals)), + decimalsToShow, + ).toString(); +} + +export function formatCurrencyAmount( + currency: string, + amount: number, + decimals = 2, +) { + const symbol = getCurrencyMeta(currency).symbol; + return `${symbol}${formatNumber(amount, decimals).toFixed(decimals)}`; +} diff --git a/packages/thirdweb/src/react/web/ui/PayEmbed.tsx b/packages/thirdweb/src/react/web/ui/PayEmbed.tsx index cea1ada5907..294c243404c 100644 --- a/packages/thirdweb/src/react/web/ui/PayEmbed.tsx +++ b/packages/thirdweb/src/react/web/ui/PayEmbed.tsx @@ -1,31 +1,32 @@ "use client"; -import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import type { Token } from "../../../bridge/index.js"; import type { Chain } from "../../../chains/types.js"; import type { ThirdwebClient } from "../../../client/client.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js"; +import { getToken } from "../../../pay/convert/get-token.js"; +import { toTokens } from "../../../utils/units.js"; import type { Wallet } from "../../../wallets/interfaces/wallet.js"; import type { SmartWalletOptions } from "../../../wallets/smart/types.js"; import type { AppMetadata } from "../../../wallets/types.js"; import type { WalletId } from "../../../wallets/wallet-types.js"; import { CustomThemeProvider } from "../../core/design-system/CustomThemeProvider.js"; import type { Theme } from "../../core/design-system/index.js"; -import { - type SiweAuthOptions, - useSiweAuth, -} from "../../core/hooks/auth/useSiweAuth.js"; +import type { SiweAuthOptions } from "../../core/hooks/auth/useSiweAuth.js"; import type { ConnectButton_connectModalOptions, + FundWalletOptions, PayUIOptions, } from "../../core/hooks/connection/ConnectButtonProps.js"; -import { useActiveAccount } from "../../core/hooks/wallets/useActiveAccount.js"; -import { useActiveWallet } from "../../core/hooks/wallets/useActiveWallet.js"; -import { useConnectionManager } from "../../core/providers/connection-manager.js"; import type { SupportedTokens } from "../../core/utils/defaultTokens.js"; -import { AutoConnect } from "../../web/ui/AutoConnect/AutoConnect.js"; +import { + BridgeOrchestrator, + type UIOptions, +} from "./Bridge/BridgeOrchestrator.js"; +import { UnsupportedTokenScreen } from "./Bridge/UnsupportedTokenScreen.js"; import { EmbedContainer } from "./ConnectWallet/Modal/ConnectEmbed.js"; import { useConnectLocale } from "./ConnectWallet/locale/getConnectLocale.js"; -import BuyScreen from "./ConnectWallet/screens/Buy/BuyScreen.js"; -import { ExecutingTxScreen } from "./TransactionButton/ExecutingScreen.js"; import { DynamicHeight } from "./components/DynamicHeight.js"; import { Spinner } from "./components/Spinner.js"; import type { LocaleId } from "./types.js"; @@ -151,6 +152,20 @@ export type PayEmbedProps = { paymentLinkId?: string; }; +// Enhanced UIOptions to handle unsupported token state +type UIOptionsResult = + | { type: "success"; data: UIOptions } + | { + type: "indexing_token"; + token: Token; + chain: Chain; + } + | { + type: "unsupported_token"; + token: { address: string; symbol?: string; name?: string }; + chain: Chain; + }; + /** * Embed a prebuilt UI for funding wallets, purchases or transactions with crypto or fiat. * @@ -204,7 +219,8 @@ export type PayEmbedProps = { * sellerAddress: "0x...", // the wallet address of the seller * }, * metadata: { - * name: "Black Hoodie (Size L)", + * name: "Black Hoodie", + * description: "Size L. Ships worldwide.", * image: "/drip-hoodie.png", * }, * }} @@ -308,43 +324,134 @@ export type PayEmbedProps = { */ export function PayEmbed(props: PayEmbedProps) { const localeQuery = useConnectLocale(props.locale || "en_US"); - const [screen, setScreen] = useState<"buy" | "execute-tx">("buy"); const theme = props.theme || "dark"; - const connectionManager = useConnectionManager(); - const activeAccount = useActiveAccount(); - const activeWallet = useActiveWallet(); - const siweAuth = useSiweAuth( - activeWallet, - activeAccount, - props.connectOptions?.auth, - ); - // Add props.chain and props.chains to defined chains store - useEffect(() => { - if (props.connectOptions?.chain) { - connectionManager.defineChains([props.connectOptions?.chain]); - } - }, [props.connectOptions?.chain, connectionManager]); + const bridgeDataQuery = useQuery({ + queryKey: ["bridgeData", props], + queryFn: async (): Promise => { + if (!props.payOptions?.mode) { + const ETH = await getToken(props.client, NATIVE_TOKEN_ADDRESS, 1); + return { + type: "success", + data: { + mode: "fund_wallet", + destinationToken: ETH, + initialAmount: "0.01", + }, + }; + } - useEffect(() => { - if (props.connectOptions?.chains) { - connectionManager.defineChains(props.connectOptions?.chains); - } - }, [props.connectOptions?.chains, connectionManager]); + if (props.payOptions?.mode === "fund_wallet") { + const prefillInfo = props.payOptions?.prefillBuy; + if (!prefillInfo) { + const ETH = await getToken(props.client, NATIVE_TOKEN_ADDRESS, 1); + return { + type: "success", + data: { + mode: "fund_wallet", + destinationToken: ETH, + metadata: props.payOptions?.metadata, + }, + }; + } + const token = await getToken( + props.client, + prefillInfo.token?.address || NATIVE_TOKEN_ADDRESS, + prefillInfo.chain.id, + ).catch((err) => + err.message.includes("not supported") + ? undefined + : Promise.reject(err), + ); + if (!token) { + return { + type: "unsupported_token", + token: { + address: prefillInfo.token?.address || NATIVE_TOKEN_ADDRESS, + symbol: prefillInfo.token?.symbol, + name: prefillInfo.token?.name, + }, + chain: prefillInfo.chain, + }; + } + return { + type: "success", + data: { + mode: "fund_wallet", + destinationToken: token, + initialAmount: prefillInfo.amount, + metadata: { + ...props.payOptions?.metadata, + title: props.payOptions?.metadata?.name, + }, + }, + }; + } - useEffect(() => { - if (props.activeWallet) { - connectionManager.setActiveWallet(props.activeWallet); - } - }, [props.activeWallet, connectionManager]); + if (props.payOptions?.mode === "direct_payment") { + const paymentInfo = props.payOptions.paymentInfo; + const token = await getToken( + props.client, + paymentInfo.token?.address || NATIVE_TOKEN_ADDRESS, + paymentInfo.chain.id, + ).catch((err) => + err.message.includes("not supported") + ? undefined + : Promise.reject(err), + ); + if (!token) { + return { + type: "unsupported_token", + token: { + address: paymentInfo.token?.address || NATIVE_TOKEN_ADDRESS, + symbol: paymentInfo.token?.symbol, + name: paymentInfo.token?.name, + }, + chain: paymentInfo.chain, + }; + } + const amount = + "amount" in paymentInfo + ? paymentInfo.amount + : toTokens(paymentInfo.amountWei, token.decimals); + return { + type: "success", + data: { + mode: "direct_payment", + metadata: { + ...props.payOptions?.metadata, + title: props.payOptions?.metadata?.name, + }, + paymentInfo: { + token, + amount, + sellerAddress: paymentInfo.sellerAddress as `0x${string}`, + feePayer: paymentInfo.feePayer, + }, + }, + }; + } - let content = null; - const metadata = - props.payOptions && "metadata" in props.payOptions - ? props.payOptions.metadata - : null; + if (props.payOptions?.mode === "transaction") { + return { + type: "success", + data: { + mode: "transaction", + metadata: { + ...props.payOptions?.metadata, + title: props.payOptions?.metadata?.name, + }, + transaction: props.payOptions.transaction, + }, + }; + } - if (!localeQuery.data) { + throw new Error("Invalid mode"); + }, + }); + + let content = null; + if (!localeQuery.data || bridgeDataQuery.isLoading) { content = (
); - } else { + } else if (bridgeDataQuery.data?.type === "unsupported_token") { + // Show unsupported token screen + content = ; + } else if (bridgeDataQuery.data?.type === "success") { + // Show normal bridge orchestrator content = ( - <> - - {screen === "buy" && ( - { - if (props.payOptions?.mode === "transaction") { - setScreen("execute-tx"); - } - }} - connectOptions={props.connectOptions} - onBack={undefined} - /> - )} - - {screen === "execute-tx" && - props.payOptions?.mode === "transaction" && - props.payOptions.transaction && ( - { - setScreen("buy"); - }} - onBack={() => { - setScreen("buy"); - }} - onTxSent={(data) => { - props.payOptions?.onPurchaseSuccess?.({ - type: "transaction", - chainId: data.chain.id, - transactionHash: data.transactionHash, - }); - }} - /> - )} - + { + props.payOptions?.onPurchaseSuccess?.(); + }} + presetOptions={ + (props.payOptions as FundWalletOptions)?.prefillBuy?.presetOptions + } + /> ); } diff --git a/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx b/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx index 3bfd1a51644..bfdad370482 100644 --- a/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/TransactionButton/DepositScreen.tsx @@ -168,10 +168,9 @@ export function DepositScreen(props: { style={{ justifyContent: "space-between", padding: spacing.sm, - marginBottom: spacing.sm, - borderRadius: spacing.md, - backgroundColor: theme.colors.tertiaryBg, + borderRadius: `${radius.md} ${radius.md} 0 0`, border: `1px solid ${theme.colors.borderColor}`, + borderBottom: "none", }} > {activeAccount && ( @@ -223,7 +222,13 @@ export function DepositScreen(props: { />
- + {address ? shortenAddress(address) : ""} { display: "flex", justifyContent: "space-between", border: `1px solid ${theme.colors.borderColor}`, - borderRadius: radius.lg, + borderRadius: `0 0 ${radius.md} ${radius.md}`, transition: "border-color 200ms ease", "&:hover": { borderColor: theme.colors.accentText, diff --git a/packages/thirdweb/src/react/web/ui/TransactionButton/ExecutingScreen.tsx b/packages/thirdweb/src/react/web/ui/TransactionButton/ExecutingScreen.tsx index 2d060e0692b..0322b6caae4 100644 --- a/packages/thirdweb/src/react/web/ui/TransactionButton/ExecutingScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/TransactionButton/ExecutingScreen.tsx @@ -1,9 +1,11 @@ -import { CheckCircledIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; +import { CheckIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; import { useCallback, useEffect, useRef, useState } from "react"; import type { Hex } from "viem"; import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js"; import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js"; import { formatExplorerTxUrl } from "../../../../utils/url.js"; +import type { WindowAdapter } from "../../../core/adapters/WindowAdapter.js"; +import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; import { iconSize } from "../../../core/design-system/index.js"; import { useChainExplorers } from "../../../core/hooks/others/useChainQuery.js"; import { useSendTransaction } from "../../hooks/transaction/useSendTransaction.js"; @@ -11,7 +13,7 @@ import { AccentFailIcon } from "../ConnectWallet/icons/AccentFailIcon.js"; import { Spacer } from "../components/Spacer.js"; import { Spinner } from "../components/Spinner.js"; import { Container, ModalHeader } from "../components/basic.js"; -import { Button, ButtonLink } from "../components/buttons.js"; +import { Button } from "../components/buttons.js"; import { Text } from "../components/text.js"; export function ExecutingTxScreen(props: { @@ -19,6 +21,7 @@ export function ExecutingTxScreen(props: { closeModal: () => void; onTxSent: (data: WaitForReceiptOptions) => void; onBack?: () => void; + windowAdapter: WindowAdapter; }) { const sendTxCore = useSendTransaction({ payModal: false, @@ -29,6 +32,7 @@ export function ExecutingTxScreen(props: { const [status, setStatus] = useState<"loading" | "failed" | "sent">( "loading", ); + const theme = useCustomTheme(); const sendTx = useCallback(async () => { setStatus("loading"); @@ -67,15 +71,32 @@ export function ExecutingTxScreen(props: { {status === "loading" && } {status === "failed" && } {status === "sent" && ( - - + )} - + + {status === "loading" && "Sending transaction"} @@ -87,7 +108,7 @@ export function ExecutingTxScreen(props: { {status === "failed" && txError ? txError.message || "" : ""} - + {status === "failed" && ( {txHash && ( <> - - { + props.windowAdapter.open( + formatExplorerTxUrl( + chainExplorers.explorers[0]?.url ?? "", + txHash, + ), + ); }} + gap="xs" + color="primaryText" > View on Explorer - + + )} + )} + + {/* CSS Animations */} +
); } diff --git a/packages/thirdweb/src/react/web/ui/TransactionButton/TransactionModal.tsx b/packages/thirdweb/src/react/web/ui/TransactionButton/TransactionModal.tsx index c76cb25644b..cd90d3946dd 100644 --- a/packages/thirdweb/src/react/web/ui/TransactionButton/TransactionModal.tsx +++ b/packages/thirdweb/src/react/web/ui/TransactionButton/TransactionModal.tsx @@ -11,9 +11,10 @@ import type { PayUIOptions } from "../../../core/hooks/connection/ConnectButtonP import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js"; import type { SupportedTokens } from "../../../core/utils/defaultTokens.js"; +import { webWindowAdapter } from "../../adapters/WindowAdapter.js"; import { LoadingScreen } from "../../wallets/shared/LoadingScreen.js"; +import { BridgeOrchestrator } from "../Bridge/BridgeOrchestrator.js"; import { useConnectLocale } from "../ConnectWallet/locale/getConnectLocale.js"; -import { LazyBuyScreen } from "../ConnectWallet/screens/Buy/LazyBuyScreen.js"; import { Modal } from "../components/Modal.js"; import type { LocaleId } from "../types.js"; import { DepositScreen } from "./DepositScreen.js"; @@ -94,6 +95,7 @@ function TransactionModalContent(props: ModalProps & { onBack?: () => void }) { tx={props.tx} closeModal={props.onClose} onTxSent={props.onTxSent} + windowAdapter={webWindowAdapter} /> ); } @@ -113,19 +115,16 @@ function TransactionModalContent(props: ModalProps & { onBack?: () => void }) { } return ( - { + onComplete={() => { setScreen("execute-tx"); }} - connectOptions={undefined} /> ); } diff --git a/packages/thirdweb/src/react/web/ui/components/ChainName.tsx b/packages/thirdweb/src/react/web/ui/components/ChainName.tsx index 8543c4936ba..d685e066660 100644 --- a/packages/thirdweb/src/react/web/ui/components/ChainName.tsx +++ b/packages/thirdweb/src/react/web/ui/components/ChainName.tsx @@ -11,14 +11,16 @@ import { Text } from "./text.js"; export const ChainName: React.FC<{ chain: Chain; size: "xs" | "sm" | "md" | "lg"; + color?: "primaryText" | "secondaryText"; client: ThirdwebClient; short?: boolean; + style?: React.CSSProperties; }> = (props) => { const { name } = useChainName(props.chain); if (name) { return ( - + {props.short ? shorterChainName(name) : name} ); @@ -27,7 +29,7 @@ export const ChainName: React.FC<{ return ; }; -function shorterChainName(name: string) { +export function shorterChainName(name: string) { const split = name.split(" "); const wordsToRemove = new Set(["mainnet", "testnet", "chain"]); return split diff --git a/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx b/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx index 66c4f0ddc92..cb9ef79cc77 100644 --- a/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx +++ b/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx @@ -4,7 +4,7 @@ import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../constants/addresses.js"; import { iconSize } from "../../../core/design-system/index.js"; -import { useChainIconUrl } from "../../../core/hooks/others/useChainQuery.js"; +import { useChainMetadata } from "../../../core/hooks/others/useChainQuery.js"; import { genericTokenIcon } from "../../../core/utils/walletIcon.js"; import { CoinsIcon } from "../ConnectWallet/icons/CoinsIcon.js"; import { @@ -27,17 +27,20 @@ export function TokenIcon(props: { size: keyof typeof iconSize; client: ThirdwebClient; }) { - const chainIconQuery = useChainIconUrl(props.chain); + const chainMeta = useChainMetadata(props.chain).data; const tokenImage = useMemo(() => { if ( isNativeToken(props.token) || props.token.address === NATIVE_TOKEN_ADDRESS ) { - return chainIconQuery.url; + if (chainMeta?.nativeCurrency.symbol === "ETH") { + return "ipfs://QmcxZHpyJa8T4i63xqjPYrZ6tKrt55tZJpbXcjSDKuKaf9/ethereum/512.png"; // ETH icon + } + return chainMeta?.icon?.url; } return props.token.icon; - }, [props.token, chainIconQuery.url]); + }, [props.token, chainMeta?.icon?.url, chainMeta?.nativeCurrency.symbol]); return tokenImage ? ( { border: `1px solid ${theme.colors.borderColor}`, "&:hover": { borderColor: theme.colors.accentText, - transform: "scale(1.01)", }, '&[aria-selected="true"]': { borderColor: theme.colors.accentText, @@ -106,7 +105,6 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { border: "1px solid transparent", "&:hover": { borderColor: theme.colors.accentText, - transform: "scale(1.01)", }, }; } @@ -114,7 +112,7 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { if (props.variant === "accent") { return { "&:hover": { - transform: "scale(1.01)", + opacity: 0.8, }, }; } @@ -123,7 +121,6 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { return { "&:hover": { background: theme.colors.secondaryButtonHoverBg, - transform: "scale(1.01)", }, }; } @@ -133,7 +130,6 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { padding: 0, "&:hover": { color: theme.colors.primaryText, - transform: "scale(1.01)", }, }; } diff --git a/packages/thirdweb/src/stories/Bridge/BridgeOrchestrator.stories.tsx b/packages/thirdweb/src/stories/Bridge/BridgeOrchestrator.stories.tsx new file mode 100644 index 00000000000..710f1f6d6ca --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/BridgeOrchestrator.stories.tsx @@ -0,0 +1,205 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import {} from "../../react/core/design-system/CustomThemeProvider.js"; +import type { Theme } from "../../react/core/design-system/index.js"; +import { + BridgeOrchestrator, + type BridgeOrchestratorProps, +} from "../../react/web/ui/Bridge/BridgeOrchestrator.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { + DIRECT_PAYMENT_UI_OPTIONS, + FUND_WALLET_UI_OPTIONS, + TRANSACTION_UI_OPTIONS, +} from "./fixtures.js"; + +/** + * BridgeOrchestrator is the main orchestrator component for the Bridge payment flow. + * It manages the complete state machine navigation between different screens and + * handles the coordination of payment methods, routes, and execution. + */ + +// Props interface for the wrapper component +interface BridgeOrchestratorWithThemeProps extends BridgeOrchestratorProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const BridgeOrchestratorWithTheme = ( + props: BridgeOrchestratorWithThemeProps, +) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/BridgeOrchestrator", + component: BridgeOrchestratorWithTheme, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: + "**BridgeOrchestrator** is the main orchestrator component that manages the complete Bridge payment flow using XState FSM.\n\n" + + "## Features\n" + + "- **State Machine Navigation**: Uses XState v5 for predictable state transitions\n" + + "- **Payment Method Selection**: Supports wallet and fiat payment methods\n" + + "- **Route Preview**: Shows detailed transaction steps and fees\n" + + "- **Step Execution**: Real-time progress tracking\n" + + "- **Error Handling**: Comprehensive error states with retry functionality\n" + + "- **Theme Support**: Works with both light and dark themes\n\n" + + "## State Flow\n" + + "1. **Resolve Requirements** → 2. **Method Selection** → 3. **Quote** → 4. **Preview** → 5. **Prepare** → 6. **Execute** → 7. **Success**\n\n" + + "Each state can transition to the **Error** state, which provides retry functionality.", + }, + }, + }, + tags: ["autodocs"], + args: { + client: storyClient, + uiOptions: FUND_WALLET_UI_OPTIONS.usdcDefault, + onComplete: () => console.log("Bridge flow completed"), + onError: (error) => console.error("Bridge error:", error), + onCancel: () => console.log("Bridge flow cancelled"), + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + presetOptions: { + control: "object", + description: "Quick buy options", + }, + onComplete: { action: "flow completed" }, + onError: { action: "error occurred" }, + onCancel: { action: "flow cancelled" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * Default BridgeOrchestrator in light theme. + */ +export const Light: Story = { + args: { + theme: "light", + uiOptions: FUND_WALLET_UI_OPTIONS.usdcDefault, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +/** + * BridgeOrchestrator in dark theme. + */ +export const Dark: Story = { + args: { + theme: "dark", + uiOptions: FUND_WALLET_UI_OPTIONS.usdcDefault, + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +/** + * Direct payment mode for purchasing a specific product/service. + */ +export const DirectPayment: Story = { + args: { + theme: "dark", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.digitalArt, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Direct payment mode shows a product purchase interface with the item image, price, seller address, and network information. The user can connect their wallet and proceed with the payment.", + }, + }, + }, +}; + +/** + * Direct payment mode in light theme. + */ +export const DirectPaymentLight: Story = { + args: { + theme: "light", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.concertTicket, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version of direct payment mode, showing a different product example with USDC payment.", + }, + }, + }, +}; + +/** + * Transaction mode showing a complex contract interaction. + */ +export const Transaction: Story = { + args: { + theme: "dark", + uiOptions: TRANSACTION_UI_OPTIONS.contractInteraction, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Transaction mode showing a complex contract interaction (claimTo function) with function name extraction from contract ABI and detailed cost breakdown.", + }, + }, + }, +}; + +/** + * Transaction mode in light theme showing an ERC20 token transfer. + */ +export const TransactionLight: Story = { + args: { + theme: "light", + uiOptions: TRANSACTION_UI_OPTIONS.erc20Transfer, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version of transaction mode showing an ERC20 token transfer with proper token amount formatting and USD conversion.", + }, + }, + }, +}; + +export const CustompresetOptions: Story = { + args: { + theme: "dark", + uiOptions: FUND_WALLET_UI_OPTIONS.ethDefault, + presetOptions: [1, 2, 3], + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Fund wallet mode with custom quick options showing ETH with [1, 2, 3] preset amounts.", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/DirectPayment.stories.tsx b/packages/thirdweb/src/stories/Bridge/DirectPayment.stories.tsx new file mode 100644 index 00000000000..600f93fdb9c --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/DirectPayment.stories.tsx @@ -0,0 +1,230 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Theme } from "../../react/core/design-system/index.js"; +import { + DirectPayment, + type DirectPaymentProps, +} from "../../react/web/ui/Bridge/DirectPayment.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { DIRECT_PAYMENT_UI_OPTIONS } from "./fixtures.js"; + +// Props interface for the wrapper component +interface DirectPaymentWithThemeProps extends DirectPaymentProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const DirectPaymentWithTheme = (props: DirectPaymentWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/DirectPayment", + component: DirectPaymentWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "DirectPayment component displays a product/service purchase interface with payment details.\n\n" + + "## Features\n" + + "- **Product Display**: Shows product name, image, and pricing\n" + + "- **Payment Details**: Token amount, network information, and seller address\n" + + "- **Wallet Integration**: Connect button or continue with active wallet\n" + + "- **Responsive Design**: Adapts to different screen sizes and themes\n" + + "- **Fee Configuration**: Support for sender or receiver paying fees\n\n" + + "This component is used in the 'direct_payment' mode of BridgeOrchestrator for purchasing specific items or services. It now accepts uiOptions directly to configure payment info and metadata.", + }, + }, + }, + tags: ["autodocs"], + args: { + client: storyClient, + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.digitalArt, + onContinue: (amount, token, receiverAddress) => + console.log("Continue with payment:", { + amount, + token, + receiverAddress, + }), + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onContinue: { + action: "continue clicked", + description: "Called when user continues with the payment", + }, + uiOptions: { + description: + "UI configuration for direct payment mode including payment info and metadata", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const DigitalArt: Story = { + args: { + theme: "dark", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.digitalArt, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example of purchasing a digital art NFT with ETH payment. Shows the product image, pricing in ETH, and seller information with sender paying fees.", + }, + }, + }, +}; + +export const DigitalArtLight: Story = { + args: { + theme: "light", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.digitalArt, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Light theme version of the digital art purchase interface.", + }, + }, + }, +}; + +export const ConcertTicket: Story = { + args: { + theme: "dark", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.concertTicket, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example of purchasing a concert ticket with USDC payment. Shows different product type, stable token pricing, and receiver paying fees.", + }, + }, + }, +}; + +export const ConcertTicketLight: Story = { + args: { + theme: "light", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.concertTicket, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Light theme version of the concert ticket purchase.", + }, + }, + }, +}; + +export const SubscriptionService: Story = { + args: { + theme: "dark", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.subscription, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example of a subscription service payment with detailed description. Shows how the component works for recurring service payments with comprehensive product information.", + }, + }, + }, +}; + +export const SubscriptionServiceLight: Story = { + args: { + theme: "light", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.subscription, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version of subscription service payment with full description text.", + }, + }, + }, +}; + +export const PhysicalProduct: Story = { + args: { + theme: "dark", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.sneakers, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example of purchasing physical products with crypto payments. Shows how the component adapts to different product types with ETH payment.", + }, + }, + }, +}; + +export const PhysicalProductLight: Story = { + args: { + theme: "light", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.sneakers, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Light theme version of physical product purchase.", + }, + }, + }, +}; + +export const NoImage: Story = { + args: { + theme: "dark", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.credits, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example of purchasing digital credits without product image. Shows how the component handles text-only products with description fallback.", + }, + }, + }, +}; + +export const NoImageLight: Story = { + args: { + theme: "light", + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.credits, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Light theme version of credits purchase without image.", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/ErrorBanner.stories.tsx b/packages/thirdweb/src/stories/Bridge/ErrorBanner.stories.tsx new file mode 100644 index 00000000000..eb2b99f10da --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/ErrorBanner.stories.tsx @@ -0,0 +1,184 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Theme } from "../../react/core/design-system/index.js"; +import { ErrorBanner } from "../../react/web/ui/Bridge/ErrorBanner.js"; +import { ModalThemeWrapper } from "../utils.js"; + +const mockNetworkError = new Error( + "Network connection failed. Please check your internet connection and try again.", +); +const mockUserRejectedError = new Error("Transaction was rejected by user."); +const mockInsufficientFundsError = new Error( + "Insufficient funds to complete this transaction.", +); +const mockGenericError = new Error("An unexpected error occurred."); + +// Props interface for the wrapper component +interface ErrorBannerWithThemeProps { + error: Error; + onRetry: () => void; + onCancel?: () => void; + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const ErrorBannerWithTheme = (props: ErrorBannerWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/ErrorBanner", + component: ErrorBannerWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "Error banner component that displays user-friendly error messages with retry functionality and optional cancel action.", + }, + }, + }, + tags: ["autodocs"], + args: { + error: mockNetworkError, + onRetry: () => console.log("Retry clicked"), + onCancel: () => console.log("Cancel clicked"), + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onRetry: { action: "retry clicked" }, + onCancel: { action: "cancel clicked" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Light: Story = { + args: { + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const Dark: Story = { + args: { + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const NetworkError: Story = { + args: { + theme: "dark", + error: mockNetworkError, + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const NetworkErrorLight: Story = { + args: { + theme: "light", + error: mockNetworkError, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const UserRejectedError: Story = { + args: { + theme: "dark", + error: mockUserRejectedError, + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const UserRejectedErrorLight: Story = { + args: { + theme: "light", + error: mockUserRejectedError, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const InsufficientFundsError: Story = { + args: { + theme: "dark", + error: mockInsufficientFundsError, + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const InsufficientFundsErrorLight: Story = { + args: { + theme: "light", + error: mockInsufficientFundsError, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const WithoutCancelButton: Story = { + args: { + theme: "dark", + error: mockGenericError, + onCancel: undefined, + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const WithoutCancelButtonLight: Story = { + args: { + theme: "light", + error: mockGenericError, + onCancel: undefined, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const EmptyMessage: Story = { + args: { + theme: "dark", + error: new Error(""), + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const EmptyMessageLight: Story = { + args: { + theme: "light", + error: new Error(""), + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/FundWallet.stories.tsx b/packages/thirdweb/src/stories/Bridge/FundWallet.stories.tsx new file mode 100644 index 00000000000..84671d2f725 --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/FundWallet.stories.tsx @@ -0,0 +1,197 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Theme } from "../../react/core/design-system/index.js"; +import { FundWallet } from "../../react/web/ui/Bridge/FundWallet.js"; +import type { FundWalletProps } from "../../react/web/ui/Bridge/FundWallet.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { FUND_WALLET_UI_OPTIONS, RECEIVER_ADDRESSES } from "./fixtures.js"; + +// Props interface for the wrapper component +interface FundWalletWithThemeProps extends FundWalletProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const FundWalletWithTheme = (props: FundWalletWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/FundWallet", + component: FundWalletWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "FundWallet component allows users to specify the amount they want to add to their wallet. This is the first screen in the fund_wallet flow before method selection.\n\n" + + "## Features\n" + + "- **Token Selection**: Choose from different tokens (ETH, USDC, UNI)\n" + + "- **Amount Input**: Enter custom amount or use quick options\n" + + "- **Receiver Address**: Optional receiver address (defaults to connected wallet)\n" + + "- **Quick Options**: Preset amounts for faster selection\n" + + "- **Theme Support**: Works with both light and dark themes\n\n" + + "This component now accepts uiOptions directly to configure the destination token, initial amount, and quick options.", + }, + }, + }, + tags: ["autodocs"], + args: { + uiOptions: FUND_WALLET_UI_OPTIONS.ethDefault, + client: storyClient, + onContinue: (amount, token, receiverAddress) => { + console.log("Continue clicked:", { amount, token, receiverAddress }); + alert(`Continue with ${amount} ${token.symbol} to ${receiverAddress}`); + }, + receiverAddress: RECEIVER_ADDRESSES.primary, + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onContinue: { action: "continue clicked" }, + uiOptions: { + description: "UI configuration for fund wallet mode", + }, + receiverAddress: { + description: "Optional receiver address (defaults to connected wallet)", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Light: Story = { + args: { + theme: "light", + uiOptions: FUND_WALLET_UI_OPTIONS.ethDefault, + receiverAddress: undefined, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Default fund wallet interface in light theme with ETH token.", + }, + }, + }, +}; + +export const Dark: Story = { + args: { + theme: "dark", + uiOptions: FUND_WALLET_UI_OPTIONS.ethDefault, + receiverAddress: undefined, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: "Default fund wallet interface in dark theme with ETH token.", + }, + }, + }, +}; + +export const WithInitialAmount: Story = { + args: { + theme: "dark", + uiOptions: FUND_WALLET_UI_OPTIONS.ethWithAmount, + receiverAddress: RECEIVER_ADDRESSES.secondary, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Fund wallet with pre-filled amount and specified receiver address.", + }, + }, + }, +}; + +export const WithInitialAmountLight: Story = { + args: { + theme: "light", + uiOptions: FUND_WALLET_UI_OPTIONS.ethWithAmount, + receiverAddress: RECEIVER_ADDRESSES.secondary, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version with pre-filled amount and receiver address.", + }, + }, + }, +}; + +export const USDCToken: Story = { + args: { + theme: "dark", + uiOptions: FUND_WALLET_UI_OPTIONS.usdcDefault, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: "Fund wallet configured for USDC token with initial amount.", + }, + }, + }, +}; + +export const USDCTokenLight: Story = { + args: { + theme: "light", + uiOptions: FUND_WALLET_UI_OPTIONS.usdcDefault, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Light theme version for USDC token funding.", + }, + }, + }, +}; + +export const LargeAmount: Story = { + args: { + theme: "dark", + uiOptions: FUND_WALLET_UI_OPTIONS.uniLarge, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Fund wallet with UNI token and large pre-filled amount to test formatting.", + }, + }, + }, +}; + +export const LargeAmountLight: Story = { + args: { + theme: "light", + uiOptions: FUND_WALLET_UI_OPTIONS.uniLarge, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Light theme version with UNI token and large amount.", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/PaymentDetails.stories.tsx b/packages/thirdweb/src/stories/Bridge/PaymentDetails.stories.tsx new file mode 100644 index 00000000000..7b8cbe5e777 --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/PaymentDetails.stories.tsx @@ -0,0 +1,501 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Theme } from "../../react/core/design-system/index.js"; +import type { PaymentMethod } from "../../react/core/machines/paymentMachine.js"; +import { + PaymentDetails, + type PaymentDetailsProps, +} from "../../react/web/ui/Bridge/payment-details/PaymentDetails.js"; +import { stringify } from "../../utils/json.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { + DIRECT_PAYMENT_UI_OPTIONS, + STORY_MOCK_WALLET, + TRANSACTION_UI_OPTIONS, + USDC, + buyWithApprovalQuote, + complexBuyQuote, + longTextBuyQuote, + onrampWithSwapsQuote, + simpleBuyQuote, + simpleOnrampQuote, +} from "./fixtures.js"; + +const fiatPaymentMethod: PaymentMethod = { + type: "fiat", + currency: "USD", + onramp: "coinbase", + payerWallet: STORY_MOCK_WALLET, +}; + +const cryptoPaymentMethod: PaymentMethod = JSON.parse( + stringify({ + type: "wallet", + payerWallet: STORY_MOCK_WALLET, + balance: 100000000n, + originToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + }), +); + +const ethCryptoPaymentMethod: PaymentMethod = JSON.parse( + stringify({ + type: "wallet", + payerWallet: STORY_MOCK_WALLET, + balance: 1000000000000000000n, + originToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + }), +); + +// Props interface for the wrapper component +interface PaymentDetailsWithThemeProps extends PaymentDetailsProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const PaymentDetailsWithTheme = (props: PaymentDetailsWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/PaymentDetails", + component: PaymentDetailsWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "Route preview screen that displays prepared quote details, fees, estimated time, and transaction steps for user confirmation.", + }, + }, + }, + tags: ["autodocs"], + args: { + preparedQuote: simpleOnrampQuote, + onConfirm: () => console.log("Route confirmed"), + onBack: () => console.log("Back clicked"), + onError: (error) => console.error("Error:", error), + theme: "dark", + uiOptions: { + mode: "fund_wallet", + destinationToken: USDC, + }, + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onConfirm: { action: "route confirmed" }, + onBack: { action: "back clicked" }, + onError: { action: "error occurred" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const OnrampSimple: Story = { + args: { + theme: "dark", + preparedQuote: simpleOnrampQuote, + paymentMethod: fiatPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Simple onramp quote with no extra steps - direct fiat to crypto.", + }, + }, + }, +}; + +export const OnrampSimpleLight: Story = { + args: { + theme: "light", + preparedQuote: simpleOnrampQuote, + paymentMethod: fiatPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Simple onramp quote with no extra steps (light theme).", + }, + }, + }, +}; + +export const OnrampSimpleDirectPayment: Story = { + args: { + theme: "dark", + preparedQuote: simpleOnrampQuote, + paymentMethod: fiatPaymentMethod, + client: storyClient, + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.credits, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Simple onramp quote with no extra steps - direct fiat to crypto.", + }, + }, + }, +}; + +export const OnrampSimpleLightDirectPayment: Story = { + args: { + theme: "light", + preparedQuote: simpleOnrampQuote, + paymentMethod: fiatPaymentMethod, + client: storyClient, + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.concertTicket, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Simple onramp quote with no extra steps (light theme).", + }, + }, + }, +}; + +export const OnrampWithSwaps: Story = { + args: { + theme: "dark", + preparedQuote: onrampWithSwapsQuote, + paymentMethod: fiatPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Onramp quote with 2 additional swap steps after the fiat purchase.", + }, + }, + }, +}; + +export const OnrampWithSwapsLight: Story = { + args: { + theme: "light", + preparedQuote: onrampWithSwapsQuote, + paymentMethod: fiatPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Onramp quote with 2 additional swap steps (light theme).", + }, + }, + }, +}; + +export const BuySimple: Story = { + args: { + theme: "dark", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Simple buy quote with a single transaction (no approval needed).", + }, + }, + }, +}; + +export const BuySimpleLight: Story = { + args: { + theme: "light", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Simple buy quote with a single transaction (light theme).", + }, + }, + }, +}; + +export const BuySimpleDirectPayment: Story = { + args: { + theme: "dark", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.digitalArt, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Simple buy quote with a single transaction (no approval needed).", + }, + }, + }, +}; + +export const BuySimpleLightDirectPayment: Story = { + args: { + theme: "light", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + uiOptions: DIRECT_PAYMENT_UI_OPTIONS.subscription, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Simple buy quote with a single transaction (light theme).", + }, + }, + }, +}; + +export const BuyWithLongText: Story = { + args: { + theme: "dark", + preparedQuote: longTextBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: "Simple buy quote with a single transaction (light theme).", + }, + }, + }, +}; + +export const BuyWithApproval: Story = { + args: { + theme: "dark", + preparedQuote: buyWithApprovalQuote, + paymentMethod: cryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Buy quote requiring both approval and buy transactions in a single step.", + }, + }, + }, +}; + +export const BuyWithApprovalLight: Story = { + args: { + theme: "light", + preparedQuote: buyWithApprovalQuote, + paymentMethod: cryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: "Buy quote with approval and buy transactions (light theme).", + }, + }, + }, +}; + +export const BuyComplex: Story = { + args: { + theme: "dark", + preparedQuote: complexBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Complex buy quote with 3 steps, each requiring approval and execution transactions across multiple chains.", + }, + }, + }, +}; + +export const BuyComplexLight: Story = { + args: { + theme: "light", + preparedQuote: complexBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Complex multi-step buy quote spanning multiple chains (light theme).", + }, + }, + }, +}; + +// ========== TRANSACTION MODE STORIES ========== // + +export const TransactionEthTransfer: Story = { + args: { + theme: "dark", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + uiOptions: TRANSACTION_UI_OPTIONS.ethTransfer, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Transaction mode showing ETH transfer payment details with function name and contract information displayed in the PaymentDetails screen.", + }, + }, + }, +}; + +export const TransactionEthTransferLight: Story = { + args: { + theme: "light", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + uiOptions: TRANSACTION_UI_OPTIONS.ethTransfer, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version of transaction mode for ETH transfer with detailed payment overview.", + }, + }, + }, +}; + +export const TransactionERC20Transfer: Story = { + args: { + theme: "dark", + preparedQuote: simpleBuyQuote, + paymentMethod: cryptoPaymentMethod, + client: storyClient, + uiOptions: TRANSACTION_UI_OPTIONS.erc20Transfer, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Transaction mode for ERC20 token transfer showing token details and transfer function in payment preview.", + }, + }, + }, +}; + +export const TransactionERC20TransferLight: Story = { + args: { + theme: "light", + preparedQuote: simpleBuyQuote, + paymentMethod: cryptoPaymentMethod, + client: storyClient, + uiOptions: TRANSACTION_UI_OPTIONS.erc20Transfer, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version of ERC20 token transfer transaction mode with payment details.", + }, + }, + }, +}; + +export const TransactionContractInteraction: Story = { + args: { + theme: "dark", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + uiOptions: TRANSACTION_UI_OPTIONS.contractInteraction, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Transaction mode for complex contract interaction (claimTo function) showing detailed contract information and function details in payment preview.", + }, + }, + }, +}; + +export const TransactionContractInteractionLight: Story = { + args: { + theme: "light", + preparedQuote: simpleBuyQuote, + paymentMethod: ethCryptoPaymentMethod, + client: storyClient, + uiOptions: TRANSACTION_UI_OPTIONS.contractInteraction, + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version of contract interaction transaction mode with comprehensive payment details.", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/PaymentSelection.stories.tsx b/packages/thirdweb/src/stories/Bridge/PaymentSelection.stories.tsx new file mode 100644 index 00000000000..8c9dee58f3b --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/PaymentSelection.stories.tsx @@ -0,0 +1,178 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Theme } from "../../react/core/design-system/index.js"; +import { + PaymentSelection, + type PaymentSelectionProps, +} from "../../react/web/ui/Bridge/payment-selection/PaymentSelection.js"; +import en from "../../react/web/ui/ConnectWallet/locale/en.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { UNI, USDC } from "./fixtures.js"; + +// Props interface for the wrapper component +interface PaymentSelectionWithThemeProps extends PaymentSelectionProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const PaymentSelectionWithTheme = (props: PaymentSelectionWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/PaymentSelection", + component: PaymentSelectionWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "Payment method selection screen with a 2-step flow:\n\n" + + "**Step 1:** Choose payment method - shows connected wallets, connect wallet option, and pay with fiat option\n\n" + + "**Step 2a:** If wallet selected - shows available origin tokens for bridging to the destination token (fetches real routes data from the Bridge API)\n\n" + + "**Step 2b:** If fiat selected - shows onramp provider options (Coinbase, Stripe, Transak)\n\n" + + "The component intelligently manages wallet context and provides proper error handling for each step.", + }, + }, + }, + tags: ["autodocs"], + args: { + destinationToken: USDC, + client: storyClient, + onPaymentMethodSelected: (paymentMethod) => + console.log("Payment method selected:", paymentMethod), + onError: (error) => console.error("Error:", error), + theme: "dark", + destinationAmount: "1", + connectLocale: en, + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + destinationToken: { + description: "The target token to bridge to", + }, + destinationAmount: { + description: "Amount of destination token to bridge", + }, + onPaymentMethodSelected: { + action: "payment method selected", + description: "Called when user selects a wallet token or fiat provider", + }, + onError: { + action: "error occurred", + description: "Called when an error occurs during the flow", + }, + onBack: { + action: "back clicked", + description: "Called when user wants to go back (only shown in Step 1)", + }, + connectLocale: { + description: "Locale for connecting wallets", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Light: Story = { + args: { + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version showing the initial wallet selection step. Click on a connected wallet to see token selection, or click 'Pay with Fiat' to see provider selection.", + }, + }, + }, +}; + +export const Dark: Story = { + args: { + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Dark theme version of the payment selection flow. The component starts with wallet selection and provides navigation through the 2-step process.", + }, + }, + }, +}; + +export const WithBackButton: Story = { + args: { + theme: "dark", + onBack: () => console.log("Back clicked"), + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Version with a back button in the header. The back behavior changes based on the current step - Step 1 calls onBack, Steps 2a/2b return to Step 1.", + }, + }, + }, +}; + +export const WithBackButtonLight: Story = { + args: { + theme: "light", + onBack: () => console.log("Back clicked"), + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Light theme version with back button functionality. Demonstrates the navigation flow between steps.", + }, + }, + }, +}; + +export const DifferentDestinationToken: Story = { + args: { + theme: "dark", + destinationToken: UNI, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example with a different destination token (UNI). This will show different available origin tokens in Step 2a when a wallet is selected.", + }, + }, + }, +}; + +export const LargeAmount: Story = { + args: { + theme: "dark", + destinationAmount: "1000", + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Example with a larger destination amount (1000 USDC). This may affect which origin tokens are available based on user balances.", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/StepRunner.stories.tsx b/packages/thirdweb/src/stories/Bridge/StepRunner.stories.tsx new file mode 100644 index 00000000000..2f3fc23543f --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/StepRunner.stories.tsx @@ -0,0 +1,112 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { ThirdwebClient } from "../../client/client.js"; +import type { WindowAdapter } from "../../react/core/adapters/WindowAdapter.js"; +import type { Theme } from "../../react/core/design-system/index.js"; +import type { BridgePrepareRequest } from "../../react/core/hooks/useBridgePrepare.js"; +import type { CompletedStatusResult } from "../../react/core/hooks/useStepExecutor.js"; +import { StepRunner } from "../../react/web/ui/Bridge/StepRunner.js"; +import type { Wallet } from "../../wallets/interfaces/wallet.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { STORY_MOCK_WALLET, simpleBuyRequest } from "./fixtures.js"; + +// Mock window adapter +const mockWindowAdapter: WindowAdapter = { + open: async (url: string) => { + console.log(`Mock opening URL: ${url}`); + }, +}; + +// Props interface for the wrapper component +interface StepRunnerWithThemeProps { + request: BridgePrepareRequest; + wallet: Wallet; + client: ThirdwebClient; + windowAdapter: WindowAdapter; + onComplete: (completedStatuses: CompletedStatusResult[]) => void; + onError: (error: Error) => void; + onCancel?: () => void; + onBack?: () => void; + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const StepRunnerWithTheme = (props: StepRunnerWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/StepRunner", + component: StepRunnerWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "**StepRunner** executes prepared route steps sequentially, showing real-time progress and transaction status.\n\n" + + "## Features\n" + + "- **Real Execution**: Uses useStepExecutor hook for actual transaction processing\n" + + "- **Progress Tracking**: Visual progress bar and step-by-step status updates\n" + + "- **Error Handling**: Retry functionality for failed transactions\n" + + "- **Transaction Batching**: Optimizes multiple transactions when possible\n" + + "- **Onramp Support**: Handles fiat-to-crypto onramp flows\n\n" + + "## Props\n" + + "- `steps`: Array of RouteStep objects from Bridge.prepare()\n" + + "- `wallet`: Connected wallet for transaction signing\n" + + "- `client`: ThirdwebClient instance\n" + + "- `windowAdapter`: Platform-specific window/URL handler\n" + + "- `onramp`: Optional onramp configuration\n\n" + + "## Integration\n" + + "This component is typically used within the BridgeOrchestrator after route preparation.", + }, + }, + }, + tags: ["autodocs"], + args: { + wallet: STORY_MOCK_WALLET, + client: storyClient, + windowAdapter: mockWindowAdapter, + onComplete: (completedStatuses: CompletedStatusResult[]) => + console.log("Execution completed", completedStatuses), + onError: (error: Error) => console.error("Error:", error), + onCancel: () => console.log("Execution cancelled"), + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onComplete: { action: "execution completed" }, + onError: { action: "error occurred" }, + onCancel: { action: "execution cancelled" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Light: Story = { + args: { + theme: "light", + request: simpleBuyRequest, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const Dark: Story = { + args: { + theme: "dark", + request: simpleBuyRequest, + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/SuccessScreen.stories.tsx b/packages/thirdweb/src/stories/Bridge/SuccessScreen.stories.tsx new file mode 100644 index 00000000000..5ab8a470d42 --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/SuccessScreen.stories.tsx @@ -0,0 +1,217 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { stringify } from "viem"; +import type { Theme } from "../../react/core/design-system/index.js"; +import type { CompletedStatusResult } from "../../react/core/hooks/useStepExecutor.js"; +import { webWindowAdapter } from "../../react/web/adapters/WindowAdapter.js"; +import { + SuccessScreen, + type SuccessScreenProps, +} from "../../react/web/ui/Bridge/payment-success/SuccessScreen.js"; +import { ModalThemeWrapper } from "../utils.js"; +import { + FUND_WALLET_UI_OPTIONS, + TRANSACTION_UI_OPTIONS, + simpleBuyQuote, + simpleOnrampQuote, +} from "./fixtures.js"; + +const mockBuyCompletedStatuses: CompletedStatusResult[] = JSON.parse( + stringify([ + { + type: "buy", + status: "COMPLETED", + paymentId: "payment-12345", + originAmount: 1000000000000000000n, + destinationAmount: 100000000n, + originChainId: 1, + destinationChainId: 1, + originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + originToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + symbol: "ETH", + name: "Ethereum", + decimals: 18, + priceUsd: 2500, + }, + destinationToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + symbol: "USDC", + name: "USD Coin", + decimals: 6, + priceUsd: 1, + }, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + transactions: [ + { + chainId: 1, + transactionHash: + "0x1234567890abcdef1234567890abcdef12345678901234567890abcdef123456", + }, + ], + }, + ]), +); + +const mockOnrampCompletedStatuses: CompletedStatusResult[] = JSON.parse( + stringify([ + { + type: "onramp", + status: "COMPLETED", + transactions: [ + { + chainId: 137, + transactionHash: + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + }, + ], + purchaseData: { + orderId: "stripe-order-abc123", + }, + }, + ]), +); + +// Props interface for the wrapper component +interface SuccessScreenWithThemeProps extends SuccessScreenProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const SuccessScreenWithTheme = (props: SuccessScreenWithThemeProps) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/SuccessScreen", + component: SuccessScreenWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "Success screen that displays completion confirmation with transaction summary, payment details, and action buttons for next steps. Includes animated success icon and detailed transaction view.", + }, + }, + }, + tags: ["autodocs"], + args: { + preparedQuote: simpleBuyQuote, + completedStatuses: mockBuyCompletedStatuses, + onDone: () => console.log("Success screen closed"), + theme: "dark", + windowAdapter: webWindowAdapter, + uiOptions: FUND_WALLET_UI_OPTIONS.ethDefault, + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onDone: { action: "success screen closed" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const DefaultLight: Story = { + args: { + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const OnrampPayment: Story = { + args: { + theme: "dark", + preparedQuote: simpleOnrampQuote, + completedStatuses: mockOnrampCompletedStatuses, + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Success screen for onramp payments showing payment ID that can be copied to clipboard.", + }, + }, + }, +}; + +export const OnrampPaymentLight: Story = { + args: { + theme: "light", + preparedQuote: simpleOnrampQuote, + completedStatuses: mockOnrampCompletedStatuses, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const ComplexPayment: Story = { + args: { + theme: "dark", + preparedQuote: simpleOnrampQuote, + completedStatuses: [ + ...mockOnrampCompletedStatuses, + ...mockBuyCompletedStatuses, + ], + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Success screen for onramp payments showing payment ID that can be copied to clipboard.", + }, + }, + }, +}; + +export const ComplexPaymentLight: Story = { + args: { + theme: "light", + preparedQuote: simpleOnrampQuote, + completedStatuses: [ + ...mockOnrampCompletedStatuses, + ...mockBuyCompletedStatuses, + ], + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const TransactionPayment: Story = { + args: { + theme: "light", + preparedQuote: simpleBuyQuote, + completedStatuses: mockBuyCompletedStatuses, + uiOptions: TRANSACTION_UI_OPTIONS.contractInteraction, + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/TransactionPayment.stories.tsx b/packages/thirdweb/src/stories/Bridge/TransactionPayment.stories.tsx new file mode 100644 index 00000000000..dff301fb87f --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/TransactionPayment.stories.tsx @@ -0,0 +1,171 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + TransactionPayment, + type TransactionPaymentProps, +} from "../../react/web/ui/Bridge/TransactionPayment.js"; +import { ModalThemeWrapper, storyClient } from "../utils.js"; +import { TRANSACTION_UI_OPTIONS } from "./fixtures.js"; + +// Props interface for the wrapper component +interface TransactionPaymentWithThemeProps extends TransactionPaymentProps { + theme: "light" | "dark"; +} + +// Wrapper component to provide theme context +const TransactionPaymentWithTheme = ( + props: TransactionPaymentWithThemeProps, +) => { + const { theme, ...componentProps } = props; + + return ( + +
+ +
+
+ ); +}; + +const meta = { + title: "Bridge/TransactionPayment", + component: TransactionPaymentWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "Transaction payment component that displays detailed transaction information including contract details, function names, transaction costs, and network fees.\n\n" + + "## Features\n" + + "- **Contract Information**: Shows contract name and clickable address\n" + + "- **Function Detection**: Extracts function names from transaction data using ABI\n" + + "- **Cost Calculation**: Displays transaction value and USD equivalent\n" + + "- **Network Fees**: Shows estimated gas costs with token amounts\n" + + "- **Chain Details**: Network name and logo with proper formatting\n" + + "- **Skeleton Loading**: Comprehensive loading states matching final layout\n\n" + + "This component now accepts uiOptions directly to configure the transaction and metadata. Supports both native token and ERC20 token transactions with proper function name extraction.", + }, + }, + }, + tags: ["autodocs"], + args: { + uiOptions: TRANSACTION_UI_OPTIONS.ethTransfer, + client: storyClient, + onContinue: (amount, token, receiverAddress) => + console.log("Execute transaction:", { amount, token, receiverAddress }), + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onContinue: { + action: "continue clicked", + description: "Called when user continues with the transaction", + }, + uiOptions: { + description: + "UI configuration for transaction mode including prepared transaction", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const EthereumTransfer: Story = { + args: { + uiOptions: TRANSACTION_UI_OPTIONS.ethTransfer, + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Simple ETH transfer transaction showing native token value and network fees with USD conversion. Demonstrates function name extraction from contract ABI.", + }, + }, + }, +}; + +export const EthereumTransferLight: Story = { + args: { + uiOptions: TRANSACTION_UI_OPTIONS.ethTransfer, + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Simple ETH transfer transaction in light theme with skeleton loading support.", + }, + }, + }, +}; + +export const ERC20TokenTransfer: Story = { + args: { + uiOptions: TRANSACTION_UI_OPTIONS.erc20Transfer, + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "ERC20 token transaction showing token amount, USD value, and proper formatting using real token data. Displays transfer function details.", + }, + }, + }, +}; + +export const ERC20TokenTransferLight: Story = { + args: { + uiOptions: TRANSACTION_UI_OPTIONS.erc20Transfer, + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "ERC20 token transaction in light theme with enhanced formatting.", + }, + }, + }, +}; + +export const ContractInteraction: Story = { + args: { + uiOptions: TRANSACTION_UI_OPTIONS.contractInteraction, + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Complex contract interaction showing function name extraction from ABI (claimTo), cost calculation, and network details with proper currency formatting.", + }, + }, + }, +}; + +export const ContractInteractionLight: Story = { + args: { + uiOptions: TRANSACTION_UI_OPTIONS.contractInteraction, + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Contract interaction transaction in light theme with enhanced UX and skeleton loading.", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/UnsupportedTokenScreen.stories.tsx b/packages/thirdweb/src/stories/Bridge/UnsupportedTokenScreen.stories.tsx new file mode 100644 index 00000000000..71dc490b325 --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/UnsupportedTokenScreen.stories.tsx @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { defineChain } from "../../chains/utils.js"; +import type { Theme } from "../../react/core/design-system/index.js"; +import { + UnsupportedTokenScreen, + type UnsupportedTokenScreenProps, +} from "../../react/web/ui/Bridge/UnsupportedTokenScreen.js"; +import { ModalThemeWrapper } from "../utils.js"; + +// Props interface for the wrapper component +interface UnsupportedTokenScreenWithThemeProps + extends UnsupportedTokenScreenProps { + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const UnsupportedTokenScreenWithTheme = ( + props: UnsupportedTokenScreenWithThemeProps, +) => { + const { theme, ...componentProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/UnsupportedTokenScreen", + component: UnsupportedTokenScreenWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "Screen displayed when a token is being indexed or when using an unsupported testnet. Shows loading state for indexing tokens or error state for testnets.", + }, + }, + }, + tags: ["autodocs"], + args: { + chain: defineChain(1), // Ethereum mainnet + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const TokenNotSupported: Story = { + args: { + theme: "dark", + chain: defineChain(1), // Ethereum mainnet - will show indexing spinner + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Shows the loading state when a token is being indexed by the Universal Bridge on a mainnet chain.", + }, + }, + }, +}; + +export const TokenNotSupportedLight: Story = { + args: { + theme: "light", + chain: defineChain(1), // Ethereum mainnet - will show indexing spinner + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Shows the loading state when a token is being indexed by the Universal Bridge on a mainnet chain (light theme).", + }, + }, + }, +}; + +export const TestnetNotSupported: Story = { + args: { + theme: "dark", + chain: defineChain(11155111), // Sepolia testnet - will show error state + }, + parameters: { + backgrounds: { default: "dark" }, + docs: { + description: { + story: + "Shows the error state when trying to use the Universal Bridge on a testnet chain (Sepolia in this example).", + }, + }, + }, +}; + +export const TestnetNotSupportedLight: Story = { + args: { + theme: "light", + chain: defineChain(11155111), // Sepolia testnet - will show error state + }, + parameters: { + backgrounds: { default: "light" }, + docs: { + description: { + story: + "Shows the error state when trying to use the Universal Bridge on a testnet chain (Sepolia in this example, light theme).", + }, + }, + }, +}; diff --git a/packages/thirdweb/src/stories/Bridge/fixtures.ts b/packages/thirdweb/src/stories/Bridge/fixtures.ts new file mode 100644 index 00000000000..f26f78af27c --- /dev/null +++ b/packages/thirdweb/src/stories/Bridge/fixtures.ts @@ -0,0 +1,802 @@ +import { stringify } from "viem"; +import type { Token } from "../../bridge/index.js"; +import { baseSepolia } from "../../chains/chain-definitions/base-sepolia.js"; +import { base } from "../../chains/chain-definitions/base.js"; +import { polygon } from "../../chains/chain-definitions/polygon.js"; +import { defineChain } from "../../chains/utils.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; +import { getContract } from "../../contract/contract.js"; +import { claimTo } from "../../extensions/erc20/drops/write/claimTo.js"; +import { transfer } from "../../extensions/erc20/write/transfer.js"; +import type { BridgePrepareResult } from "../../react/core/hooks/useBridgePrepare.js"; +import type { BridgePrepareRequest } from "../../react/core/hooks/useBridgePrepare.js"; +import { getDefaultToken } from "../../react/core/utils/defaultTokens.js"; +import type { UIOptions } from "../../react/web/ui/Bridge/BridgeOrchestrator.js"; +import { prepareTransaction } from "../../transaction/prepare-transaction.js"; +import type { Account, Wallet } from "../../wallets/interfaces/wallet.js"; +import { storyClient } from "../utils.js"; +import { toWei } from "../../utils/units.js"; + +export const ETH: Token = { + address: NATIVE_TOKEN_ADDRESS, + name: "Ethereum", + symbol: "ETH", + chainId: 10, + decimals: 18, + priceUsd: 1000, + iconUri: + "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png", +}; + +export const USDC: Token = { + address: getDefaultToken(base, "USDC")?.address ?? "", + name: "USD Coin", + symbol: "USDC", + chainId: base.id, + decimals: 6, + priceUsd: 1, + iconUri: + "https://coin-images.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", +}; + +export const UNI: Token = { + address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + name: "Uniswap", + symbol: "UNI", + chainId: 10, + decimals: 18, + priceUsd: 1000, + iconUri: + "https://coin-images.coingecko.com/coins/images/12504/large/uniswap-uni.png", +}; + +const createStoryMockWallet = (): Wallet => { + const mockAccount: Account = { + address: "0x1234567890123456789012345678901234567890" as `0x${string}`, + sendTransaction: async () => ({ + transactionHash: "0xmockhash123" as `0x${string}`, + chain: defineChain(1), + client: storyClient, + }), + signMessage: async () => "0xsignature" as `0x${string}`, + signTypedData: async () => "0xsignature" as `0x${string}`, + }; + + // Simple mock wallet implementation for storybook display only + return { + id: "inApp", + getAccount: () => mockAccount, + getChain: async () => defineChain(1), + autoConnect: async () => mockAccount, + connect: async () => mockAccount, + disconnect: async () => { }, + switchChain: async () => { }, + subscribe: () => () => { }, + getConfig: () => ({}), + } as unknown as Wallet; +}; + +export const STORY_MOCK_WALLET = createStoryMockWallet(); + +// Simple onramp quote with no extra steps +export const simpleOnrampQuote: BridgePrepareResult = JSON.parse( + stringify({ + type: "onramp", + id: "onramp-simple-123", + link: "https://stripe.com/session/simple", + currency: "USD", + currencyAmount: 50.0, + destinationAmount: 50000000n, // 50 USDC + destinationToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + name: "USD Coin (PoS)", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + timestamp: Date.now(), + steps: [], // No additional steps needed + intent: { + onramp: "stripe", + chainId: 137, + tokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + amount: 50000000n, + }, + }), +); + +// Onramp quote with 2 extra swap steps +export const onrampWithSwapsQuote: BridgePrepareResult = JSON.parse( + stringify({ + type: "onramp", + id: "onramp-swaps-456", + link: "https://stripe.com/session/swaps", + currency: "EUR", + currencyAmount: 100.0, + destinationAmount: 1000000000000000000n, // 1 ETH + destinationToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, + }, + timestamp: Date.now(), + steps: [ + { + originToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + name: "USD Coin (PoS)", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + destinationToken: { + chainId: 137, + address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + name: "Wrapped Ether", + symbol: "WETH", + decimals: 18, + priceUsd: 2500.0, + }, + originAmount: 110000000n, // 110 USDC + destinationAmount: 44000000000000000n, // 0.044 WETH + estimatedExecutionTimeMs: 30000, + transactions: [ + { + action: "approval", + id: "0x1a2b3c", + to: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + data: "0x095ea7b3", + chainId: 137, + client: storyClient, + chain: defineChain(137), + }, + { + action: "buy", + id: "0x4d5e6f", + to: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + data: "0x472b43f3", + chainId: 137, + client: storyClient, + chain: defineChain(137), + }, + ], + }, + { + originToken: { + chainId: 137, + address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + name: "Wrapped Ether", + symbol: "WETH", + decimals: 18, + priceUsd: 2500.0, + }, + destinationToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, + }, + originAmount: 44000000000000000n, // 0.044 WETH + destinationAmount: 1000000000000000000n, // 1 ETH + estimatedExecutionTimeMs: 180000, + transactions: [ + { + action: "approval", + id: "0x7g8h9i", + to: "0x3fc91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + data: "0x095ea7b3", + chainId: 137, + client: storyClient, + chain: defineChain(137), + }, + { + action: "transfer", + id: "0xj1k2l3", + to: "0x3fc91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + data: "0x3593564c", + chainId: 137, + client: storyClient, + chain: defineChain(137), + }, + ], + }, + ], + intent: { + onramp: "stripe", + chainId: 1, + tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + amount: 1000000000000000000n, + }, + }), +); + +// Simple buy quote with single step (no approval needed) +export const simpleBuyQuote: BridgePrepareResult = JSON.parse( + stringify({ + type: "buy", + originAmount: 1000000000000000000n, // 1 ETH + destinationAmount: 100000000n, // 100 USDC + timestamp: Date.now(), + estimatedExecutionTimeMs: 60000, + steps: [ + { + originToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + destinationToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + originAmount: 1000000000000000000n, + destinationAmount: 100000000n, + estimatedExecutionTimeMs: 60000, + transactions: [ + { + action: "buy", + id: "0xsingle123", + to: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + data: "0x472b43f3", + value: 1000000000000000000n, + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + ], + }, + ], + intent: { + originChainId: 1, + originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + destinationChainId: 1, + destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + amount: 100000000n, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b", + }, + }), +); + +export const longTextBuyQuote: BridgePrepareResult = JSON.parse( + stringify({ + type: "buy", + originAmount: 1000000000000000000n, // 1 ETH + destinationAmount: 1000394284092830482309n, + timestamp: Date.now(), + estimatedExecutionTimeMs: 60000, + steps: [ + { + originToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + destinationToken: { + chainId: 42793, + address: "0x796Ea11Fa2dD751eD01b53C372fFDB4AAa8f00F9", + name: "USD Coin (USDC.e on Etherlink)", + symbol: "USDC.e", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + originAmount: 1000000000000000000n, + destinationAmount: 1000394284092830482309n, + estimatedExecutionTimeMs: 60000, + transactions: [ + { + action: "buy", + id: "0xsingle123", + to: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + data: "0x472b43f3", + value: 1000000000000000000n, + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + ], + }, + ], + intent: { + originChainId: 1, + originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + destinationChainId: 1, + destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + amount: 100000000n, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }, + }), +); + +// Buy quote with approval + buy in single step +export const buyWithApprovalQuote: BridgePrepareResult = JSON.parse( + stringify({ + type: "buy", + originAmount: 100000000n, // 100 USDC + destinationAmount: 100000000n, // 100 USDC on different chain + timestamp: Date.now(), + estimatedExecutionTimeMs: 120000, + steps: [ + { + originToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + destinationToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + name: "USD Coin (PoS)", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + originAmount: 100000000n, + destinationAmount: 100000000n, + estimatedExecutionTimeMs: 120000, + transactions: [ + { + action: "approval", + id: "0xapproval789", + to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + data: "0x095ea7b3", + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + { + action: "buy", + id: "0xbuy456", + to: "0x3fc91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + data: "0x3593564c", + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + ], + }, + ], + intent: { + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationChainId: 137, + destinationTokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + amount: 100000000n, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }, + }), +); + +// Complex buy quote with 3 steps, each with approval + buy +export const complexBuyQuote: BridgePrepareResult = JSON.parse( + stringify({ + type: "buy", + originAmount: 1000000000000000000n, // 1 ETH + destinationAmount: 1000000000000000000n, // 1 ETH on final chain + timestamp: Date.now(), + estimatedExecutionTimeMs: 300000, + steps: [ + { + originToken: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + priceUsd: 2500.0, + }, + destinationToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + originAmount: 1000000000000000000n, + destinationAmount: 2500000000n, // 2500 USDC + estimatedExecutionTimeMs: 60000, + transactions: [ + { + action: "approval", + id: "0xstep1approval", + to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + data: "0x095ea7b3", + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + { + action: "buy", + id: "0xstep1buy", + to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + data: "0x7ff36ab5", + value: 1000000000000000000n, + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + ], + }, + { + originToken: { + chainId: 1, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + destinationToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + name: "USD Coin (PoS)", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + originAmount: 2500000000n, + destinationAmount: 2495000000n, // 2495 USDC (after bridge fees) + estimatedExecutionTimeMs: 180000, + transactions: [ + { + action: "approval", + id: "0xstep2approval", + to: "0x3fc91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + data: "0x095ea7b3", + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + { + action: "transfer", + id: "0xstep2bridge", + to: "0x3fc91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + data: "0x3593564c", + chainId: 1, + client: storyClient, + chain: defineChain(1), + }, + ], + }, + { + originToken: { + chainId: 137, + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + name: "USD Coin (PoS)", + symbol: "USDC", + decimals: 6, + priceUsd: 1.0, + iconUri: + "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + }, + destinationToken: { + chainId: 43114, + address: "0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE", + symbol: "BEAM", + name: "Beam", + decimals: 18, + priceUsd: 0.00642458, + iconUri: + "https://coin-images.coingecko.com/coins/images/32417/small/cgicon.png?1747892021", + }, + originAmount: 2495000000n, + destinationAmount: 1000000000000000000n, // 1 BEAM + estimatedExecutionTimeMs: 60000, + transactions: [ + { + action: "approval", + id: "0xstep3approval", + to: "0x1111111254fb6c44bAC0beD2854e76F90643097d", + data: "0x095ea7b3", + chainId: 137, + client: storyClient, + chain: defineChain(137), + }, + { + action: "buy", + id: "0xstep3buy", + to: "0x1111111254fb6c44bAC0beD2854e76F90643097d", + data: "0x12aa3caf", + chainId: 137, + client: storyClient, + chain: defineChain(137), + }, + ], + }, + ], + intent: { + originChainId: 1, + originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + destinationChainId: 42161, + destinationTokenAddress: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + amount: 1000000000000000000n, + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + }, + }), +); +export const simpleBuyRequest: BridgePrepareRequest = { + type: "buy", + originChainId: 1, + originTokenAddress: NATIVE_TOKEN_ADDRESS, + destinationChainId: 10, + destinationTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: toWei("0.01"), + sender: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + receiver: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD", + client: storyClient, +}; + +// ========== PREPARED TRANSACTIONS FOR TRANSACTION PAYMENT ========== // + +// mintTo raw transaction +export const ethTransferTransaction = prepareTransaction({ + to: "0x87C52295891f208459F334975a3beE198fE75244", + data: "0x449a52f80000000000000000000000008447c7a30d18e9adf2abe362689fc994cc6a340d00000000000000000000000000000000000000000000000000038d7ea4c68000", + chain: baseSepolia, + client: storyClient, +}); + +// ERC20 token transaction with value +export const erc20Transaction = transfer({ + contract: getContract({ + client: storyClient, + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + chain: base, + }), + to: "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b", + amount: 100, +}); + +// claimTo on Polygon +export const contractInteractionTransaction = claimTo({ + contract: getContract({ + client: storyClient, + address: "0x683f91F407301b90e501492F8A26A3498D8d9638", + chain: polygon, + }), + to: "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b", + quantity: "10", +}); + +// ========== COMMON DUMMY DATA FOR STORYBOOK ========== // + +// Common receiver addresses for testing +export const RECEIVER_ADDRESSES = { + primary: "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b" as const, + secondary: "0xa3841994009B4fEabb01ebcC62062F9E56F701CD" as const, + seller: "0x1234567890123456789012345678901234567890" as const, + subscription: "0x9876543210987654321098765432109876543210" as const, + physical: "0x5555666677778888999900001111222233334444" as const, +}; + +// Product metadata for direct payments +export const PRODUCT_METADATA = { + digitalArt: { + name: "Premium Digital Art NFT", + image: + "https://images.unsplash.com/photo-1465101046530-73398c7f28ca?w=500&h=300&fit=crop", + description: "This is a premium digital art by a famous artist", + }, + concertTicket: { + name: "Concert Ticket - The Midnight Live", + image: + "https://images.unsplash.com/photo-1501281668745-f7f57925c3b4?w=500&h=300&fit=crop", + description: "Concert ticket for the upcoming show", + }, + subscription: { + name: "Premium Streaming Service - Monthly", + image: + "https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=500&h=300&fit=crop", + description: + "Get unlimited access to our premium streaming service with this monthly subscription. Enjoy ad-free viewing, exclusive content, and the ability to download shows for offline viewing.", + }, + sneakers: { + name: "Limited Edition Sneakers", + image: + "https://images.unsplash.com/photo-1549298916-b41d501d3772?w=500&h=300&fit=crop", + }, + credits: { + name: "Thirdweb Credits", + description: + "Add credits to your account for future billing cycles. Credits are non-refundable and do not expire.", + }, +}; + +// Type aliases for better type safety +type FundWalletUIOptions = Extract; +type DirectPaymentUIOptions = Extract; +type TransactionUIOptions = Extract; + +// UI Options for FundWallet mode +export const FUND_WALLET_UI_OPTIONS: Record< + "ethDefault" | "ethWithAmount" | "usdcDefault" | "uniLarge", + FundWalletUIOptions +> = { + ethDefault: { + mode: "fund_wallet" as const, + destinationToken: ETH, + metadata: { + title: "Fund Wallet", + description: "Add funds to your wallet", + }, + }, + ethWithAmount: { + mode: "fund_wallet" as const, + destinationToken: ETH, + initialAmount: "0.001", + metadata: { + title: "Fund Wallet", + description: "Add funds to your wallet", + }, + }, + usdcDefault: { + mode: "fund_wallet" as const, + destinationToken: USDC, + initialAmount: "5", + }, + uniLarge: { + mode: "fund_wallet" as const, + destinationToken: UNI, + initialAmount: "150000", + metadata: { + title: "Fund Wallet", + description: "Add UNI tokens to your wallet", + }, + }, +}; + +// UI Options for DirectPayment mode +export const DIRECT_PAYMENT_UI_OPTIONS: Record< + "digitalArt" | "concertTicket" | "subscription" | "sneakers" | "credits", + DirectPaymentUIOptions +> = { + digitalArt: { + mode: "direct_payment" as const, + paymentInfo: { + sellerAddress: RECEIVER_ADDRESSES.seller, + token: ETH, + amount: "0.1", + feePayer: "sender" as const, + }, + metadata: { + title: "Purchase Digital Art", + description: "Buy premium digital art NFT", + image: PRODUCT_METADATA.digitalArt.image, + }, + }, + concertTicket: { + mode: "direct_payment" as const, + paymentInfo: { + sellerAddress: RECEIVER_ADDRESSES.primary, + token: USDC, + amount: "25.00", + feePayer: "receiver" as const, + }, + metadata: { + title: "Buy Concert Ticket", + description: "Get your ticket for The Midnight Live", + image: PRODUCT_METADATA.concertTicket.image, + }, + }, + subscription: { + mode: "direct_payment" as const, + paymentInfo: { + sellerAddress: RECEIVER_ADDRESSES.subscription, + token: USDC, + amount: "9.99", + feePayer: "sender" as const, + }, + metadata: { + title: "Subscribe to Premium", + description: PRODUCT_METADATA.subscription.description, + image: PRODUCT_METADATA.subscription.image, + }, + }, + sneakers: { + mode: "direct_payment" as const, + paymentInfo: { + sellerAddress: RECEIVER_ADDRESSES.physical, + token: ETH, + amount: "0.05", + feePayer: "receiver" as const, + }, + metadata: { + title: "Buy Sneakers", + description: "Limited edition sneakers", + image: PRODUCT_METADATA.sneakers.image, + }, + }, + credits: { + mode: "direct_payment" as const, + paymentInfo: { + sellerAddress: RECEIVER_ADDRESSES.physical, + token: USDC, + amount: "25", + feePayer: "receiver" as const, + }, + metadata: { + title: "Add Credits", + description: PRODUCT_METADATA.credits.description, + }, + }, +}; + +// UI Options for Transaction mode +export const TRANSACTION_UI_OPTIONS: Record< + "ethTransfer" | "erc20Transfer" | "contractInteraction", + TransactionUIOptions +> = { + ethTransfer: { + mode: "transaction" as const, + transaction: ethTransferTransaction, + metadata: { + title: "Execute Transaction", + description: "Review and execute transaction", + }, + }, + erc20Transfer: { + mode: "transaction" as const, + transaction: erc20Transaction, + metadata: { + title: "Token Transfer", + description: "Transfer ERC20 tokens", + }, + }, + contractInteraction: { + mode: "transaction" as const, + transaction: contractInteractionTransaction, + metadata: { + title: "Contract Interaction", + description: "Interact with smart contract", + }, + }, +}; diff --git a/packages/thirdweb/src/stories/TokenBalanceRow.stories.tsx b/packages/thirdweb/src/stories/TokenBalanceRow.stories.tsx new file mode 100644 index 00000000000..719cdaeede9 --- /dev/null +++ b/packages/thirdweb/src/stories/TokenBalanceRow.stories.tsx @@ -0,0 +1,171 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Token } from "../bridge/index.js"; +import { ethereum } from "../chains/chain-definitions/ethereum.js"; +import type { Chain } from "../chains/types.js"; +import type { ThirdwebClient } from "../client/client.js"; +import { CustomThemeProvider } from "../react/core/design-system/CustomThemeProvider.js"; +import type { Theme } from "../react/core/design-system/index.js"; +import { TokenBalanceRow } from "../react/web/ui/Bridge/common/TokenBalanceRow.js"; +import { ETH, UNI, USDC } from "./Bridge/fixtures.js"; +import { storyClient } from "./utils.js"; + +// Props interface for the wrapper component +interface TokenBalanceRowWithThemeProps { + client: ThirdwebClient; + token: Token; + chain: Chain; + amount: string; + onClick: (token: Token) => void; + style?: React.CSSProperties; + theme: "light" | "dark" | Theme; +} + +const dummyBalanceETH: string = "1.2345"; + +const dummyBalanceUSDC: string = "1234.56"; + +const dummyBalanceLowUNI: string = "0.0012"; + +// Wrapper component to provide theme context +const TokenBalanceRowWithTheme = (props: TokenBalanceRowWithThemeProps) => { + const { theme, ...tokenBalanceRowProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Bridge/TokenBalanceRow", + component: TokenBalanceRowWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A row component that displays token balance information including token icon, symbol, chain, balance amount and fiat value. Used in bridge interfaces for token selection.", + }, + }, + }, + tags: ["autodocs"], + args: { + client: storyClient, + token: ETH, + chain: ethereum, + amount: dummyBalanceETH, + onClick: (token: Token) => { + console.log("Token selected:", token.symbol); + }, + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + onClick: { + action: "clicked", + description: "Callback function when token row is clicked", + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const TokenList: Story = { + render: (args) => ( + +
+ + + + +
+
+ ), + args: { + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const DarkTokenList: Story = { + render: (args) => ( + +
+ + + + +
+
+ ), + args: { + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export default meta; diff --git a/packages/thirdweb/src/stories/WalletRow.stories.tsx b/packages/thirdweb/src/stories/WalletRow.stories.tsx new file mode 100644 index 00000000000..ebe21bb060d --- /dev/null +++ b/packages/thirdweb/src/stories/WalletRow.stories.tsx @@ -0,0 +1,166 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { ThirdwebClient } from "../client/client.js"; +import { CustomThemeProvider } from "../react/core/design-system/CustomThemeProvider.js"; +import type { Theme } from "../react/core/design-system/index.js"; +import { WalletRow } from "../react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.js"; +import { storyClient } from "./utils.js"; + +// Props interface for the wrapper component +interface WalletRowWithThemeProps { + client: ThirdwebClient; + address: string; + iconSize?: "xs" | "sm" | "md" | "lg" | "xl"; + textSize?: "xs" | "sm" | "md" | "lg" | "xl"; + label?: string; + theme: "light" | "dark" | Theme; +} + +// Wrapper component to provide theme context +const WalletRowWithTheme = (props: WalletRowWithThemeProps) => { + const { theme, ...walletRowProps } = props; + return ( + + + + ); +}; + +const meta = { + title: "Connect/WalletRow", + component: WalletRowWithTheme, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A reusable component that displays wallet information including address, wallet type, and optional ENS name or email.", + }, + }, + }, + tags: ["autodocs"], + args: { + client: storyClient, + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // Vitalik's address for ENS demo + theme: "dark", + }, + argTypes: { + theme: { + control: "select", + options: ["light", "dark"], + description: "Theme for the component", + }, + iconSize: { + control: "select", + options: ["xs", "sm", "md", "lg", "xl"], + description: "Size of the wallet icon", + }, + textSize: { + control: "select", + options: ["xs", "sm", "md", "lg", "xl"], + description: "Size of the main address text", + }, + label: { + control: "text", + description: "Optional label to display above the address", + }, + address: { + control: "text", + description: "Wallet address to display", + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const Light: Story = { + args: { + theme: "light", + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const Dark: Story = { + args: { + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const WithLabel: Story = { + args: { + theme: "dark", + label: "Recipient Wallet", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const LargeSize: Story = { + args: { + theme: "light", + iconSize: "lg", + textSize: "md", + label: "Primary Wallet", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + }, + parameters: { + backgrounds: { default: "light" }, + }, +}; + +export const SmallSize: Story = { + args: { + theme: "dark", + iconSize: "sm", + textSize: "xs", + address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export const DifferentAddresses: Story = { + render: (args) => ( + +
+ + + +
+
+ ), + args: { + theme: "dark", + }, + parameters: { + backgrounds: { default: "dark" }, + }, +}; + +export default meta; diff --git a/packages/thirdweb/src/stories/utils.tsx b/packages/thirdweb/src/stories/utils.tsx index 45502b9fc3b..f1a739892da 100644 --- a/packages/thirdweb/src/stories/utils.tsx +++ b/packages/thirdweb/src/stories/utils.tsx @@ -1,4 +1,10 @@ import { createThirdwebClient } from "../client/client.js"; +import { + CustomThemeProvider, + useCustomTheme, +} from "../react/core/design-system/CustomThemeProvider.js"; +import type { Theme } from "../react/core/design-system/index.js"; +import { radius } from "../react/native/design-system/index.js"; const clientId = process.env.STORYBOOK_CLIENT_ID; @@ -9,3 +15,32 @@ if (!clientId) { export const storyClient = createThirdwebClient({ clientId: clientId, }); + +export const ModalThemeWrapper = (props: { + children: React.ReactNode; + theme: "light" | "dark" | Theme; +}) => { + const { theme } = props; + return ( + + {props.children} + + ); +}; + +export const ModalWrapper = (props: { children: React.ReactNode }) => { + const theme = useCustomTheme(); + return ( +
+ {props.children} +
+ ); +}; diff --git a/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts b/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts index 4ab9c12f4cd..4e4074ad9f5 100644 --- a/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts +++ b/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts @@ -13,7 +13,7 @@ export type MinimalWalletInfo = { /** * @internal */ -const ALL_MINIMAL_WALLET_INFOS = ([ +const ALL_MINIMAL_WALLET_INFOS = [ { id: "io.metamask", name: "MetaMask", @@ -2154,6 +2154,6 @@ const ALL_MINIMAL_WALLET_INFOS = ([ name: "WalletConnect", hasMobileSupport: false, }, -]) satisfies MinimalWalletInfo[]; +] as const satisfies MinimalWalletInfo[]; export default ALL_MINIMAL_WALLET_INFOS; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3187e7adc64..478b52134b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,7 +135,7 @@ importers: version: 0.6.19(@hyperjump/browser@1.3.0)(axios@1.9.0)(idb-keyval@6.2.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@19.1.0)(tailwindcss@3.4.17)(typescript@5.8.3) '@sentry/nextjs': specifier: 9.13.0 - version: 9.13.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9(esbuild@0.25.4)) + version: 9.13.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9) '@shazow/whatsabi': specifier: 0.21.0 version: 0.21.0(@noble/hashes@1.8.0)(typescript@5.8.3)(zod@3.25.24) @@ -349,7 +349,7 @@ importers: version: 8.6.14(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)) '@storybook/nextjs': specifier: 8.6.14 - version: 8.6.14(esbuild@0.25.4)(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4)) + version: 8.6.14(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9) '@storybook/react': specifier: 8.6.14 version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) @@ -1015,43 +1015,43 @@ importers: version: 3.592.0(@aws-sdk/client-sso-oidc@3.812.0) '@coinbase/wallet-mobile-sdk': specifier: ^1 - version: 1.1.2(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + version: 1.1.2(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) '@mobile-wallet-protocol/client': specifier: 1.0.0 - version: 1.0.0(pa2dnbx44bjd545fhrwcs7ul24) + version: 1.0.0(l3chqbbfq5xsam2v6kknqaadlm) '@react-native-async-storage/async-storage': specifier: ^1 || ^2 - version: 2.1.2(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + version: 2.1.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) '@react-native-community/netinfo': specifier: ^11 - version: 11.4.1(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + version: 11.4.1(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) '@walletconnect/react-native-compat': specifier: ^2 - version: 2.17.3(6pl4qrpuuyrxg6hqebpqzel6um) + version: 2.17.3(7vbla5aezw67h6uloqevg27fqe) expo-application: specifier: ^5 || ^6 - version: 6.0.1(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)) + version: 6.0.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)) expo-linking: specifier: ^6 || ^7 - version: 7.0.5(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + version: 7.0.5(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) expo-web-browser: specifier: ^13 || ^14 - version: 14.0.2(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + version: 14.0.2(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) react-native: specifier: '>=0.70' - version: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + version: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) react-native-aes-gcm-crypto: specifier: ^0.2 - version: 0.2.2(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + version: 0.2.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) react-native-get-random-values: specifier: ^1 - version: 1.11.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + version: 1.11.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) react-native-quick-crypto: specifier: '>=0.7' - version: 0.7.8(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + version: 0.7.8(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) react-native-svg: specifier: ^15 - version: 15.10.1(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + version: 15.10.1(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) typescript: specifier: '>=5.0.4' version: 5.8.3 @@ -1207,7 +1207,7 @@ importers: version: 2.1.2(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) '@size-limit/preset-big-lib': specifier: 11.2.0 - version: 11.2.0(bufferutil@4.0.9)(size-limit@11.2.0)(utf-8-validate@5.0.10) + version: 11.2.0(bufferutil@4.0.9)(esbuild@0.25.4)(size-limit@11.2.0)(utf-8-validate@5.0.10) '@storybook/addon-essentials': specifier: 8.6.14 version: 8.6.14(@types/react@19.1.4)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)) @@ -16443,7 +16443,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -16489,7 +16489,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -16535,7 +16535,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -16874,6 +16874,24 @@ snapshots: '@smithy/util-stream': 4.2.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': + dependencies: + '@aws-sdk/client-sts': 3.592.0 + '@aws-sdk/credential-provider-env': 3.587.0 + '@aws-sdk/credential-provider-http': 3.587.0 + '@aws-sdk/credential-provider-process': 3.587.0 + '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0)) + '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/types': 3.577.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/client-sts': 3.592.0 @@ -16928,6 +16946,25 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': + dependencies: + '@aws-sdk/credential-provider-env': 3.587.0 + '@aws-sdk/credential-provider-http': 3.587.0 + '@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-process': 3.587.0 + '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0)) + '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/types': 3.577.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' + - aws-crt + '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/credential-provider-env': 3.587.0 @@ -17000,6 +17037,19 @@ snapshots: '@smithy/types': 4.3.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))': + dependencies: + '@aws-sdk/client-sso': 3.592.0 + '@aws-sdk/token-providers': 3.587.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0)) + '@aws-sdk/types': 3.577.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + '@aws-sdk/credential-provider-sso@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)': dependencies: '@aws-sdk/client-sso': 3.592.0 @@ -17199,6 +17249,15 @@ snapshots: '@smithy/util-middleware': 4.0.3 tslib: 2.8.1 + '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))': + dependencies: + '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/types': 3.577.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.592.0)': dependencies: '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) @@ -17314,7 +17373,7 @@ snapshots: '@babel/traverse': 7.27.1 '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 7.7.2 @@ -17334,7 +17393,7 @@ snapshots: '@babel/traverse': 7.27.3 '@babel/types': 7.27.3 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 7.7.2 @@ -17359,7 +17418,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.1': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.27.3 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -17382,6 +17441,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.3) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.27.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17389,11 +17461,29 @@ snapshots: regexpu-core: 6.2.0 semver: 7.7.2 + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + regexpu-core: 6.2.0 + semver: 7.7.2 + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 debug: 4.4.1(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.10 @@ -17403,7 +17493,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/types': 7.27.3 transitivePeerDependencies: - supports-color @@ -17423,6 +17513,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.3 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.3)': dependencies: '@babel/core': 7.27.3 @@ -17434,7 +17542,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.27.3 '@babel/helper-plugin-utils@7.27.1': {} @@ -17447,6 +17555,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17456,10 +17573,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/types': 7.27.3 transitivePeerDependencies: - supports-color @@ -17506,6 +17632,14 @@ snapshots: dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.27.1 transitivePeerDependencies: - supports-color @@ -17515,11 +17649,21 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17529,10 +17673,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/traverse': 7.27.1 transitivePeerDependencies: - supports-color @@ -17546,11 +17707,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-decorators@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17563,127 +17738,252 @@ snapshots: dependencies: '@babel/core': 7.27.1 + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-export-default-from@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-export-default-from@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-async-generator-functions@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17693,6 +17993,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-generator-functions@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.3) + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17702,21 +18011,45 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.27.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.27.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17725,6 +18058,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17733,6 +18074,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-classes@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17745,60 +18094,126 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-classes@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.3) + '@babel/traverse': 7.27.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + '@babel/plugin-transform-destructuring@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring@7.27.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring@7.27.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17807,6 +18222,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17816,30 +18239,67 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 - '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.1) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color @@ -17852,10 +18312,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 - '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.1) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.3) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 '@babel/traverse': 7.27.1 @@ -17865,7 +18343,15 @@ snapshots: '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 - '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.1) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color @@ -17876,21 +18362,42 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-object-rest-spread@7.27.2(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17899,6 +18406,14 @@ snapshots: '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-object-rest-spread@7.27.2(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-object-rest-spread@7.27.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17907,6 +18422,14 @@ snapshots: '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.27.1) '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-object-rest-spread@7.27.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.27.3) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17915,11 +18438,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17928,11 +18464,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-parameters@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-parameters@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17941,6 +18490,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17950,16 +18507,35 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-display-name@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-display-name@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17967,16 +18543,33 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -17984,6 +18577,17 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) + '@babel/types': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.3) '@babel/types': 7.27.1 transitivePeerDependencies: - supports-color @@ -17994,22 +18598,44 @@ snapshots: '@babel/helper-annotate-as-pure': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-runtime@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18022,6 +18648,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-runtime@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.3) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.3) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.3) + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-runtime@7.27.3(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18034,11 +18672,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-runtime@7.27.3(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.3) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.3) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.3) + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18047,21 +18702,44 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18073,29 +18751,63 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.1) '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/preset-env@7.27.2(@babel/core@7.27.1)': dependencies: '@babel/compat-data': 7.27.2 @@ -18116,12 +18828,12 @@ snapshots: '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-transform-block-scoping': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-block-scoping': 7.27.3(@babel/core@7.27.1) '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.27.1) '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.1) @@ -18142,7 +18854,7 @@ snapshots: '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-transform-object-rest-spread': 7.27.2(@babel/core@7.27.1) + '@babel/plugin-transform-object-rest-spread': 7.27.3(@babel/core@7.27.1) '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.1) @@ -18171,6 +18883,81 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-env@7.27.2(@babel/core@7.27.3)': + dependencies: + '@babel/compat-data': 7.27.2 + '@babel/core': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.3) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.27.3) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-block-scoping': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-object-rest-spread': 7.27.2(@babel/core@7.27.3) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-regenerator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.27.3) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.27.3) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.3) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.3) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.3) + core-js-compat: 3.42.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + '@babel/preset-flow@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18182,6 +18969,13 @@ snapshots: dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.27.3 + esutils: 2.0.3 + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/types': 7.27.1 esutils: 2.0.3 @@ -18197,6 +18991,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-react@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/preset-typescript@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18208,6 +19014,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-typescript@7.27.1(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + '@babel/register@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -18239,7 +19056,7 @@ snapshots: '@babel/parser': 7.27.2 '@babel/template': 7.27.2 '@babel/types': 7.27.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -18251,7 +19068,7 @@ snapshots: '@babel/parser': 7.27.3 '@babel/template': 7.27.2 '@babel/types': 7.27.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -18711,6 +19528,18 @@ snapshots: react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) react-native-mmkv: 2.12.2(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@coinbase/wallet-mobile-sdk@1.1.2(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + bn.js: 5.2.1 + buffer: 6.0.3 + eth-rpc-errors: 4.0.3 + events: 3.3.0 + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-mmkv: 2.12.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@coinbase/wallet-sdk@3.9.3': dependencies: bn.js: 5.2.2 @@ -18758,6 +19587,14 @@ snapshots: - react - react-native + '@craftzdog/react-native-buffer@6.0.5(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': + dependencies: + ieee754: 1.2.1 + react-native-quick-base64: 2.1.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + transitivePeerDependencies: + - react + - react-native + '@dirtycajunrice/klee@1.0.6(react@19.1.0)': dependencies: react: 19.1.0 @@ -19466,7 +20303,7 @@ snapshots: ci-info: 3.9.0 compression: 1.8.0 connect: 3.7.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 env-editor: 0.4.2 freeport-async: 2.0.0 getenv: 1.0.0 @@ -19516,7 +20353,7 @@ snapshots: '@expo/plist': 0.3.4 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 getenv: 1.0.0 glob: 10.4.5 resolve-from: 5.0.0 @@ -19535,7 +20372,7 @@ snapshots: '@expo/plist': 0.2.2 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 getenv: 1.0.0 glob: 10.4.5 resolve-from: 5.0.0 @@ -19598,7 +20435,7 @@ snapshots: '@expo/env@0.4.2': dependencies: chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 dotenv: 16.4.7 dotenv-expand: 11.0.7 getenv: 1.0.0 @@ -19608,7 +20445,7 @@ snapshots: '@expo/env@1.0.5': dependencies: chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 dotenv: 16.4.7 dotenv-expand: 11.0.7 getenv: 1.0.0 @@ -19620,7 +20457,7 @@ snapshots: '@expo/spawn-async': 1.7.2 arg: 5.0.2 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 find-up: 5.0.0 getenv: 1.0.0 minimatch: 9.0.5 @@ -19664,7 +20501,7 @@ snapshots: '@expo/json-file': 9.1.4 '@expo/spawn-async': 1.7.2 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 dotenv: 16.4.7 dotenv-expand: 11.0.7 getenv: 1.0.0 @@ -19711,7 +20548,7 @@ snapshots: '@expo/image-utils': 0.7.4 '@expo/json-file': 9.1.4 '@react-native/normalize-colors': 0.79.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 resolve-from: 5.0.0 semver: 7.7.2 xml2js: 0.6.0 @@ -19732,6 +20569,12 @@ snapshots: react: 19.1.0 react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + '@expo/vector-icons@14.1.0(expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': + dependencies: + expo-font: 13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + '@expo/ws-tunnel@1.0.6': {} '@expo/xcpretty@4.3.2': @@ -20676,6 +21519,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@mobile-wallet-protocol/client@1.0.0(l3chqbbfq5xsam2v6kknqaadlm)': + dependencies: + '@noble/ciphers': 0.5.3 + '@noble/curves': 1.8.2 + '@noble/hashes': 1.7.2 + '@react-native-async-storage/async-storage': 2.1.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + eventemitter3: 5.0.1 + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + expo-web-browser: 14.0.2(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + fflate: 0.8.2 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + '@mobile-wallet-protocol/client@1.0.0(pa2dnbx44bjd545fhrwcs7ul24)': dependencies: '@noble/ciphers': 0.5.3 @@ -21283,7 +22139,7 @@ snapshots: dependencies: playwright: 1.52.0 - '@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9)': dependencies: ansi-html: 0.0.9 core-js-pure: 3.42.0 @@ -21293,7 +22149,7 @@ snapshots: react-refresh: 0.14.2 schema-utils: 4.3.2 source-map: 0.7.4 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 optionalDependencies: type-fest: 4.41.0 webpack-hot-middleware: 2.26.1 @@ -21469,7 +22325,7 @@ snapshots: '@puppeteer/browsers@2.7.1': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -22151,9 +23007,14 @@ snapshots: merge-options: 3.0.4 react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - '@react-native-community/netinfo@11.4.1(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))': + '@react-native-async-storage/async-storage@2.1.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))': dependencies: - react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + merge-options: 3.0.4 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + + '@react-native-community/netinfo@11.4.1(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))': + dependencies: + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) '@react-native/assets-registry@0.78.1': {} @@ -22165,6 +23026,14 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/babel-plugin-codegen@0.78.1(@babel/preset-env@7.27.2(@babel/core@7.27.3))': + dependencies: + '@babel/traverse': 7.27.1 + '@react-native/codegen': 0.78.1(@babel/preset-env@7.27.2(@babel/core@7.27.3)) + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + '@react-native/babel-plugin-codegen@0.79.2(@babel/core@7.27.1)': dependencies: '@babel/traverse': 7.27.3 @@ -22173,6 +23042,14 @@ snapshots: - '@babel/core' - supports-color + '@react-native/babel-plugin-codegen@0.79.2(@babel/core@7.27.3)': + dependencies: + '@babel/traverse': 7.27.3 + '@react-native/codegen': 0.79.2(@babel/core@7.27.3) + transitivePeerDependencies: + - '@babel/core' + - supports-color + '@react-native/babel-preset@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))': dependencies: '@babel/core': 7.27.1 @@ -22224,6 +23101,57 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/babel-preset@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))': + dependencies: + '@babel/core': 7.27.3 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-block-scoping': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-destructuring': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-object-rest-spread': 7.27.2(@babel/core@7.27.3) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-display-name': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-regenerator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-runtime': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.3) + '@babel/template': 7.27.2 + '@react-native/babel-plugin-codegen': 0.78.1(@babel/preset-env@7.27.2(@babel/core@7.27.3)) + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.3) + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + '@react-native/babel-preset@0.79.2(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -22274,6 +23202,56 @@ snapshots: transitivePeerDependencies: - supports-color + '@react-native/babel-preset@0.79.2(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-block-scoping': 7.27.3(@babel/core@7.27.3) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.27.3) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-object-rest-spread': 7.27.3(@babel/core@7.27.3) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-display-name': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-regenerator': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-runtime': 7.27.3(@babel/core@7.27.3) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.3) + '@babel/template': 7.27.2 + '@react-native/babel-plugin-codegen': 0.79.2(@babel/core@7.27.3) + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.3) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + '@react-native/codegen@0.78.1(@babel/preset-env@7.27.2(@babel/core@7.27.1))': dependencies: '@babel/parser': 7.27.2 @@ -22287,6 +23265,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@react-native/codegen@0.78.1(@babel/preset-env@7.27.2(@babel/core@7.27.3))': + dependencies: + '@babel/parser': 7.27.2 + '@babel/preset-env': 7.27.2(@babel/core@7.27.3) + glob: 7.2.3 + hermes-parser: 0.25.1 + invariant: 2.2.4 + jscodeshift: 17.3.0(@babel/preset-env@7.27.2(@babel/core@7.27.3)) + nullthrows: 1.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + '@react-native/codegen@0.79.2(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -22296,6 +23287,15 @@ snapshots: nullthrows: 1.1.1 yargs: 17.7.2 + '@react-native/codegen@0.79.2(@babel/core@7.27.3)': + dependencies: + '@babel/core': 7.27.3 + glob: 7.2.3 + hermes-parser: 0.25.1 + invariant: 2.2.4 + nullthrows: 1.1.1 + yargs: 17.7.2 + '@react-native/community-cli-plugin@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@react-native/dev-middleware': 0.78.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -22315,6 +23315,25 @@ snapshots: - supports-color - utf-8-validate + '@react-native/community-cli-plugin@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@react-native/dev-middleware': 0.78.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@react-native/metro-babel-transformer': 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3)) + chalk: 4.1.2 + debug: 2.6.9 + invariant: 2.2.4 + metro: 0.81.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) + metro-config: 0.81.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) + metro-core: 0.81.4 + readline: 1.3.0 + semver: 7.7.2 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - supports-color + - utf-8-validate + '@react-native/debugger-frontend@0.78.1': {} '@react-native/debugger-frontend@0.79.2': {} @@ -22370,6 +23389,16 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/metro-babel-transformer@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))': + dependencies: + '@babel/core': 7.27.3 + '@react-native/babel-preset': 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3)) + hermes-parser: 0.25.1 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + '@react-native/normalize-colors@0.78.1': {} '@react-native/normalize-colors@0.79.2': {} @@ -22383,6 +23412,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.4 + '@react-native/virtualized-lists@0.78.1(@types/react@19.1.4)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + '@types/react': 19.1.4 + '@react-stately/flags@3.1.1': dependencies: '@swc/helpers': 0.5.17 @@ -23459,7 +24497,7 @@ snapshots: '@sentry/core@9.13.0': {} - '@sentry/nextjs@9.13.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9(esbuild@0.25.4))': + '@sentry/nextjs@9.13.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.32.0 @@ -23470,7 +24508,7 @@ snapshots: '@sentry/opentelemetry': 9.13.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.32.0) '@sentry/react': 9.13.0(react@19.1.0) '@sentry/vercel-edge': 9.13.0 - '@sentry/webpack-plugin': 3.3.1(encoding@0.1.13)(webpack@5.99.9(esbuild@0.25.4)) + '@sentry/webpack-plugin': 3.3.1(encoding@0.1.13)(webpack@5.99.9) chalk: 3.0.0 next: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) resolve: 1.22.8 @@ -23547,12 +24585,12 @@ snapshots: '@opentelemetry/api': 1.9.0 '@sentry/core': 9.13.0 - '@sentry/webpack-plugin@3.3.1(encoding@0.1.13)(webpack@5.99.9(esbuild@0.25.4))': + '@sentry/webpack-plugin@3.3.1(encoding@0.1.13)(webpack@5.99.9)': dependencies: '@sentry/bundler-plugin-core': 3.3.1(encoding@0.1.13) unplugin: 1.0.1 uuid: 9.0.1 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 transitivePeerDependencies: - encoding - supports-color @@ -23635,7 +24673,7 @@ snapshots: '@sitespeed.io/tracium@0.3.3': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -23643,11 +24681,11 @@ snapshots: dependencies: size-limit: 11.2.0 - '@size-limit/preset-big-lib@11.2.0(bufferutil@4.0.9)(size-limit@11.2.0)(utf-8-validate@5.0.10)': + '@size-limit/preset-big-lib@11.2.0(bufferutil@4.0.9)(esbuild@0.25.4)(size-limit@11.2.0)(utf-8-validate@5.0.10)': dependencies: '@size-limit/file': 11.2.0(size-limit@11.2.0) '@size-limit/time': 11.2.0(bufferutil@4.0.9)(size-limit@11.2.0)(utf-8-validate@5.0.10) - '@size-limit/webpack': 11.2.0(size-limit@11.2.0) + '@size-limit/webpack': 11.2.0(esbuild@0.25.4)(size-limit@11.2.0) size-limit: 11.2.0 transitivePeerDependencies: - '@swc/core' @@ -23669,11 +24707,11 @@ snapshots: - supports-color - utf-8-validate - '@size-limit/webpack@11.2.0(size-limit@11.2.0)': + '@size-limit/webpack@11.2.0(esbuild@0.25.4)(size-limit@11.2.0)': dependencies: nanoid: 5.1.5 size-limit: 11.2.0 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) transitivePeerDependencies: - '@swc/core' - esbuild @@ -24453,7 +25491,7 @@ snapshots: ts-dedent: 2.2.0 vite: 6.3.5(@types/node@22.15.20)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - '@storybook/builder-webpack5@8.6.14(esbuild@0.25.4)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3)': + '@storybook/builder-webpack5@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)) '@types/semver': 7.7.0 @@ -24461,23 +25499,23 @@ snapshots: case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 constants-browserify: 1.0.0 - css-loader: 6.11.0(webpack@5.99.9(esbuild@0.25.4)) + css-loader: 6.11.0(webpack@5.99.9) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)) - html-webpack-plugin: 5.6.3(webpack@5.99.9(esbuild@0.25.4)) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9) + html-webpack-plugin: 5.6.3(webpack@5.99.9) magic-string: 0.30.17 path-browserify: 1.0.1 process: 0.11.10 semver: 7.7.2 storybook: 8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10) - style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.4)) - terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.99.9(esbuild@0.25.4)) + style-loader: 3.3.4(webpack@5.99.9) + terser-webpack-plugin: 5.3.14(webpack@5.99.9) ts-dedent: 2.2.0 url: 0.11.4 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.99.9(esbuild@0.25.4) - webpack-dev-middleware: 6.1.3(webpack@5.99.9(esbuild@0.25.4)) + webpack: 5.99.9 + webpack-dev-middleware: 6.1.3(webpack@5.99.9) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: @@ -24545,7 +25583,7 @@ snapshots: dependencies: storybook: 8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10) - '@storybook/nextjs@8.6.14(esbuild@0.25.4)(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4))': + '@storybook/nextjs@8.6.14(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9)': dependencies: '@babel/core': 7.27.1 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.1) @@ -24560,30 +25598,30 @@ snapshots: '@babel/preset-react': 7.27.1(@babel/core@7.27.1) '@babel/preset-typescript': 7.27.1(@babel/core@7.27.1) '@babel/runtime': 7.27.1 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4)) - '@storybook/builder-webpack5': 8.6.14(esbuild@0.25.4)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) - '@storybook/preset-react-webpack': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(esbuild@0.25.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9) + '@storybook/builder-webpack5': 8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) + '@storybook/preset-react-webpack': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) '@storybook/test': 8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)) '@types/semver': 7.7.0 - babel-loader: 9.2.1(@babel/core@7.27.1)(webpack@5.99.9(esbuild@0.25.4)) - css-loader: 6.11.0(webpack@5.99.9(esbuild@0.25.4)) + babel-loader: 9.2.1(@babel/core@7.27.1)(webpack@5.99.9) + css-loader: 6.11.0(webpack@5.99.9) find-up: 5.0.0 image-size: 1.2.1 loader-utils: 3.3.1 next: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9(esbuild@0.25.4)) + node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9) pnp-webpack-plugin: 1.7.0(typescript@5.8.3) postcss: 8.5.3 - postcss-loader: 8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)) + postcss-loader: 8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) react-refresh: 0.14.2 resolve-url-loader: 5.0.0 - sass-loader: 14.2.1(webpack@5.99.9(esbuild@0.25.4)) + sass-loader: 14.2.1(webpack@5.99.9) semver: 7.7.2 storybook: 8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10) - style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.4)) + style-loader: 3.3.4(webpack@5.99.9) styled-jsx: 5.1.7(@babel/core@7.27.1)(react@19.1.0) ts-dedent: 2.2.0 tsconfig-paths: 4.2.0 @@ -24591,7 +25629,7 @@ snapshots: optionalDependencies: sharp: 0.33.5 typescript: 5.8.3 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -24610,11 +25648,11 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(esbuild@0.25.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3)': + '@storybook/preset-react-webpack@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10))(typescript@5.8.3) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9) '@types/semver': 7.7.0 find-up: 5.0.0 magic-string: 0.30.17 @@ -24625,7 +25663,7 @@ snapshots: semver: 7.7.2 storybook: 8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10) tsconfig-paths: 4.2.0 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -24640,7 +25678,7 @@ snapshots: dependencies: storybook: 8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@5.0.10) - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9)': dependencies: debug: 4.4.1(supports-color@8.1.1) endent: 2.1.0 @@ -24650,7 +25688,7 @@ snapshots: react-docgen-typescript: 2.2.2(typescript@5.8.3) tslib: 2.8.1 typescript: 5.8.3 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 transitivePeerDependencies: - supports-color @@ -25823,7 +26861,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -25909,7 +26947,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.13 tinyrainbow: 2.0.0 - vitest: 3.1.4(@types/debug@4.1.12)(@types/node@22.14.1)(@vitest/ui@3.1.4)(happy-dom@17.4.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.8.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.20)(@vitest/ui@3.1.4)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.7.5(@types/node@22.15.20)(typescript@5.8.3))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@2.0.5': dependencies: @@ -26479,17 +27517,17 @@ snapshots: - '@types/react' - react - '@walletconnect/react-native-compat@2.17.3(6pl4qrpuuyrxg6hqebpqzel6um)': + '@walletconnect/react-native-compat@2.17.3(7vbla5aezw67h6uloqevg27fqe)': dependencies: - '@react-native-async-storage/async-storage': 2.1.2(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) - '@react-native-community/netinfo': 11.4.1(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + '@react-native-async-storage/async-storage': 2.1.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + '@react-native-community/netinfo': 11.4.1(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) events: 3.3.0 fast-text-encoding: 1.0.6 - react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - react-native-get-random-values: 1.11.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) - react-native-url-polyfill: 2.0.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-get-random-values: 1.11.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + react-native-url-polyfill: 2.0.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) optionalDependencies: - expo-application: 6.0.1(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)) + expo-application: 6.0.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)) '@walletconnect/relay-api@1.0.11': dependencies: @@ -27388,12 +28426,25 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@9.2.1(@babel/core@7.27.1)(webpack@5.99.9(esbuild@0.25.4)): + babel-jest@29.7.0(@babel/core@7.27.3): + dependencies: + '@babel/core': 7.27.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.27.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-loader@9.2.1(@babel/core@7.27.1)(webpack@5.99.9): dependencies: '@babel/core': 7.27.1 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 babel-plugin-istanbul@6.1.1: dependencies: @@ -27427,6 +28478,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.27.3): + dependencies: + '@babel/compat-data': 7.27.2 + '@babel/core': 7.27.3 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.3) + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.27.1): dependencies: '@babel/core': 7.27.1 @@ -27435,6 +28495,14 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.27.3): + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.3) + core-js-compat: 3.42.0 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.27.1): dependencies: '@babel/core': 7.27.1 @@ -27442,6 +28510,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.27.3): + dependencies: + '@babel/core': 7.27.3 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + babel-plugin-react-native-web@0.19.13: {} babel-plugin-syntax-hermes-parser@0.25.1: @@ -27454,6 +28529,12 @@ snapshots: transitivePeerDependencies: - '@babel/core' + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.27.3): + dependencies: + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.27.3) + transitivePeerDependencies: + - '@babel/core' + babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.1): dependencies: '@babel/core': 7.27.1 @@ -27473,6 +28554,25 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.1) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.1) + babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.3): + dependencies: + '@babel/core': 7.27.3 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.3) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.3) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.3) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.3) + babel-preset-expo@13.1.11(@babel/core@7.27.1): dependencies: '@babel/helper-module-imports': 7.27.1 @@ -27493,6 +28593,33 @@ snapshots: babel-plugin-react-native-web: 0.19.13 babel-plugin-syntax-hermes-parser: 0.25.1 babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.1) + debug: 4.4.1 + react-refresh: 0.14.2 + resolve-from: 5.0.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + babel-preset-expo@13.1.11(@babel/core@7.27.3): + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/plugin-proposal-decorators': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-object-rest-spread': 7.27.3(@babel/core@7.27.3) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.27.3) + '@babel/plugin-transform-runtime': 7.27.3(@babel/core@7.27.3) + '@babel/preset-react': 7.27.1(@babel/core@7.27.3) + '@babel/preset-typescript': 7.27.1(@babel/core@7.27.3) + '@react-native/babel-preset': 0.79.2(@babel/core@7.27.3) + babel-plugin-react-native-web: 0.19.13 + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.3) debug: 4.4.1(supports-color@8.1.1) react-refresh: 0.14.2 resolve-from: 5.0.0 @@ -27506,6 +28633,12 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.1) + babel-preset-jest@29.6.3(@babel/core@7.27.3): + dependencies: + '@babel/core': 7.27.3 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.3) + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -28376,7 +29509,7 @@ snapshots: css-gradient-parser@0.0.16: {} - css-loader@6.11.0(webpack@5.99.9(esbuild@0.25.4)): + css-loader@6.11.0(webpack@5.99.9): dependencies: icss-utils: 5.1.0(postcss@8.5.3) postcss: 8.5.3 @@ -28387,7 +29520,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 css-select@4.3.0: dependencies: @@ -28524,6 +29657,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -28995,7 +30132,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.4): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 esbuild: 0.25.4 transitivePeerDependencies: - supports-color @@ -29734,9 +30871,9 @@ snapshots: expect-type@1.2.1: {} - expo-application@6.0.1(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)): + expo-application@6.0.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: - expo: 53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) expo-asset@11.1.5(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: @@ -29748,6 +30885,16 @@ snapshots: transitivePeerDependencies: - supports-color + expo-asset@11.1.5(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + '@expo/image-utils': 0.7.4 + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + expo-constants: 17.1.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + expo-constants@17.0.8(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: '@expo/config': 10.0.11 @@ -29757,6 +30904,15 @@ snapshots: transitivePeerDependencies: - supports-color + expo-constants@17.0.8(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + dependencies: + '@expo/config': 10.0.11 + '@expo/env': 0.4.2 + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + expo-constants@17.1.6(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: '@expo/config': 11.0.10 @@ -29766,22 +30922,47 @@ snapshots: transitivePeerDependencies: - supports-color + expo-constants@17.1.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + dependencies: + '@expo/config': 11.0.10 + '@expo/env': 1.0.5 + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + expo-file-system@18.1.10(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: expo: 53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + expo-file-system@18.1.10(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + dependencies: + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: expo: 53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) fontfaceobserver: 2.3.0 react: 19.1.0 + expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + fontfaceobserver: 2.3.0 + react: 19.1.0 + expo-keep-awake@14.1.4(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: expo: 53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) react: 19.1.0 + expo-keep-awake@14.1.4(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + react: 19.1.0 + expo-linking@7.0.5(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: expo-constants: 17.0.8(expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) @@ -29792,6 +30973,16 @@ snapshots: - expo - supports-color + expo-linking@7.0.5(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + expo-constants: 17.0.8(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - expo + - supports-color + expo-modules-autolinking@2.1.10: dependencies: '@expo/spawn-async': 1.7.2 @@ -29811,6 +31002,11 @@ snapshots: expo: 53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + expo-web-browser@14.0.2(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + dependencies: + expo: 53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + expo@53.0.9(@babel/core@7.27.1)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10): dependencies: '@babel/runtime': 7.27.3 @@ -29840,6 +31036,35 @@ snapshots: - supports-color - utf-8-validate + expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10): + dependencies: + '@babel/runtime': 7.27.3 + '@expo/cli': 0.24.13(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@5.0.10) + '@expo/config': 11.0.10 + '@expo/config-plugins': 10.0.2 + '@expo/fingerprint': 0.12.4 + '@expo/metro-config': 0.20.14 + '@expo/vector-icons': 14.1.0(expo-font@13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + babel-preset-expo: 13.1.11(@babel/core@7.27.3) + expo-asset: 11.1.5(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + expo-constants: 17.1.6(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + expo-file-system: 18.1.10(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + expo-font: 13.3.1(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + expo-keep-awake: 14.1.4(expo@53.0.9(@babel/core@7.27.3)(bufferutil@4.0.9)(graphql@16.11.0)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + expo-modules-autolinking: 2.1.10 + expo-modules-core: 2.3.13 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-edge-to-edge: 1.6.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + whatwg-url-without-unicode: 8.0.0-3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-react-compiler + - bufferutil + - graphql + - supports-color + - utf-8-validate + exponential-backoff@3.1.2: {} extend@3.0.2: {} @@ -29859,7 +31084,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -30099,7 +31324,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -30114,7 +31339,7 @@ snapshots: semver: 7.7.2 tapable: 2.2.2 typescript: 5.8.3 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 form-data-encoder@2.1.4: {} @@ -30269,7 +31494,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -30707,7 +31932,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.4)): + html-webpack-plugin@5.6.3(webpack@5.99.9): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -30715,7 +31940,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 html-whitespace-sensitive-tag-names@3.0.1: {} @@ -30759,7 +31984,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -30788,7 +32013,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -30887,7 +32112,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -31193,7 +32418,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -31393,6 +32618,31 @@ snapshots: transitivePeerDependencies: - supports-color + jscodeshift@17.3.0(@babel/preset-env@7.27.2(@babel/core@7.27.3)): + dependencies: + '@babel/core': 7.27.1 + '@babel/parser': 7.27.2 + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.1) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.1) + '@babel/preset-flow': 7.27.1(@babel/core@7.27.1) + '@babel/preset-typescript': 7.27.1(@babel/core@7.27.1) + '@babel/register': 7.27.1(@babel/core@7.27.1) + flow-parser: 0.271.0 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + neo-async: 2.6.2 + picocolors: 1.1.1 + recast: 0.23.11 + tmp: 0.2.3 + write-file-atomic: 5.0.1 + optionalDependencies: + '@babel/preset-env': 7.27.2(@babel/core@7.27.3) + transitivePeerDependencies: + - supports-color + jsdoc-type-pratt-parser@4.1.0: {} jsesc@3.0.2: {} @@ -32485,7 +33735,7 @@ snapshots: micromark@4.0.1: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -32906,7 +34156,7 @@ snapshots: node-int64@0.4.0: {} - node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9(esbuild@0.25.4)): + node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9): dependencies: assert: 2.1.0 browserify-zlib: 0.2.0 @@ -32933,7 +34183,7 @@ snapshots: url: 0.11.4 util: 0.12.5 vm-browserify: 1.1.2 - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 node-releases@2.0.19: {} @@ -33367,7 +34617,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -33686,14 +34936,14 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - postcss-loader@8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)): + postcss-loader@8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 postcss: 8.5.3 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 transitivePeerDependencies: - typescript @@ -33851,7 +35101,7 @@ snapshots: proxy-agent@6.4.0: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -33864,7 +35114,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -34164,21 +35414,36 @@ snapshots: react: 19.1.0 react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-aes-gcm-crypto@0.2.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-edge-to-edge@1.6.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: react: 19.1.0 react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - react-native-get-random-values@1.11.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + react-native-edge-to-edge@1.6.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + + react-native-get-random-values@1.11.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: fast-base64-decode: 1.0.0 - react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) react-native-mmkv@2.12.2(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: react: 19.1.0 react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-mmkv@2.12.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-passkey@3.1.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: react: 19.1.0 @@ -34190,6 +35455,12 @@ snapshots: react: 19.1.0 react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-quick-base64@2.1.2(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + base64-js: 1.5.1 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-quick-crypto@0.7.8(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: '@craftzdog/react-native-buffer': 6.0.5(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) @@ -34200,6 +35471,16 @@ snapshots: string_decoder: 1.3.0 util: 0.12.5 + react-native-quick-crypto@0.7.8(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + '@craftzdog/react-native-buffer': 6.0.5(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + events: 3.3.0 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + readable-stream: 4.7.0 + string_decoder: 1.3.0 + util: 0.12.5 + react-native-svg@15.10.1(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: css-select: 5.1.0 @@ -34208,9 +35489,17 @@ snapshots: react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) warn-once: 0.1.1 - react-native-url-polyfill@2.0.0(react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + react-native-svg@15.10.1(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: - react-native: 0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + css-select: 5.1.0 + css-tree: 1.1.3 + react: 19.1.0 + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + warn-once: 0.1.1 + + react-native-url-polyfill@2.0.0(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)): + dependencies: + react-native: 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) whatwg-url-without-unicode: 8.0.0-3 react-native@0.78.1(@babel/core@7.27.1)(@babel/preset-env@7.27.2(@babel/core@7.27.1))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10): @@ -34262,6 +35551,55 @@ snapshots: - supports-color - utf-8-validate + react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.78.1 + '@react-native/codegen': 0.78.1(@babel/preset-env@7.27.2(@babel/core@7.27.3)) + '@react-native/community-cli-plugin': 0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@react-native/gradle-plugin': 0.78.1 + '@react-native/js-polyfills': 0.78.1 + '@react-native/normalize-colors': 0.78.1 + '@react-native/virtualized-lists': 0.78.1(@types/react@19.1.4)(react-native@0.78.1(@babel/core@7.27.3)(@babel/preset-env@7.27.2(@babel/core@7.27.3))(@types/react@19.1.4)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.27.3) + babel-plugin-syntax-hermes-parser: 0.25.1 + base64-js: 1.5.1 + chalk: 4.1.2 + commander: 12.1.0 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + memoize-one: 5.2.1 + metro-runtime: 0.81.4 + metro-source-map: 0.81.4 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.1.0 + react-devtools-core: 6.1.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.25.0 + semver: 7.7.2 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.1.4 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - '@react-native-community/cli' + - bufferutil + - supports-color + - utf-8-validate + react-pick-color@2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -34891,11 +36229,11 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@14.2.1(webpack@5.99.9(esbuild@0.25.4)): + sass-loader@14.2.1(webpack@5.99.9): dependencies: neo-async: 2.6.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 satori@0.12.2: dependencies: @@ -35237,7 +36575,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -35517,9 +36855,9 @@ snapshots: structured-headers@0.4.1: {} - style-loader@3.3.4(webpack@5.99.9(esbuild@0.25.4)): + style-loader@3.3.4(webpack@5.99.9): dependencies: - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 style-mod@4.1.2: {} @@ -36639,7 +37977,7 @@ snapshots: vite-node@3.1.4(@types/node@22.15.20)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(@types/node@22.15.20)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) @@ -36743,7 +38081,7 @@ snapshots: '@vitest/spy': 3.1.4 '@vitest/utils': 3.1.4 chai: 5.2.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -36895,7 +38233,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@6.1.3(webpack@5.99.9(esbuild@0.25.4)): + webpack-dev-middleware@6.1.3(webpack@5.99.9): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -36903,7 +38241,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.4) + webpack: 5.99.9 webpack-hot-middleware@2.26.1: dependencies: