diff --git a/.changeset/checkout-provider-core.md b/.changeset/checkout-provider-core.md new file mode 100644 index 000000000..edc906165 --- /dev/null +++ b/.changeset/checkout-provider-core.md @@ -0,0 +1,7 @@ +--- +"@lifi/widget-provider": minor +"@lifi/widget": minor +"@lifi/wallet-management": patch +--- + +Add the checkout session core to `@lifi/widget-provider` (contexts, session client, on-ramp session registry) and expose shared widget primitives via `@lifi/widget/shared`. diff --git a/.gitignore b/.gitignore index 8b994f310..a2e9791a1 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ next-env.d.ts .claude/worktrees/ .omc/ +.cursor/ diff --git a/CLAUDE.md b/CLAUDE.md index 402905467..a1f00101d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,6 +113,7 @@ QueryClient → Settings → WidgetConfig → I18n → Theme → SDK → Wallet - Library packages use `tsdown` with `unbundle: true` mode. The widget package needs `neverBundle: [/\.json$/]` for i18n JSON files. - **PR template** at `.github/pull_request_template.md` — always use it when creating PRs via `gh pr create`. - `packages/widget-embedded/README.md` — main integration guide for widget-light (not a typical package readme). +- **Minimal comments** — default to no comments. Add one short line only when the *why* is non-obvious (hidden constraint, subtle invariant, SDK quirk, workaround). Never narrate what the code does, never reference the task/PR/issue, never leave multi-paragraph docstrings on internal functions. ## Release diff --git a/knip.json b/knip.json index ee19f0b56..c1f15e8ad 100644 --- a/knip.json +++ b/knip.json @@ -26,6 +26,10 @@ "@walletconnect/ethereum-provider", "porto" ] + }, + "packages/widget": { + "ignore": ["src/config/version.ts"], + "ignoreDependencies": ["@transak/ui-js-sdk", "@meshconnect/web-link-sdk"] } } } diff --git a/packages/wallet-management/src/components/WalletMenuContent.tsx b/packages/wallet-management/src/components/WalletMenuContent.tsx index d64069509..5b4c4d22a 100644 --- a/packages/wallet-management/src/components/WalletMenuContent.tsx +++ b/packages/wallet-management/src/components/WalletMenuContent.tsx @@ -12,7 +12,7 @@ import { List, Typography, } from '@mui/material' -import { useMemo, useReducer, useRef } from 'react' +import { useEffect, useMemo, useReducer, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useAccount } from '../hooks/useAccount.js' import type { CombinedWallet } from '../hooks/useCombinedWallets.js' @@ -83,7 +83,19 @@ export const WalletMenuContent: React.FC = ({ .filter(Boolean) }, [accounts]) - const [state, dispatch] = useReducer(reducer, { view: 'wallet-list' }) + const openedWithWalletId = Boolean(walletChainArgs?.walletId) + + const [state, dispatch] = useReducer(reducer, undefined, (): State => { + const walletId = walletChainArgs?.walletId + if (!walletId) { + return { view: 'wallet-list' } + } + const wallet = installedWallets.find((w) => w.id === walletId) + if (!wallet) { + return { view: 'wallet-list' } + } + return { view: 'multi-ecosystem', selectedWalletId: walletId } + }) const handleMultiEcosystem = (id: string) => { dispatch({ type: 'SHOW_MULTI_ECOSYSTEM', id }) @@ -94,7 +106,18 @@ export const WalletMenuContent: React.FC = ({ } const handleBack = () => { - dispatch({ type: 'SHOW_WALLET_LIST' }) + if (openedWithWalletId) { + if (state.view === 'connecting') { + dispatch({ + type: 'SHOW_MULTI_ECOSYSTEM', + id: state.selectedWalletId!, + }) + } else { + onClose() + } + } else { + dispatch({ type: 'SHOW_WALLET_LIST' }) + } } const handleError = (id: string, error: any) => { @@ -119,6 +142,10 @@ export const WalletMenuContent: React.FC = ({ const targetChainType = walletChainArgs.chain?.chainType ?? walletChainArgs.chainType + if (!targetChainType) { + return installedWallets + } + return installedWallets .map((wallet) => { const filteredConnectors = wallet.connectors.filter( @@ -131,6 +158,23 @@ export const WalletMenuContent: React.FC = ({ .filter(Boolean) as typeof installedWallets }, [installedWallets, walletChainArgs]) + const filteredWalletsRef = useRef(filteredWallets) + filteredWalletsRef.current = filteredWallets + + useEffect(() => { + const walletId = walletChainArgs?.walletId + if (!walletId) { + dispatch({ type: 'SHOW_WALLET_LIST' }) + return + } + const wallet = filteredWalletsRef.current.find((w) => w.id === walletId) + if (!wallet) { + dispatch({ type: 'SHOW_WALLET_LIST' }) + return + } + dispatch({ type: 'SHOW_MULTI_ECOSYSTEM', id: walletId }) + }, [walletChainArgs?.walletId]) + const isMultiEcosystem = state.view === 'multi-ecosystem' const isConnecting = state.view === 'connecting' diff --git a/packages/wallet-management/src/index.ts b/packages/wallet-management/src/index.ts index db7fc323e..3e3bf734f 100644 --- a/packages/wallet-management/src/index.ts +++ b/packages/wallet-management/src/index.ts @@ -1,7 +1,16 @@ import type {} from '@mui/material/themeCssVarsAugmentation' +export { BitcoinListItemButton } from './components/BitcoinListItemButton.js' +export { CardListItemButton } from './components/CardListItemButton.js' +export { EthereumListItemButton } from './components/EthereumListItemButton.js' +export { SolanaListItemButton } from './components/SolanaListItemButton.js' +export { SuiListItemButton } from './components/SuiListItemButton.js' export * from './hooks/useAccount.js' export * from './hooks/useAccountDisconnect.js' +export { + type CombinedWallet, + useCombinedWallets, +} from './hooks/useCombinedWallets.js' export { useWalletManagementEvents, type WalletManagementEventEmitter, @@ -14,5 +23,12 @@ export * from './providers/WalletManagementProviders.js' export type { WalletMenuOpenArgs } from './providers/WalletMenuProvider/types.js' export * from './providers/WalletMenuProvider/WalletMenuContext.js' export * from './types/events.js' +export { WalletTagType } from './types/walletTagType.js' export * from './utils/getConnectorIcon.js' +export { getConnectorId } from './utils/getConnectorId.js' +export { getSortedByTags } from './utils/getSortedByTags.js' export * from './utils/getWalletPriority.js' +export { + getConnectorTagType, + getWalletTagType, +} from './utils/walletTags.js' diff --git a/packages/wallet-management/src/providers/WalletMenuProvider/types.ts b/packages/wallet-management/src/providers/WalletMenuProvider/types.ts index a47bb40ef..0df3fa529 100644 --- a/packages/wallet-management/src/providers/WalletMenuProvider/types.ts +++ b/packages/wallet-management/src/providers/WalletMenuProvider/types.ts @@ -3,6 +3,7 @@ import type { ChainType, ExtendedChain } from '@lifi/sdk' export interface WalletMenuOpenArgs { chain?: ExtendedChain chainType?: ChainType + walletId?: string } export interface WalletMenuContext { diff --git a/packages/widget-provider/package.json b/packages/widget-provider/package.json index 68ee85a3c..a309ae842 100644 --- a/packages/widget-provider/package.json +++ b/packages/widget-provider/package.json @@ -5,6 +5,10 @@ "type": "module", "main": "./src/index.ts", "types": "./src/index.d.ts", + "exports": { + ".": "./src/index.ts", + "./checkout": "./src/checkout/index.ts" + }, "sideEffects": false, "scripts": { "watch": "tsdown --watch", @@ -12,6 +16,7 @@ "build:prerelease": "node ../../scripts/prerelease.js && cpy '../../README.md' .", "build:postrelease": "node ../../scripts/postrelease.js && rm -rf README.md", "clean": "rm -rf dist", + "test": "vitest run", "check:types": "tsc --noEmit", "check:circular-deps": "madge --circular $(find ./src -name '*.ts' -o -name '*.tsx')", "check:circular-deps-graph": "madge --circular $(find ./src -name '*.ts' -o -name '*.tsx') --image graph.svg" @@ -37,12 +42,14 @@ "lifi" ], "dependencies": { - "@lifi/sdk": "^4.0.0" + "@lifi/sdk": "^4.0.0", + "zustand": "^5.0.12" }, "devDependencies": { "cpy-cli": "^7.0.0", "madge": "^8.0.0", "react": "^19.2.7", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.8" } } diff --git a/packages/widget-provider/src/checkout/api.ts b/packages/widget-provider/src/checkout/api.ts new file mode 100644 index 000000000..e504b4a5f --- /dev/null +++ b/packages/widget-provider/src/checkout/api.ts @@ -0,0 +1,34 @@ +/** Body for `POST /v1/checkout/onramp/session`. */ +export interface OnrampSessionRequest { + walletAddress: string + tokenAddress: string + chainId: number + integrator: string + amount?: string + fiatCurrency?: 'USD' | 'EUR' | 'GBP' + fiatAmount?: string +} + +export interface OnrampSessionResponse { + widgetUrl: string +} + +/** Body for `POST /v1/checkout/cex/session` (Mesh CEX funding). */ +export interface CexSessionRequest { + walletAddress: string + tokenAddress: string + chainId: number + userId?: string + integrator?: string + amount?: string +} + +export interface CexSessionResponse { + linkToken: string +} + +/** Common non-2xx payload shape returned by checkout session APIs. */ +export interface CheckoutSessionApiError { + error?: string + code?: string +} diff --git a/packages/widget-provider/src/checkout/contexts/CheckoutContext.ts b/packages/widget-provider/src/checkout/contexts/CheckoutContext.ts new file mode 100644 index 000000000..05080714f --- /dev/null +++ b/packages/widget-provider/src/checkout/contexts/CheckoutContext.ts @@ -0,0 +1,19 @@ +'use client' +import { type Context, createContext, useContext } from 'react' +import type { CheckoutContextValue } from '../types.js' + +export const CheckoutContext: Context = + createContext(null) + +/** + * Reads the runtime checkout config (integrator, callbacks, session API + * base URL) on-ramp host components depend on. Throws when called outside + * the widget's `CheckoutProvider`. + */ +export function useCheckoutConfig(): CheckoutContextValue { + const ctx = useContext(CheckoutContext) + if (!ctx) { + throw new Error('useCheckoutConfig must be used within CheckoutProvider') + } + return ctx +} diff --git a/packages/widget-provider/src/checkout/contexts/OnRampSessionsContext.test.ts b/packages/widget-provider/src/checkout/contexts/OnRampSessionsContext.test.ts new file mode 100644 index 000000000..6f37dd4dd --- /dev/null +++ b/packages/widget-provider/src/checkout/contexts/OnRampSessionsContext.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' +import type { OnRampSession } from '../types.js' +import { createOnRampSessionsStore } from './OnRampSessionsContext.js' + +const noop = () => {} + +function makeSession(): OnRampSession { + return { + open: noop, + close: noop, + cancel: noop, + isOpen: false, + isLoading: false, + error: null, + failure: null, + depositTxHash: null, + acknowledgeDepositTxHash: noop, + mountTargetId: null, + } +} + +describe('createOnRampSessionsStore', () => { + it('registers a session under an id', () => { + const store = createOnRampSessionsStore() + const session = makeSession() + store.getState().register('transak', session) + expect(store.getState().sessions.transak).toBe(session) + }) + + it('is a no-op (same state reference) when re-registering the same session', () => { + const store = createOnRampSessionsStore() + const session = makeSession() + store.getState().register('transak', session) + const before = store.getState().sessions + store.getState().register('transak', session) + expect(store.getState().sessions).toBe(before) + }) + + it('replaces the slot when registering a different session for the same id', () => { + const store = createOnRampSessionsStore() + const first = makeSession() + const second = makeSession() + store.getState().register('transak', first) + store.getState().register('transak', second) + expect(store.getState().sessions.transak).toBe(second) + }) + + it('does not touch other slots when registering', () => { + const store = createOnRampSessionsStore() + const transak = makeSession() + const mesh = makeSession() + store.getState().register('transak', transak) + store.getState().register('mesh', mesh) + expect(store.getState().sessions.transak).toBe(transak) + expect(store.getState().sessions.mesh).toBe(mesh) + }) + + it('removes a registered session on unregister', () => { + const store = createOnRampSessionsStore() + store.getState().register('transak', makeSession()) + store.getState().unregister('transak') + expect(store.getState().sessions.transak).toBeUndefined() + }) + + it('is a no-op (same state reference) when unregistering an unknown id', () => { + const store = createOnRampSessionsStore() + store.getState().register('transak', makeSession()) + const before = store.getState().sessions + store.getState().unregister('mesh') + expect(store.getState().sessions).toBe(before) + }) +}) diff --git a/packages/widget-provider/src/checkout/contexts/OnRampSessionsContext.ts b/packages/widget-provider/src/checkout/contexts/OnRampSessionsContext.ts new file mode 100644 index 000000000..cf74bf3db --- /dev/null +++ b/packages/widget-provider/src/checkout/contexts/OnRampSessionsContext.ts @@ -0,0 +1,77 @@ +'use client' +import { type Context, createContext, useContext, useEffect } from 'react' +import { useStore } from 'zustand' +import { createStore, type StoreApi } from 'zustand/vanilla' +import type { OnRampSession } from '../types.js' + +interface OnRampSessionsState { + sessions: Record + register: (id: string, session: OnRampSession) => void + unregister: (id: string) => void +} + +export type OnRampSessionsStore = StoreApi + +/** + * Creates a per-instance store that holds the registered `OnRampSession`s. + * The widget owns one store per `` mount; Hosts register + * into it and consumers subscribe to specific session slots via selectors. + */ +export function createOnRampSessionsStore(): OnRampSessionsStore { + return createStore((set) => ({ + sessions: {}, + register: (id, session) => + set((s) => + s.sessions[id] === session + ? s + : { sessions: { ...s.sessions, [id]: session } } + ), + unregister: (id) => + set((s) => { + if (!(id in s.sessions)) { + return s + } + const { [id]: _, ...rest } = s.sessions + return { sessions: rest } + }), + })) +} + +export const OnRampSessionsContext: Context = + createContext(null) + +function useOnRampSessionsStore(): OnRampSessionsStore { + const store = useContext(OnRampSessionsContext) + if (!store) { + throw new Error( + 'OnRampSessionsContext.Provider is missing — wrap descendants with ' + ) + } + return store +} + +/** + * Host hook: registers `session` under `id` for the lifetime of the mount. + * Re-runs when the `session` reference changes so consumers always see the + * latest state. CONTRACT: `session` must be memoized by the caller (see + * `OnRampSession`) — an unmemoized object re-registers every render. + */ +export function useRegisterOnRampSession( + id: string, + session: OnRampSession +): void { + const store = useOnRampSessionsStore() + useEffect(() => { + store.getState().register(id, session) + return () => store.getState().unregister(id) + }, [id, session, store]) +} + +/** + * Consumer hook: subscribes to a single session slot. Re-renders only when + * `sessions[id]` changes — other providers' updates do not trigger re-renders. + */ +export function useOnRampSession(id: string): OnRampSession | null { + const store = useOnRampSessionsStore() + return useStore(store, (s) => s.sessions[id] ?? null) +} diff --git a/packages/widget-provider/src/checkout/hooks/useCheckoutUserId.ts b/packages/widget-provider/src/checkout/hooks/useCheckoutUserId.ts new file mode 100644 index 000000000..2a524e3d1 --- /dev/null +++ b/packages/widget-provider/src/checkout/hooks/useCheckoutUserId.ts @@ -0,0 +1,31 @@ +'use client' +import { useState } from 'react' + +const STORAGE_KEY = 'lifi.checkout.userId' + +function getOrCreateCheckoutUserId(): string { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + return stored + } + const id = crypto.randomUUID() + localStorage.setItem(STORAGE_KEY, id) + return id + } catch { + // localStorage inaccessible — ephemeral UUID for this session. + return crypto.randomUUID() + } +} + +/** + * Returns a stable anonymous userId for the checkout session. Reads from / + * persists to `lifi.checkout.userId` in localStorage; falls back to an + * ephemeral UUID per mount when storage is unavailable. Providers like Mesh + * require a stable user id across sessions to resume link tokens and + * reconcile deposits, so the persistent identifier is intentional. + */ +export function useCheckoutUserId(): string { + const [userId] = useState(getOrCreateCheckoutUserId) + return userId +} diff --git a/packages/widget-provider/src/checkout/index.ts b/packages/widget-provider/src/checkout/index.ts new file mode 100644 index 000000000..0365e255c --- /dev/null +++ b/packages/widget-provider/src/checkout/index.ts @@ -0,0 +1,39 @@ +export type { + CexSessionRequest, + CexSessionResponse, + CheckoutSessionApiError, + OnrampSessionRequest, + OnrampSessionResponse, +} from './api.js' +export { + CheckoutContext, + useCheckoutConfig, +} from './contexts/CheckoutContext.js' +export { + createOnRampSessionsStore, + OnRampSessionsContext, + type OnRampSessionsStore, + useOnRampSession, + useRegisterOnRampSession, +} from './contexts/OnRampSessionsContext.js' +export { useCheckoutUserId } from './hooks/useCheckoutUserId.js' +export type { + CheckoutContextValue, + CheckoutError, + CheckoutResult, + OnRampError, + OnRampErrorCode, + OnRampFailure, + OnRampFailureKind, + OnRampFundingCategory, + OnRampHostWidgetConfig, + OnRampOpenArgs, + OnRampProvider, + OnRampProviderFactory, + OnRampSession, +} from './types.js' +export { + type CheckoutSessionRequestArgs, + type CheckoutSessionRequestResult, + postCheckoutSession, +} from './utils/sessionClient.js' diff --git a/packages/widget-provider/src/checkout/types.ts b/packages/widget-provider/src/checkout/types.ts new file mode 100644 index 000000000..e2af76643 --- /dev/null +++ b/packages/widget-provider/src/checkout/types.ts @@ -0,0 +1,192 @@ +import type { FC } from 'react' + +/** + * Structural subset of the widget's `WidgetConfig` that an on-ramp Host + * reads. The widget passes its full `WidgetConfig` at runtime; structural + * typing keeps provider packages from importing `@lifi/widget`'s type + * surface. Field names are intentionally identical to `WidgetConfig` so + * assignment works without conversion. + */ +export interface OnRampHostWidgetConfig { + apiKey?: string + toChain?: number + toToken?: string +} + +/** + * Funding category the widget routes by. The checkout's funding-source + * UI offers each category to the user; the registered provider for that + * category receives the `open()` call. + */ +export type OnRampFundingCategory = 'cash' | 'exchange' + +/** + * Adapter contract each `@lifi/widget-provider-*` on-ramp package implements. + * The factory returns identity metadata plus a `Host` component that mounts + * the provider's SDK and registers its session via `useRegisterOnRampSession`. + * + * Partners pass the array of adapters via `LifiWidgetCheckout`'s + * `onRampProviders` prop. Not installing a provider package keeps its SDK + * out of the bundle entirely. + */ +export interface OnRampProvider { + id: string + fundingCategory: OnRampFundingCategory + name: string + description: string + features: string[] + recommended?: boolean + /** + * Origins worth warming with `` before the provider + * opens (e.g. the SDK's iframe host) — opening is network-bound on them. + */ + preconnectOrigins?: string[] + Host: FC<{ widgetConfig: OnRampHostWidgetConfig }> +} + +export type OnRampProviderFactory = TOptions extends void + ? () => OnRampProvider + : (options: TOptions) => OnRampProvider + +/** + * Session a Host registers via `useRegisterOnRampSession`. The Host MUST pass a + * referentially-stable (memoized) object: the registry short-circuits on + * reference equality, so a new object every render thrashes register/unregister + * and churns subscribed consumers. Memoize it with `useMemo` keyed on its + * fields. + */ +export interface OnRampSession { + open: (args: OnRampOpenArgs) => void + close: () => void + /** + * User-initiated abort: surfaces a `cancelled` failure (with retry) instead + * of clearing state like `close()`, so the checkout returns to amount entry. + */ + cancel: () => void + isOpen: boolean + isLoading: boolean + /** + * Structured pre-flight error (e.g. session API failures shown next to the + * funding card). Hosts emit stable codes / free-text messages; the widget + * formats them with its own i18n. Terminal post-open failures live on + * `failure` instead. + */ + error: OnRampError | null + failure: OnRampFailure | null + /** + * On-chain deposit hash, set only when the provider reports a real hash + * (not an internal txId). Consumed by the checkout to drive status polling. + */ + depositTxHash: string | null + /** + * Clears `depositTxHash` after the consumer consumes it. Providers that + * never emit a hash (e.g. Transak) may implement this as a no-op. + */ + acknowledgeDepositTxHash: () => void + /** + * DOM element id the provider's SDK targets. The widget renders a + * `
` inside its hosted modal when this is set. + * Providers that manage their own overlay (e.g. Mesh) set this to `null`. + * + * NOTE: typically a `React.useId()` value, which contains `:` characters. + * Safe with `document.getElementById` (what every SDK we ship uses), but + * NOT a valid CSS selector — do not pass it to `document.querySelector`. + */ + mountTargetId: string | null +} + +/** + * Stable, provider-agnostic error codes the widget knows how to translate + * under `checkout.onramp.errors.`. Hosts emit one of these (with + * `{{providerName}}` and other interpolation params populated by the + * widget); free-text server messages travel on `OnRampError.message` + * instead. + */ +export type OnRampErrorCode = + | 'MISSING_API_URL' + | 'MISSING_API_KEY' + | 'TARGET_NOT_CONFIGURED' + | 'INVALID_RESPONSE' + | 'NETWORK_ERROR' + | 'SESSION_HTTP' + +export interface OnRampError { + /** Stable translation code; the widget renders the matching string. */ + code?: OnRampErrorCode + /** + * Free-text fallback (e.g. server error from API response). Takes + * precedence over `code` when present. + */ + message?: string + /** + * Extra interpolation params for the `code` translation (e.g. `status`). + * Reserved: `providerName` is supplied by the widget and overrides any + * value here. + */ + params?: Record +} + +export interface OnRampOpenArgs { + depositAddress: string + amount: string + fiatCurrency?: 'USD' | 'EUR' | 'GBP' + /** + * Prefilled fiat amount for the provider's widget (e.g. Transak's + * `fiatAmount`). Derived from the checkout's EnterAmount value times the + * source token's USD price; optional because not every provider honors it. + */ + fiatAmount?: string + /** + * The deposit asset the on-ramp must withdraw to `depositAddress`. This is + * the route's source token/chain (not the widget's destination): the LI.FI + * deposit address handles any final swap to the destination asset. + */ + fromChainId: number + fromTokenAddress: string +} + +export type OnRampFailureKind = + | 'connection' + | 'withdrawal' + | 'cancelled' + | 'unavailable' + +export interface OnRampFailure { + kind: OnRampFailureKind + /** + * Optional free-text override; the widget falls back to its own default + * description for the failure kind when this is undefined. + */ + message?: string + reportCode?: string + retry: () => void +} + +/** + * Runtime checkout config the on-ramp hosts need. The widget's + * `CheckoutProvider` pushes these fields into `CheckoutContext`; provider + * packages read them via `useCheckoutConfig`. + */ +export interface CheckoutContextValue { + integrator: string + /** Resolved from `sdkConfig.apiUrl` by `CheckoutSdkBridge`. */ + apiUrl?: string + onSuccess?: (result: CheckoutResult) => void + onError?: (error: CheckoutError) => void + /** @default true */ + resumePending?: boolean +} + +export interface CheckoutResult { + provider: string + transactionHash?: string + amount: string + token: string + chainId: number +} + +export interface CheckoutError { + code: string + message: string + provider?: string +} diff --git a/packages/widget-provider/src/checkout/utils/sessionClient.test.ts b/packages/widget-provider/src/checkout/utils/sessionClient.test.ts new file mode 100644 index 000000000..a0a79f764 --- /dev/null +++ b/packages/widget-provider/src/checkout/utils/sessionClient.test.ts @@ -0,0 +1,146 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { postCheckoutSession } from './sessionClient.js' + +function mockFetch( + status: number, + body: unknown, + { rejectJson = false }: { rejectJson?: boolean } = {} +) { + const fetchMock = vi.fn().mockResolvedValue({ + ok: status >= 200 && status < 300, + status, + json: rejectJson + ? vi.fn().mockRejectedValue(new Error('invalid json')) + : vi.fn().mockResolvedValue(body), + }) + vi.stubGlobal('fetch', fetchMock) + return fetchMock +} + +const base = { + endpointPath: '/v1/checkout/onramp/session' as const, + apiKey: 'key-123', + body: { amount: '10' }, +} + +describe('postCheckoutSession', () => { + beforeEach(() => { + vi.unstubAllGlobals() + }) + afterEach(() => { + vi.unstubAllGlobals() + vi.restoreAllMocks() + }) + + describe('base url normalization', () => { + it('does not double the /v1 segment when baseUrl ends in /v1', async () => { + const fetchMock = mockFetch(200, { id: 'a' }) + await postCheckoutSession({ ...base, baseUrl: 'https://li.quest/v1' }) + expect(fetchMock).toHaveBeenCalledWith( + 'https://li.quest/v1/checkout/onramp/session', + expect.any(Object) + ) + }) + + it('strips trailing slashes from baseUrl', async () => { + const fetchMock = mockFetch(200, { id: 'a' }) + await postCheckoutSession({ ...base, baseUrl: 'https://li.quest///' }) + expect(fetchMock).toHaveBeenCalledWith( + 'https://li.quest/v1/checkout/onramp/session', + expect.any(Object) + ) + }) + + it('keeps a plain baseUrl untouched', async () => { + const fetchMock = mockFetch(200, { id: 'a' }) + await postCheckoutSession({ ...base, baseUrl: 'https://li.quest' }) + expect(fetchMock).toHaveBeenCalledWith( + 'https://li.quest/v1/checkout/onramp/session', + expect.any(Object) + ) + }) + }) + + describe('headers', () => { + it('sets the api-key header and omits integrator when not provided', async () => { + const fetchMock = mockFetch(200, { id: 'a' }) + await postCheckoutSession({ ...base, baseUrl: 'https://li.quest' }) + const headers = fetchMock.mock.calls[0][1].headers + expect(headers['x-lifi-api-key']).toBe('key-123') + expect(headers['x-lifi-integrator']).toBeUndefined() + }) + + it('sets the integrator header when provided', async () => { + const fetchMock = mockFetch(200, { id: 'a' }) + await postCheckoutSession({ + ...base, + baseUrl: 'https://li.quest', + integrator: 'acme', + }) + const headers = fetchMock.mock.calls[0][1].headers + expect(headers['x-lifi-integrator']).toBe('acme') + }) + }) + + describe('success', () => { + it('returns ok with the parsed data on a 2xx object body', async () => { + mockFetch(200, { id: 'session-1' }) + const result = await postCheckoutSession< + typeof base.body, + { id: string } + >({ ...base, baseUrl: 'https://li.quest' }) + expect(result).toEqual({ ok: true, data: { id: 'session-1' } }) + }) + }) + + describe('failure', () => { + it('returns the parsed api error on a non-2xx response', async () => { + mockFetch(400, { error: 'bad request', code: 'INVALID' }) + const result = await postCheckoutSession({ + ...base, + baseUrl: 'https://li.quest', + }) + expect(result).toEqual({ + ok: false, + status: 400, + apiError: { error: 'bad request', code: 'INVALID' }, + }) + }) + + it('returns a null api error when an error body has no error/code fields', async () => { + mockFetch(500, { something: 'else' }) + const result = await postCheckoutSession({ + ...base, + baseUrl: 'https://li.quest', + }) + expect(result).toEqual({ ok: false, status: 500, apiError: null }) + }) + + it('returns a null api error when the error body is not JSON', async () => { + mockFetch(502, null, { rejectJson: true }) + const result = await postCheckoutSession({ + ...base, + baseUrl: 'https://li.quest', + }) + expect(result).toEqual({ ok: false, status: 502, apiError: null }) + }) + + it('treats a 2xx with a non-object body as a failure (misconfigured proxy)', async () => { + mockFetch(200, null) + const result = await postCheckoutSession({ + ...base, + baseUrl: 'https://li.quest', + }) + expect(result).toEqual({ ok: false, status: 200, apiError: null }) + }) + + it('treats a 2xx with a non-JSON body as a failure', async () => { + mockFetch(200, null, { rejectJson: true }) + const result = await postCheckoutSession({ + ...base, + baseUrl: 'https://li.quest', + }) + expect(result).toEqual({ ok: false, status: 200, apiError: null }) + }) + }) +}) diff --git a/packages/widget-provider/src/checkout/utils/sessionClient.ts b/packages/widget-provider/src/checkout/utils/sessionClient.ts new file mode 100644 index 000000000..7ef91f8c5 --- /dev/null +++ b/packages/widget-provider/src/checkout/utils/sessionClient.ts @@ -0,0 +1,83 @@ +import type { CheckoutSessionApiError } from '../api.js' + +export interface CheckoutSessionRequestArgs { + baseUrl: string + endpointPath: '/v1/checkout/onramp/session' | '/v1/checkout/cex/session' + apiKey: string + integrator?: string + body: TBody +} + +export type CheckoutSessionRequestResult = + | { ok: true; data: TData } + | { ok: false; status: number; apiError: CheckoutSessionApiError | null } + +function parseSessionApiError(data: unknown): CheckoutSessionApiError | null { + if (data == null || typeof data !== 'object') { + return null + } + const candidate = data as Record + const error = + typeof candidate.error === 'string' ? candidate.error : undefined + const code = typeof candidate.code === 'string' ? candidate.code : undefined + if (error === undefined && code === undefined) { + return null + } + return { error, code } +} + +/** + * `endpointPath` already includes `/v1/...`, so a `baseUrl` that itself + * ends in `/v1` would otherwise produce a doubled `/v1/v1/...`. Strip the + * trailing `/v1` (and any trailing slashes) so both forms work. + */ +function normalizeSessionApiBaseUrl(baseUrl: string): string { + const trimmed = baseUrl.replace(/\/+$/, '') + if (trimmed.endsWith('/v1')) { + return trimmed.slice(0, -3) + } + return trimmed +} + +export async function postCheckoutSession({ + baseUrl, + endpointPath, + apiKey, + integrator, + body, +}: CheckoutSessionRequestArgs): Promise< + CheckoutSessionRequestResult +> { + const normalizedBaseUrl = normalizeSessionApiBaseUrl(baseUrl) + const res = await fetch(`${normalizedBaseUrl}${endpointPath}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-lifi-api-key': apiKey, + ...(integrator ? { 'x-lifi-integrator': integrator } : {}), + }, + body: JSON.stringify(body), + }) + + const data: unknown = await res.json().catch(() => null) + if (!res.ok) { + return { + ok: false, + status: res.status, + apiError: parseSessionApiError(data), + } + } + + // A 2xx with a non-object body (empty body, HTML from a misconfigured + // proxy, JSON `null`) would otherwise be cast straight to `TData` and + // propagate as a structurally-invalid success. Treat it as a failure with + // no parseable api error. + if (data == null || typeof data !== 'object') { + return { ok: false, status: res.status, apiError: null } + } + + return { + ok: true, + data: data as TData, + } +} diff --git a/packages/widget-provider/tsdown.config.ts b/packages/widget-provider/tsdown.config.ts index c7b0477b2..3d3ff7525 100644 --- a/packages/widget-provider/tsdown.config.ts +++ b/packages/widget-provider/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsdown' export default defineConfig({ - entry: ['src/index.ts'], + entry: ['src/index.ts', 'src/checkout/index.ts'], outDir: 'dist/esm', format: 'esm', unbundle: true, diff --git a/packages/widget/package.json b/packages/widget/package.json index 1449e1a61..f8edb343b 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -5,6 +5,10 @@ "type": "module", "main": "./src/index.ts", "types": "./src/index.d.ts", + "exports": { + ".": "./src/index.ts", + "./shared": "./src/shared.ts" + }, "sideEffects": false, "scripts": { "watch": "tsdown --watch", @@ -29,6 +33,10 @@ "url": "https://github.com/lifinance/widget/issues" }, "license": "Apache-2.0", + "files": [ + "dist", + "README.md" + ], "keywords": [ "widget", "lifi-widget", diff --git a/packages/widget/src/components/TransactionCard/ActiveTransactionCard.style.tsx b/packages/widget/src/components/TransactionCard/ActiveTransactionCard.style.tsx index 7db935a4a..5626133fe 100644 --- a/packages/widget/src/components/TransactionCard/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/components/TransactionCard/ActiveTransactionCard.style.tsx @@ -22,3 +22,14 @@ export const DeleteButton: React.FC> = width: 24, height: 24, })) + +export const RetryButton: React.FC> = + styled(IconButton)(({ theme }) => ({ + fontWeight: 700, + fontSize: 12, + height: 24, + borderRadius: theme.vars.shape.borderRadius, + padding: theme.spacing(0.5, 1.5), + color: theme.vars.palette.text.primary, + backgroundColor: theme.vars.palette.background.paper, + })) diff --git a/packages/widget/src/hooks/useDisplayedTokens.ts b/packages/widget/src/hooks/useDisplayedTokens.ts new file mode 100644 index 000000000..ef5a7216f --- /dev/null +++ b/packages/widget/src/hooks/useDisplayedTokens.ts @@ -0,0 +1,70 @@ +import type { TokenExtended } from '@lifi/sdk' +import { useMemo } from 'react' +import type { FormType } from '../stores/form/types.js' +import { usePinnedTokensStore } from '../stores/pinnedTokens/PinnedTokensStore.js' +import { isSearchMatch } from '../utils/tokenList.js' +import { useTokens } from './useTokens.js' + +export type IsPinnedToken = (chainId: number, tokenAddress: string) => boolean + +// Balance-free core shared by useTokenList and useTokenBalances. +export const useDisplayedTokens = ( + selectedChainId?: number, + formType?: FormType, + isAllNetworks?: boolean, + search?: string +): { + allTokens: Record | undefined + displayedTokensList: TokenExtended[] + isPinnedToken: IsPinnedToken | undefined + isTokensLoading: boolean + isSearchLoading: boolean +} => { + const { + allTokens, + isLoading: isTokensLoading, + isSearchLoading, + } = useTokens(formType, search, isAllNetworks ? undefined : selectedChainId) + + const pinnedTokens = usePinnedTokensStore((state) => state.pinnedTokens) + + const isPinnedToken = useMemo(() => { + if (isAllNetworks) { + const pinnedSet = new Set() + Object.entries(pinnedTokens).forEach(([chainIdStr, addresses]) => { + const chainId = Number.parseInt(chainIdStr, 10) + addresses.forEach((address) => { + pinnedSet.add(`${chainId}-${address.toLowerCase()}`) + }) + }) + return (chainId, tokenAddress) => + pinnedSet.has(`${chainId}-${tokenAddress.toLowerCase()}`) + } + if (selectedChainId) { + const chainPinnedTokens = pinnedTokens[selectedChainId] || [] + const pinnedSet = new Set( + chainPinnedTokens.map((addr) => addr.toLowerCase()) + ) + return (chainId, tokenAddress) => + chainId === selectedChainId && pinnedSet.has(tokenAddress.toLowerCase()) + } + return undefined + }, [isAllNetworks, selectedChainId, pinnedTokens]) + + const displayedTokensList = useMemo(() => { + const tokensByChain = isAllNetworks + ? Object.values(allTokens ?? {}).flat() + : selectedChainId + ? allTokens?.[selectedChainId] + : undefined + return tokensByChain?.filter((t) => isSearchMatch(t, search)) ?? [] + }, [allTokens, isAllNetworks, selectedChainId, search]) + + return { + allTokens, + displayedTokensList, + isPinnedToken, + isTokensLoading, + isSearchLoading, + } +} diff --git a/packages/widget/src/hooks/useListHeight.ts b/packages/widget/src/hooks/useListHeight.ts index a9abb4449..dcb5d410a 100644 --- a/packages/widget/src/hooks/useListHeight.ts +++ b/packages/widget/src/hooks/useListHeight.ts @@ -32,6 +32,10 @@ const getContentHeight = ( console.warn( `Can't find ${ElementId.ScrollableContainer} or ${ElementId.Header} id.` ) + // Restore the height we zeroed above before returning early + if (listParentElement && oldHeight !== undefined) { + listParentElement.style.height = oldHeight + } return 0 } const { height: containerHeight } = containerElement.getBoundingClientRect() diff --git a/packages/widget/src/hooks/useTokenBalances.ts b/packages/widget/src/hooks/useTokenBalances.ts index 6afc80c3f..80de9dddf 100644 --- a/packages/widget/src/hooks/useTokenBalances.ts +++ b/packages/widget/src/hooks/useTokenBalances.ts @@ -1,14 +1,13 @@ import { useMemo } from 'react' import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' import type { FormType } from '../stores/form/types.js' -import { usePinnedTokensStore } from '../stores/pinnedTokens/PinnedTokensStore.js' import { useSettings } from '../stores/settings/useSettings.js' import type { TokenAmount } from '../types/token.js' import { formatTokenPrice } from '../utils/format.js' -import { isSearchMatch, processTokenBalances } from '../utils/tokenList.js' +import { isSearchMatch, processTokenList } from '../utils/tokenList.js' import { useAccountsBalancesData } from './useAccountsBalancesData.js' +import { useDisplayedTokens } from './useDisplayedTokens.js' import { useTokenBalancesQueries } from './useTokenBalancesQueries.js' -import { useTokens } from './useTokens.js' export const useTokenBalances = ( selectedChainId?: number, @@ -23,12 +22,14 @@ export const useTokenBalances = ( isSearchLoading: boolean isBalanceLoading: boolean } => { - const { hiddenUI } = useWidgetConfig() + const { hiddenUI, tokens: configTokens } = useWidgetConfig() const { allTokens, - isLoading: isTokensLoading, + displayedTokensList, + isPinnedToken, + isTokensLoading, isSearchLoading, - } = useTokens(formType, search, isAllNetworks ? undefined : selectedChainId) + } = useDisplayedTokens(selectedChainId, formType, isAllNetworks, search) const { data: accountsWithAllTokens, isLoading: isAccountsLoading } = useAccountsBalancesData(selectedChainId, formType, isAllNetworks, allTokens) @@ -39,58 +40,12 @@ export const useTokenBalances = ( const { data: allTokensWithBalances, isLoading: isBalanceQueriesLoading } = useTokenBalancesQueries(accountsWithAllTokens, isBalanceLoadingEnabled) - const { tokens: configTokens } = useWidgetConfig() const { smallBalanceThreshold } = useSettings(['smallBalanceThreshold']) - const pinnedTokens = usePinnedTokensStore((state) => state.pinnedTokens) - const isBalanceLoading = (isBalanceQueriesLoading || isAccountsLoading) && !allTokensWithBalances?.length - // Create function to check if token is pinned - const isPinnedToken = useMemo(() => { - if (isAllNetworks) { - // For all networks, check all pinned tokens - const allPinned: Array<{ chainId: number; tokenAddress: string }> = [] - Object.entries(pinnedTokens).forEach(([chainIdStr, addresses]) => { - const chainId = Number.parseInt(chainIdStr, 10) - addresses.forEach((address) => { - allPinned.push({ chainId, tokenAddress: address }) - }) - }) - const pinnedSet = new Set( - allPinned.map((p) => `${p.chainId}-${p.tokenAddress.toLowerCase()}`) - ) - return (chainId: number, tokenAddress: string) => { - const key = `${chainId}-${tokenAddress.toLowerCase()}` - return pinnedSet.has(key) - } - } else if (selectedChainId) { - // For single chain, check only selected chain - const chainPinnedTokens = pinnedTokens[selectedChainId] || [] - const pinnedSet = new Set( - chainPinnedTokens.map((addr) => addr.toLowerCase()) - ) - return (chainId: number, tokenAddress: string) => { - return ( - chainId === selectedChainId && - pinnedSet.has(tokenAddress.toLowerCase()) - ) - } - } - return undefined - }, [isAllNetworks, selectedChainId, pinnedTokens]) - - const displayedTokensList = useMemo(() => { - const tokensByChain = isAllNetworks - ? Object.values(allTokens ?? {}).flat() - : selectedChainId - ? allTokens?.[selectedChainId] - : undefined - return tokensByChain?.filter((t) => isSearchMatch(t, search)) ?? [] - }, [allTokens, isAllNetworks, selectedChainId, search]) - const displayedTokensWithBalances = useMemo(() => { const balancesByChain = isAllNetworks ? allTokensWithBalances @@ -160,7 +115,7 @@ export const useTokenBalances = ( ]) const { processedTokens, withCategories, withPinnedTokens } = useMemo(() => { - return processTokenBalances( + return processTokenList( isBalanceLoading, isAllNetworks || !!search, configTokens, diff --git a/packages/widget/src/hooks/useTokenList.ts b/packages/widget/src/hooks/useTokenList.ts new file mode 100644 index 000000000..7cd7051a1 --- /dev/null +++ b/packages/widget/src/hooks/useTokenList.ts @@ -0,0 +1,59 @@ +import { useMemo } from 'react' +import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' +import type { FormType } from '../stores/form/types.js' +import type { TokenAmount } from '../types/token.js' +import { processTokenList } from '../utils/tokenList.js' +import { useDisplayedTokens } from './useDisplayedTokens.js' + +/** + * Token-selection list without wallet balances - No accounts are resolved and no balance queries are fired. + */ +export const useTokenList = ( + selectedChainId?: number, + formType?: FormType, + isAllNetworks?: boolean, + search?: string +): { + tokens: TokenAmount[] + withCategories: boolean + withPinnedTokens: boolean + isTokensLoading: boolean + isSearchLoading: boolean +} => { + const { tokens: configTokens } = useWidgetConfig() + const { + displayedTokensList, + isPinnedToken, + isTokensLoading, + isSearchLoading, + } = useDisplayedTokens(selectedChainId, formType, isAllNetworks, search) + + const { processedTokens, withCategories, withPinnedTokens } = useMemo( + () => + processTokenList( + /* withoutBalances */ true, + isAllNetworks || !!search, + configTokens, + selectedChainId, + displayedTokensList, + undefined, + isPinnedToken + ), + [ + isAllNetworks, + search, + configTokens, + selectedChainId, + displayedTokensList, + isPinnedToken, + ] + ) + + return { + tokens: processedTokens ?? [], + withCategories, + withPinnedTokens, + isTokensLoading, + isSearchLoading, + } +} diff --git a/packages/widget/src/hooks/useTransactionDetails.ts b/packages/widget/src/hooks/useTransactionDetails.ts index 35d498f69..325b13b03 100644 --- a/packages/widget/src/hooks/useTransactionDetails.ts +++ b/packages/widget/src/hooks/useTransactionDetails.ts @@ -59,9 +59,7 @@ export const useTransactionDetails = ( if (fromAddress) { queryClient.setQueryData( [transactionHistoryQueryKey, fromAddress], - (data) => { - return [...data!, transaction!] - } + (data) => (data ? [...data, transaction!] : [transaction!]) ) } diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index fa1efa294..a6d900764 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -32,6 +32,9 @@ "delete": "Delete", "deposit": "Deposit", "depositReview": "Review deposit", + "depositWithCash": "Deposit with cash", + "connectExchange": "Connect exchange", + "transferCrypto": "Transfer crypto", "disconnect": "Disconnect", "done": "Done", "exchange": "Exchange", @@ -41,9 +44,16 @@ "max": "MAX", "ok": "Ok", "options": "Options", + "requestRefund": "Request refund", "reset": "Reset", "resetSettings": "Reset settings", + "resumeTransaction": "Resume transaction", + "retryDeposit": "Retry deposit", "seeDetails": "See details", + "topUp": "Top up", + "viewRefund": "View refund", + "viewRefundStatus": "View refund status", + "viewTransferDetails": "View transfer details", "showAll": "Show all", "startBridging": "Start bridging", "startSwapping": "Start swapping", @@ -61,8 +71,13 @@ "checkout": "Checkout", "checkoutDetails": "Checkout details", "deposit": "Deposit", + "depositAddress": "Deposit address", "depositDetails": "Deposit details", "depositTo": "Deposit to", + "depositWithCash": "Deposit with cash", + "connectExchange": "Connect exchange", + "transferCrypto": "Transfer crypto", + "currency": "Currency", "exchange": "Exchange", "from": "Exchange from", "gas": "Gas", @@ -316,6 +331,7 @@ "tokenSearch": "Search by token or address", "valueLoss": "Value loss", "searchBridges": "Search by bridge name", + "searchCurrency": "Search currency", "searchExchanges": "Search by exchange name", "searchNetwork": "Search network" }, @@ -356,5 +372,259 @@ "contractAddress": "Contract address", "marketCap": "Market cap", "volume24h": "Volume 24h" + }, + "tags": { + "connected": "Connected", + "getStarted": "Get Started", + "installed": "Installed", + "multichain": "Multichain", + "qrCode": "QR Code" + }, + "checkout": { + "closeConfirmation": { + "title": "Leave with transaction in progress?", + "body": "Your transaction is still being processed. You can come back to it any time by reopening the widget — we'll pick up where you left off.", + "confirm": "Yes, leave", + "cancel": "Stay" + }, + "transak": { + "forceClose": { + "tooltip": "Close (transaction won't resume)" + } + }, + "legal": { + "termsDisclaimer": "By continuing, you agree to LI.FI's Terms of Service." + }, + "resume": { + "notFoundToast": "We couldn't find your transaction. Start over or contact support if you think this is a mistake." + }, + "deposit": "Deposit", + "transferDeposit": { + "warning": "Only send {{amount}} {{symbol}} on {{chain}}. Wrong token or chain may be unrecoverable.", + "expiresIn": "Deposit address expires in {{minutes}}:{{seconds}}", + "addressLabel": "Deposit address", + "copyAddress": "Copy address", + "regenerate": "Generate new deposit address", + "expired": "This deposit window expired. Generate a new one to continue.", + "polling": "Watching for your deposit…", + "detailsTitle": "Deposit details", + "startNew": "Start a new transfer", + "startNewConfirmation": { + "title": "Start a new transfer?", + "body": "If you've already sent funds, they'll still reach your destination — check transaction history to confirm. Otherwise, this clears your previous transfer so you can pick a different token.", + "cancel": "Cancel", + "confirm": "Yes, start over" + }, + "table": { + "required": "Required", + "received": "Received", + "minRefundable": "Min. refundable", + "remaining": "Remaining", + "excess": "Excess", + "timeRemaining": "Time remaining" + }, + "errors": { + "unexpected": { + "title": "Unexpected deposit", + "description": "We received a deposit that doesn't match a supported route. Contact support for next steps." + }, + "amountLowThreshold": { + "title": "Deposit incomplete", + "description": "Top up to complete your order, or to enable a refund." + }, + "amountLowTopUp": { + "title": "Deposit incomplete", + "description": "Send the remaining before the deposit window closes." + }, + "amountLowExpired": { + "title": "Deposit incomplete", + "description": "The deposit window has expired. You can request a refund for the amount received." + }, + "excessHeld": { + "title": "Excess held", + "description": "Your order is processing. The excess is held — request a refund to retrieve it." + }, + "lateArrival": { + "title": "Deposit arrived late", + "description": "Your funds reached us after the deposit window closed. You can reactivate to continue, or request a refund." + }, + "addressExpired": { + "title": "Deposit address expired", + "description": "The deposit address has expired without receiving funds. You can try again with a new deposit address." + }, + "marketMoved": { + "title": "Transaction failed", + "description": "The exchange rate moved before your order could complete. Your funds are safe — a refund has been started." + }, + "refunding": { + "title": "Transaction failed — refunding", + "description": "We couldn't complete the transfer after several attempts. Your funds are being refunded to the wallet that sent them." + } + } + }, + "fiatCurrency": { + "label": "Pay with", + "USD": "US Dollar", + "EUR": "Euro", + "GBP": "British Pound" + }, + "flowSummary": { + "wallet": "From your wallet", + "transfer": "Transfer crypto", + "exchange": "From an exchange", + "cash": "Buy with cash" + }, + "transactionStatus": { + "detailsTitle": "Transaction details", + "watching": "Watching for transaction", + "executing": "Processing transaction", + "success": "Transaction successful", + "seeDetails": "See details", + "steps": { + "transferInitiated": "Transfer initiated", + "tokenReceived": "{{symbol}} received", + "swappedTo": "Swapped to {{symbol}}", + "bridgedTo": "Bridged to {{chain}}" + } + }, + "chooseFundingSource": "Choose funding source", + "insufficientFunds": "Insufficient funds — You don't have enough {{symbol}} on {{chain}}. Select a different token to try again.", + "useYourTokens": "Use your tokens", + "buyTokens": "Buy tokens", + "or": "or", + "payFromWallet": "Pay from Wallet", + "payFromWalletSubtitle": "Connect a browser wallet", + "payFromWalletConnectedSubtitle": "Select token & amount", + "transferCrypto": "Transfer Crypto", + "transferCryptoSubtitle": "From any wallet", + "connectExchange": "Connect Exchange", + "connectExchangeSubtitle": "Link your exchange account", + "depositWithCash": "Deposit with Cash", + "depositWithCashSubtitle": "Debit or credit card", + "connectWallet": "Connect wallet", + "moreWallets": "More", + "progress": { + "title": "Purchase in progress", + "description": "We're processing your purchase. This may take a few minutes." + }, + "onramp": { + "dialogTitle": "Deposit with {{providerName}}", + "close": "Close", + "errors": { + "MISSING_API_URL": "{{providerName}} deposit is not configured: set sdkConfig.apiUrl on the checkout.", + "MISSING_API_KEY": "{{providerName}} deposit is not configured: set widget apiKey to call checkout sessions.", + "TARGET_NOT_CONFIGURED": "{{providerName}} deposit target is not configured: set widget.toChain and widget.toToken.", + "INVALID_RESPONSE": "Invalid response from {{providerName}} session API.", + "NETWORK_ERROR": "Network error starting {{providerName}} session.", + "SESSION_HTTP": "Could not start {{providerName}} session ({{status}}). Try again later.", + "generic": "Something went wrong. Try again later." + }, + "failure": { + "connectionTitle": "Connection failed", + "connectionDescription": "We couldn't connect to {{providerName}}. Try again or contact support.", + "withdrawalTitle": "Withdrawal failed", + "withdrawalDescription": "{{providerName}} couldn't process your withdrawal. Your funds remain in your account.", + "transferTitle": "Transaction failed - refunding", + "transferDescription": "We couldn't complete the transfer after several attempts. Your funds are being refunded to the wallet that sent them." + } + }, + "status": { + "successCompleted": { + "title": "Transaction successful", + "description": "Your transaction has been completed successfully." + }, + "successPartial": { + "title": "Partially completed", + "description": "Your transaction completed, but some of the received tokens are not the requested destination tokens. Open the details to see what you received." + }, + "successRefund": { + "title": "Refund complete", + "description": "Your transaction could not be completed and your funds have been refunded." + }, + "pendingDefault": { + "title": "Processing transaction", + "description": "Your transaction is being processed. This may take a few minutes." + }, + "pendingRefund": { + "title": "Refunding your deposit", + "description": "We're sending your funds back. This usually takes a few minutes.", + "sentTo": "Sent to:" + }, + "pendingRetrying": { + "title": "Retrying transaction", + "description": "We encountered an issue and are automatically retrying your transaction." + }, + "errorExpired": { + "title": "Transaction expired", + "description": "The transaction window has expired. Please try again." + }, + "errorFailed": { + "title": "Transaction failed", + "description": "We couldn't complete your transaction. Try again or contact support." + }, + "errorConnection": { + "title": "Connection failed", + "description": "We couldn't connect to the payment provider. Try again or contact support.", + "exchange": { + "title": "Exchange connection failed", + "description": "We couldn't connect to your exchange account. Try again or contact support." + } + }, + "errorWithdrawal": { + "title": "Withdrawal failed", + "description": "The payment provider couldn't process your withdrawal. Your funds remain in your account.", + "cash": { + "title": "Payment didn't complete", + "description": "Your payment didn't go through. No funds were taken." + }, + "exchange": { + "title": "Exchange withdrawal failed", + "description": "Your exchange couldn't process the withdrawal. Your funds remain in your exchange account." + } + }, + "errorCancelled": { + "title": "Transaction cancelled", + "description": "Your transaction was cancelled. You can try again whenever you're ready.", + "cash": { + "title": "Payment cancelled", + "description": "You closed the payment window. Your order wasn't placed." + } + }, + "errorUnavailable": { + "title": "Service unavailable", + "description": "The payment provider is temporarily unavailable. Please try again later or contact support.", + "cash": { + "title": "Card payment unavailable", + "description": "Card payments are temporarily unavailable. Try again later or use a different method." + } + }, + "walletDisconnected": { + "title": "Wallet disconnected", + "description": "Your wallet was disconnected during the transaction. Reconnect and try again." + }, + "walletErrorFailed": { + "title": "Transaction failed", + "description": "We couldn't complete your transaction. View the details or try again." + }, + "walletErrorExpired": { + "title": "Order expired", + "description": "The transaction window expired before your order could be processed. Please try again." + }, + "walletPendingRefund": { + "title": "Refunding your deposit", + "description": "We're sending your funds back to the wallet you deposited from. This usually takes a few minutes.", + "sentToWallet": "Sent to wallet:" + }, + "walletSuccessRefund": { + "title": "Refund complete", + "description": "Your transaction could not be completed and your funds have been refunded to your wallet." + } + }, + "refund": { + "title": "Refund", + "inProgressDescription": "Your {{amount}} {{symbol}} on {{chain}} is being returned to your wallet. This usually takes a few minutes.", + "completeDescription": "Your {{amount}} {{symbol}} on {{chain}} was returned to your wallet.", + "viewTransaction": "View transaction" + } } } diff --git a/packages/widget/src/shared.ts b/packages/widget/src/shared.ts new file mode 100644 index 000000000..2aeb2308a --- /dev/null +++ b/packages/widget/src/shared.ts @@ -0,0 +1,177 @@ +// ───────────────────────────────────────────────────────────────────────────── +// @lifi/widget/shared +// +// Internal surface shared with sibling packages (currently @lifi/widget-checkout). +// These exports are NOT part of the public widget API. They may change in any +// release without notice. Application code must not depend on this subpath. +// ───────────────────────────────────────────────────────────────────────────── + +// Propagate widget's MUI theme augmentations (theme.vars, custom palette keys, +// component variants) to any consumer that imports through this subpath. +import type {} from '@mui/material/themeCssVarsAugmentation' +import type {} from './themes/types.js' + +// ── components ─────────────────────────────────────────────────────────────── +export { ActionRow } from './components/ActionRow/ActionRow.js' +export { + FormControl, + Input, +} from './components/AmountInput/AmountInput.style.js' +export { AmountInputEndAdornment } from './components/AmountInput/AmountInputEndAdornment.js' +export { InputPriceButton } from './components/AmountInput/PriceFormHelperText.style.js' +export { AvatarBadgedDefault } from './components/Avatar/Avatar.js' +export { TokenAvatar } from './components/Avatar/TokenAvatar.js' +export { BaseTransactionButton } from './components/BaseTransactionButton/BaseTransactionButton.js' +export type { BottomSheetBase } from './components/BottomSheet/types.js' +export { ButtonTertiary } from './components/ButtonTertiary.js' +export { Card } from './components/Card/Card.js' +export { CardIconButton } from './components/Card/CardIconButton.js' +export { CardTitle } from './components/Card/CardTitle.js' +export { InputCard } from './components/Card/InputCard.js' +export { ChainSelect } from './components/ChainSelect/ChainSelect.js' +export { ChainAvatar } from './components/ChainSelect/ChainSelect.style.js' +export { ContractComponent } from './components/ContractComponent/ContractComponent.js' +export { modalProps } from './components/Dialog/Dialog.js' +export { ExecutionStatusCard } from './components/ExecutionStatusCard/ExecutionStatusCard.js' +export { StatusIcon } from './components/ExecutionStatusCard/StatusIcon.js' +export { FeeBreakdownTooltip } from './components/FeeBreakdownTooltip.js' +export { IconCircle } from './components/IconCircle/IconCircle.js' +export { IconTypography } from './components/IconTypography.js' +export { ListItemButton } from './components/ListItem/ListItemButton.js' +export { WarningMessages } from './components/Messages/WarningMessages.js' +export { PageContainer } from './components/PageContainer.js' +export { PoweredBy } from './components/PoweredBy/PoweredBy.js' +export { ProgressToNextUpdate } from './components/ProgressToNextUpdate.js' +export { RouteCard } from './components/RouteCard/RouteCard.js' +export { RouteCardSkeleton } from './components/RouteCard/RouteCardSkeleton.js' +export { RouteDetails } from './components/RouteCard/RouteDetails.js' +export { + DetailInfoIcon, + DetailLabel, + DetailLabelContainer, + DetailRow, + DetailValue, +} from './components/RouteCard/RouteDetails.style.js' +export { RouteNotFoundCard } from './components/RouteCard/RouteNotFoundCard.js' +export { RouteTokens } from './components/RouteCard/RouteTokens.js' +export { SearchInput } from './components/Search/SearchInput.js' +export { + type ExecutionRow, + useExecutionRows, +} from './components/StepActions/executionRows.js' +export { SentToWalletRow } from './components/StepActions/SentToWalletRow.js' +export { StepActionsList } from './components/StepActions/StepActionsList.js' +export { Token } from './components/Token/Token.js' +export { TokenNotFound } from './components/TokenList/TokenNotFound.js' +export { useTokenSelect } from './components/TokenList/useTokenSelect.js' +export { VirtualizedTokenList } from './components/TokenList/VirtualizedTokenList.js' +export { TokenRate } from './components/TokenRate/TokenRate.js' +export { + DateLabelContainer, + DateLabelText, +} from './components/TransactionCard/TransactionCard.style.js' +// ── hooks ──────────────────────────────────────────────────────────────────── +export { useAddressActivity } from './hooks/useAddressActivity.js' +export { useAvailableChains } from './hooks/useAvailableChains.js' +export { useChain } from './hooks/useChain.js' +export { useContactSupport } from './hooks/useContactSupport.js' +export { useDebouncedWatch } from './hooks/useDebouncedWatch.js' +export { useExplorer } from './hooks/useExplorer.js' +export { useHeader } from './hooks/useHeader.js' +export { useListHeight } from './hooks/useListHeight.js' +export { useNavigateBack } from './hooks/useNavigateBack.js' +export { useRouteExecution } from './hooks/useRouteExecution.js' +export { useRouteExecutionMessage } from './hooks/useRouteExecutionMessage.js' +export { useRoutes } from './hooks/useRoutes.js' +export { useGetScrollableContainer } from './hooks/useScrollableContainer.js' +export { useToAddressRequirements } from './hooks/useToAddressRequirements.js' +export { useToken } from './hooks/useToken.js' +export { useTokenAddressBalance } from './hooks/useTokenAddressBalance.js' +export { useTokenBalances } from './hooks/useTokenBalances.js' +export { useTokenList } from './hooks/useTokenList.js' +export { useTools } from './hooks/useTools.js' +export { useWidgetEvents } from './hooks/useWidgetEvents.js' +// ── pages ──────────────────────────────────────────────────────────────────── +export { MainWarningMessages } from './pages/MainPage/MainWarningMessages.js' +export { Stack } from './pages/RoutesPage/RoutesPage.style.js' +export { SelectChainPage } from './pages/SelectChainPage/SelectChainPage.js' +export { SearchTokenInput } from './pages/SelectTokenPage/SearchTokenInput.js' +export { ContactSupportButton } from './pages/TransactionDetailsPage/ContactSupportButton.js' +export { ConfirmToAddressSheet } from './pages/TransactionPage/ConfirmToAddressSheet.js' +export { + ExchangeRateBottomSheet, + type ExchangeRateBottomSheetBase, +} from './pages/TransactionPage/ExchangeRateBottomSheet.js' +export { ExecutionDoneCard } from './pages/TransactionPage/ExecutionDoneCard.js' +export { RouteTracker } from './pages/TransactionPage/RouteTracker.js' +export { StartTransactionButton } from './pages/TransactionPage/StartTransactionButton.js' +export { TokenValueBottomSheet } from './pages/TransactionPage/TokenValueBottomSheet.js' +export { TransactionDoneButtons } from './pages/TransactionPage/TransactionDoneButtons.js' +export { + calculateValueLossPercentage, + getTokenValueLossThreshold, +} from './pages/TransactionPage/utils.js' + +// ── providers ──────────────────────────────────────────────────────────────── +export { I18nProvider } from './providers/I18nProvider/I18nProvider.js' +export { QueryClientProvider } from './providers/QueryClientProvider.js' +export { + SDKClientProvider, + useSDKClient, +} from './providers/SDKClientProvider.js' +export { WalletProvider } from './providers/WalletProvider/WalletProvider.js' +export { + useWidgetConfig, + WidgetProvider, +} from './providers/WidgetProvider/WidgetProvider.js' + +// ── stores ─────────────────────────────────────────────────────────────────── +export { useChainOrderStore } from './stores/chains/ChainOrderStore.js' +export { + FormKeyHelper, + type FormType, + type FormTypeProps, +} from './stores/form/types.js' +export { useFieldActions } from './stores/form/useFieldActions.js' +export { useFieldValues } from './stores/form/useFieldValues.js' +export { + useHeaderStore, + useSetHeaderHeight, +} from './stores/header/useHeaderStore.js' +export { useInputModeStore } from './stores/inputMode/useInputModeStore.js' +export { RouteExecutionStatus } from './stores/routes/types.js' +export { getSourceTxHash } from './stores/routes/utils.js' +export { StoreProvider } from './stores/StoreProvider.js' +export { + SettingsStoreProvider, + useSettingsStoreContext, +} from './stores/settings/SettingsStore.js' + +// ── themes ─────────────────────────────────────────────────────────────────── +export { createTheme } from './themes/createTheme.js' + +// ── types ──────────────────────────────────────────────────────────────────── +export { WidgetEvent } from './types/events.js' +export type { + FormRef, + WidgetConfig, + WidgetTheme, +} from './types/widget.js' + +// ── utils ──────────────────────────────────────────────────────────────────── +export { buildRouteFromTxHistory } from './utils/converters.js' +export { createElementId, ElementId } from './utils/elements.js' +export { hasEnumFlag } from './utils/enum.js' +export { getAccumulatedFeeCostsBreakdown } from './utils/fees.js' +export { + formatDuration, + formatInputAmount, + formatTokenAmount, + formatTokenPrice, + priceToTokenAmount, + usdDecimals, +} from './utils/format.js' +export { getPriceImpact } from './utils/getPriceImpact.js' +export { fitInputText } from './utils/input.js' +export { navigationRoutes } from './utils/navigationRoutes.js' +export { shortenAddress } from './utils/wallet.js' diff --git a/packages/widget/src/utils/tokenList.ts b/packages/widget/src/utils/tokenList.ts index 9973e1fa4..1780549b4 100644 --- a/packages/widget/src/utils/tokenList.ts +++ b/packages/widget/src/utils/tokenList.ts @@ -15,8 +15,8 @@ const sortByBalances = (a: TokenAmount, b: TokenAmount) => const sortByVolume = (a: TokenExtended, b: TokenExtended) => (b.volumeUSD24H ?? 0) - (a.volumeUSD24H ?? 0) -export const processTokenBalances = ( - isBalanceLoading: boolean, +export const processTokenList = ( + withoutBalances: boolean, noCategories: boolean, configTokens?: WidgetTokens, selectedChainId?: number, @@ -28,7 +28,7 @@ export const processTokenBalances = ( withCategories: boolean withPinnedTokens: boolean } => { - if (isBalanceLoading) { + if (withoutBalances) { if (noCategories) { const sortedTokens = [...(tokens ?? [])].sort(sortByVolume) // Separate pinned tokens diff --git a/packages/widget/tsdown.config.ts b/packages/widget/tsdown.config.ts index 91f3b0449..6d6242962 100644 --- a/packages/widget/tsdown.config.ts +++ b/packages/widget/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig, type UserConfig } from 'tsdown' const defaultConfig: UserConfig = defineConfig({ - entry: ['src/index.ts'], + entry: ['src/index.ts', 'src/shared.ts'], outDir: 'dist/esm', format: 'esm', unbundle: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e20fe474..3c2ad20f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1766,7 +1766,7 @@ importers: version: 6.0.2(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) react-scan: specifier: ^0.5.7 - version: 0.5.7(bufferutil@4.1.0)(esbuild@0.28.0)(eslint@10.4.1(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.0)(utf-8-validate@6.0.6) + version: 0.5.7(esbuild@0.28.0)(eslint@10.4.1(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.0) source-map-explorer: specifier: ^2.5.3 version: 2.5.3 @@ -1788,6 +1788,9 @@ importers: '@lifi/sdk': specifier: ^4.0.0 version: 4.0.0 + zustand: + specifier: ^5.0.12 + version: 5.0.14(@types/react@19.2.16)(react@19.2.7)(use-sync-external-store@1.4.0(react@19.2.7)) devDependencies: cpy-cli: specifier: ^7.0.0 @@ -1801,6 +1804,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) packages/widget-provider-bitcoin: dependencies: @@ -2702,12 +2708,6 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@effect/platform-node-shared@4.0.0-beta.70': - resolution: {integrity: sha512-3VXuL63IDmq13We+ApRKn2JW3Rb9g5gj1YEmfb8u2b73norur1VsIJ/pRE4qjShevg19dQYi2JsLawSZ6gApug==} - engines: {node: '>=18.0.0'} - peerDependencies: - effect: ^4.0.0-beta.70 - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -4229,36 +4229,6 @@ packages: resolution: {integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==} engines: {node: '>= 18'} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} - cpu: [x64] - os: [win32] - '@mui/core-downloads-tracker@9.0.1': resolution: {integrity: sha512-GzamIIhZ1bH77dq7eKaeyRgJdkypsxin4jBFq2EMs4lBWRR0LFO1CSVMsoebn/VvjcNrnrOrjy48MkrkQUK2iw==} @@ -5533,48 +5503,97 @@ packages: cpu: [x64] os: [win32] + '@oxlint/binding-android-arm-eabi@1.66.0': + resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxlint/binding-android-arm-eabi@1.68.0': resolution: {integrity: sha512-wEdsIspexXLLMCPAEOcCuFLMt6aE3AzTuA/nQKLPRnoJ+EQTturmGheDkhHuuVHx0GbutjQ3JKmEn+Gz6Ag28Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] + '@oxlint/binding-android-arm64@1.66.0': + resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxlint/binding-android-arm64@1.68.0': resolution: {integrity: sha512-6aZRNNXQTsYtgaus8HTb9nuCcsrQTlKXGnktwvwW0n/SooRWNxNb3925grDkC63aEYZuCIyOVLV16IdYIoC2aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxlint/binding-darwin-arm64@1.66.0': + resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxlint/binding-darwin-arm64@1.68.0': resolution: {integrity: sha512-lVTbsE3kO4bLpZELgjRZuAJc8kP98wb83yMXWH8gaPaFZ+cM2IDeZto4ByoUAYj0Mxv2rvw+A1ssZequSepVSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxlint/binding-darwin-x64@1.66.0': + resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxlint/binding-darwin-x64@1.68.0': resolution: {integrity: sha512-nCmw2XrmQskjBUh/sfP5yKs93V68LijQgjd1cuuZ/q4SCARngLYs60/qqyzuMsg8QQ9KArDI98hxs/RDGE4KRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxlint/binding-freebsd-x64@1.66.0': + resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxlint/binding-freebsd-x64@1.68.0': resolution: {integrity: sha512-TI4ovQJliYE9V6e06cEv+qEI9uj7Ao65fmif4er4HD+aouyYyh0P31q2jh3KtqsOHHcQqv2PZ61TjJFLpBDGWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxlint/binding-linux-arm-gnueabihf@1.68.0': resolution: {integrity: sha512-LcNnEi9g71Cmry5ZpLbKT+oVv+/zYG3hYVAbBBB5X85nOQZSk8l92CnDkxJMcxUg0NCnMCOFZuaVDlMyv4tYJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxlint/binding-linux-arm-musleabihf@1.68.0': resolution: {integrity: sha512-OovHahL3FX4UaK+hgSf11llUx2vszqjSdQQ61Ck9InOEI/ptZoC4XSQJurITqItVvd53JSlmkLMeaNjM1PoQew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxlint/binding-linux-arm64-gnu@1.66.0': + resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxlint/binding-linux-arm64-gnu@1.68.0': resolution: {integrity: sha512-YbzTglnHLzzi9zv5or8Ztz5fykAoZE8W9iM42/bOrF4HBSB6rJTqdLQWuoP76EHQw9DuKl76K1QmFlG29sPJXQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5582,6 +5601,13 @@ packages: os: [linux] libc: [glibc] + '@oxlint/binding-linux-arm64-musl@1.66.0': + resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxlint/binding-linux-arm64-musl@1.68.0': resolution: {integrity: sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5589,6 +5615,13 @@ packages: os: [linux] libc: [musl] + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxlint/binding-linux-ppc64-gnu@1.68.0': resolution: {integrity: sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5596,6 +5629,13 @@ packages: os: [linux] libc: [glibc] + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxlint/binding-linux-riscv64-gnu@1.68.0': resolution: {integrity: sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5603,6 +5643,13 @@ packages: os: [linux] libc: [glibc] + '@oxlint/binding-linux-riscv64-musl@1.66.0': + resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxlint/binding-linux-riscv64-musl@1.68.0': resolution: {integrity: sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5610,6 +5657,13 @@ packages: os: [linux] libc: [musl] + '@oxlint/binding-linux-s390x-gnu@1.66.0': + resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxlint/binding-linux-s390x-gnu@1.68.0': resolution: {integrity: sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5617,6 +5671,13 @@ packages: os: [linux] libc: [glibc] + '@oxlint/binding-linux-x64-gnu@1.66.0': + resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxlint/binding-linux-x64-gnu@1.68.0': resolution: {integrity: sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5624,6 +5685,13 @@ packages: os: [linux] libc: [glibc] + '@oxlint/binding-linux-x64-musl@1.66.0': + resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxlint/binding-linux-x64-musl@1.68.0': resolution: {integrity: sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5631,24 +5699,48 @@ packages: os: [linux] libc: [musl] + '@oxlint/binding-openharmony-arm64@1.66.0': + resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxlint/binding-openharmony-arm64@1.68.0': resolution: {integrity: sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@oxlint/binding-win32-arm64-msvc@1.66.0': + resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxlint/binding-win32-arm64-msvc@1.68.0': resolution: {integrity: sha512-fHNtVqPHSYE7UFDSLVFUjxQjnSVXxseNJmRW+XuP4pXXDwePdPda43NL7/BBCFTxHjycOc44JNDaOPtFDNui9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@oxlint/binding-win32-ia32-msvc@1.66.0': + resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxlint/binding-win32-ia32-msvc@1.68.0': resolution: {integrity: sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@oxlint/binding-win32-x64-msvc@1.66.0': + resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxlint/binding-win32-x64-msvc@1.68.0': resolution: {integrity: sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9855,8 +9947,8 @@ packages: des.js@1.1.0: resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} - deslop-js@0.0.14: - resolution: {integrity: sha512-FY0rNK+mdTozXwNmcJAt5tD9hVOkXEX5ac+Mg77KJvnnsoAh3qIUW64PMnofutjeYz7g1vENMbOcrBmD1v4FlA==} + deslop-js@0.0.24: + resolution: {integrity: sha512-ygcRwJXCUedo+hN1L4Ysm7luo4VWOvKEz/kRKWMlFp5bAtTKeXo+8fk/hQaJKsJ/nfahiqkhlYTlrKIR/5nsQg==} destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -10036,9 +10128,6 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@4.0.0-beta.70: - resolution: {integrity: sha512-8AwGTRiNriirHGEYHrOS0E9fzdhIqCdZjiHP1YXmNo2UyPGS43ILsymsSHT7V0DJS+8dvlKq2RxnrDBUhDNZHg==} - ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -10427,10 +10516,6 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} - fast-check@4.8.0: - resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==} - engines: {node: '>=12.17.0'} - fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -10542,9 +10627,6 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -11098,10 +11180,6 @@ packages: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} @@ -11540,9 +11618,6 @@ packages: knitwork@1.3.0: resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - launch-editor@2.14.1: resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} @@ -12251,22 +12326,12 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.4: - resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} - hasBin: true - - msgpackr@2.0.2: - resolution: {integrity: sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==} - muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} - nanoclone@0.2.1: resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} @@ -12371,10 +12436,6 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -12648,9 +12709,19 @@ packages: rolldown: optional: true - oxlint-plugin-react-doctor@0.2.16: - resolution: {integrity: sha512-Ul1UxcYfjBnt+HjgNaMnkvMJjyewS6rkSZl3LiD1g/QqSll1jiynYH/ZoNpUaGHp1pDQ/xLvCcXNQvqopN0tHA==} + oxlint-plugin-react-doctor@0.5.5: + resolution: {integrity: sha512-otsA5vf5JVISa5V7Jg8hdvRaFsXADTPmqwT8s9e7sMNEg+me/+nXviI0DvfQ22vmNpXK6yzq96IGIGVI1uqnRw==} + engines: {node: ^20.19.0 || >=22.13.0} + + oxlint@1.66.0: + resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true oxlint@1.68.0: resolution: {integrity: sha512-dXcbq+xsmLrMy6T8d0euf3IYUfLmjHIE11pOxiUSi5LHkFZaYPv568R6sEjcavVpUxoaQe66UBuK4HEi74NxpA==} @@ -13344,9 +13415,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pure-rand@8.4.0: - resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} - qr-code-styling@1.9.2: resolution: {integrity: sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==} engines: {node: '>=18.18.0'} @@ -13456,9 +13524,9 @@ packages: react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} - react-doctor@0.2.16: - resolution: {integrity: sha512-o108QscqM/TlC1QmKgDORf/P53PeZ69nZbRsvy53rXkKvWdKZC1Y0u1JRPmThbTh6v+QpMispBwEnFHhlpZQ7w==} - engines: {node: ^20.19.0 || >=22.12.0} + react-doctor@0.5.5: + resolution: {integrity: sha512-rNYd/a5ykD+WislQ8HIhqmtTddyveQOOzs4PuI1INel41V79uWb8fNZtnZX6hgJIObZdLIlLU7u6vwSzCUAveA==} + engines: {node: ^20.19.0 || >=22.13.0} hasBin: true react-dom@19.2.7: @@ -14642,10 +14710,6 @@ packages: toml@3.0.0: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - toml@4.1.1: - resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} - engines: {node: '>=20'} - toposort@2.0.2: resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} @@ -14689,6 +14753,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -15478,6 +15543,23 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -17554,15 +17636,6 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@effect/platform-node-shared@4.0.0-beta.70(bufferutil@4.1.0)(effect@4.0.0-beta.70)(utf-8-validate@6.0.6)': - dependencies: - '@types/ws': 8.18.1 - effect: 4.0.0-beta.70 - ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -19107,24 +19180,6 @@ snapshots: '@msgpack/msgpack@3.1.3': {} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - optional: true - '@mui/core-downloads-tracker@9.0.1': {} '@mui/icons-material@9.0.1(@mui/material@9.0.1(@emotion/react@11.14.0(@types/react@19.2.16)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.16)(react@19.2.7))(@types/react@19.2.16)(react@19.2.7))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.16)(react@19.2.7)': @@ -20246,60 +20301,117 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.131.0': optional: true + '@oxlint/binding-android-arm-eabi@1.66.0': + optional: true + '@oxlint/binding-android-arm-eabi@1.68.0': optional: true + '@oxlint/binding-android-arm64@1.66.0': + optional: true + '@oxlint/binding-android-arm64@1.68.0': optional: true + '@oxlint/binding-darwin-arm64@1.66.0': + optional: true + '@oxlint/binding-darwin-arm64@1.68.0': optional: true + '@oxlint/binding-darwin-x64@1.66.0': + optional: true + '@oxlint/binding-darwin-x64@1.68.0': optional: true + '@oxlint/binding-freebsd-x64@1.66.0': + optional: true + '@oxlint/binding-freebsd-x64@1.68.0': optional: true + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + optional: true + '@oxlint/binding-linux-arm-gnueabihf@1.68.0': optional: true + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + optional: true + '@oxlint/binding-linux-arm-musleabihf@1.68.0': optional: true + '@oxlint/binding-linux-arm64-gnu@1.66.0': + optional: true + '@oxlint/binding-linux-arm64-gnu@1.68.0': optional: true + '@oxlint/binding-linux-arm64-musl@1.66.0': + optional: true + '@oxlint/binding-linux-arm64-musl@1.68.0': optional: true + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + optional: true + '@oxlint/binding-linux-ppc64-gnu@1.68.0': optional: true + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + optional: true + '@oxlint/binding-linux-riscv64-gnu@1.68.0': optional: true + '@oxlint/binding-linux-riscv64-musl@1.66.0': + optional: true + '@oxlint/binding-linux-riscv64-musl@1.68.0': optional: true + '@oxlint/binding-linux-s390x-gnu@1.66.0': + optional: true + '@oxlint/binding-linux-s390x-gnu@1.68.0': optional: true + '@oxlint/binding-linux-x64-gnu@1.66.0': + optional: true + '@oxlint/binding-linux-x64-gnu@1.68.0': optional: true + '@oxlint/binding-linux-x64-musl@1.66.0': + optional: true + '@oxlint/binding-linux-x64-musl@1.68.0': optional: true + '@oxlint/binding-openharmony-arm64@1.66.0': + optional: true + '@oxlint/binding-openharmony-arm64@1.68.0': optional: true + '@oxlint/binding-win32-arm64-msvc@1.66.0': + optional: true + '@oxlint/binding-win32-arm64-msvc@1.68.0': optional: true + '@oxlint/binding-win32-ia32-msvc@1.66.0': + optional: true + '@oxlint/binding-win32-ia32-msvc@1.68.0': optional: true + '@oxlint/binding-win32-x64-msvc@1.66.0': + optional: true + '@oxlint/binding-win32-x64-msvc@1.68.0': optional: true @@ -27300,7 +27412,7 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 - deslop-js@0.0.14: + deslop-js@0.0.24: dependencies: '@oxc-project/types': 0.132.0 fast-glob: 3.3.3 @@ -27492,19 +27604,6 @@ snapshots: ee-first@1.1.1: {} - effect@4.0.0-beta.70: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 4.8.0 - find-my-way-ts: 0.1.6 - ini: 7.0.0 - kubernetes-types: 1.30.0 - msgpackr: 2.0.2 - multipasta: 0.2.7 - toml: 4.1.1 - uuid: 14.0.0 - yaml: 2.9.0 - ejs@3.1.10: dependencies: jake: 10.9.4 @@ -28072,10 +28171,6 @@ snapshots: eyes@0.1.8: {} - fast-check@4.8.0: - dependencies: - pure-rand: 8.4.0 - fast-copy@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -28196,8 +28291,6 @@ snapshots: transitivePeerDependencies: - supports-color - find-my-way-ts@0.1.6: {} - find-root@1.1.0: {} find-up@4.1.0: @@ -28785,8 +28878,6 @@ snapshots: ini@6.0.0: {} - ini@7.0.0: {} - inline-style-parser@0.1.1: {} invariant@2.2.4: @@ -29202,8 +29293,6 @@ snapshots: knitwork@1.3.0: {} - kubernetes-types@1.30.0: {} - launch-editor@2.14.1: dependencies: picocolors: 1.1.1 @@ -30209,28 +30298,10 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.4: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 - optional: true - - msgpackr@2.0.2: - optionalDependencies: - msgpackr-extract: 3.0.4 - muggle-string@0.4.1: {} multiformats@9.9.0: {} - multipasta@0.2.7: {} - nanoclone@0.2.1: {} nanoid@3.3.12: {} @@ -30417,11 +30488,6 @@ snapshots: node-forge@1.4.0: {} - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - node-gyp-build@4.8.4: {} node-int64@0.4.0: {} @@ -31000,13 +31066,35 @@ snapshots: oxc-parser: 0.131.0 rolldown: 1.0.3 - oxlint-plugin-react-doctor@0.2.16: + oxlint-plugin-react-doctor@0.5.5: dependencies: '@typescript-eslint/types': 8.60.1 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 oxc-parser: 0.132.0 + oxlint@1.66.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.66.0 + '@oxlint/binding-android-arm64': 1.66.0 + '@oxlint/binding-darwin-arm64': 1.66.0 + '@oxlint/binding-darwin-x64': 1.66.0 + '@oxlint/binding-freebsd-x64': 1.66.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 + '@oxlint/binding-linux-arm-musleabihf': 1.66.0 + '@oxlint/binding-linux-arm64-gnu': 1.66.0 + '@oxlint/binding-linux-arm64-musl': 1.66.0 + '@oxlint/binding-linux-ppc64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-musl': 1.66.0 + '@oxlint/binding-linux-s390x-gnu': 1.66.0 + '@oxlint/binding-linux-x64-gnu': 1.66.0 + '@oxlint/binding-linux-x64-musl': 1.66.0 + '@oxlint/binding-openharmony-arm64': 1.66.0 + '@oxlint/binding-win32-arm64-msvc': 1.66.0 + '@oxlint/binding-win32-ia32-msvc': 1.66.0 + '@oxlint/binding-win32-x64-msvc': 1.66.0 + oxlint@1.68.0: optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.68.0 @@ -31028,6 +31116,7 @@ snapshots: '@oxlint/binding-win32-arm64-msvc': 1.68.0 '@oxlint/binding-win32-ia32-msvc': 1.68.0 '@oxlint/binding-win32-x64-msvc': 1.68.0 + optional: true p-cancelable@2.1.1: {} @@ -31704,8 +31793,6 @@ snapshots: punycode@2.3.1: {} - pure-rand@8.4.0: {} - qr-code-styling@1.9.2: dependencies: qrcode-generator: 1.5.2 @@ -31830,31 +31917,29 @@ snapshots: - bufferutil - utf-8-validate - react-doctor@0.2.16(bufferutil@4.1.0)(eslint@10.4.1(jiti@2.7.0))(utf-8-validate@6.0.6): + react-doctor@0.5.5(eslint@10.4.1(jiti@2.7.0)): dependencies: '@babel/code-frame': 7.29.7 - '@effect/platform-node-shared': 4.0.0-beta.70(bufferutil@4.1.0)(effect@4.0.0-beta.70)(utf-8-validate@6.0.6) '@sentry/node': 10.55.0 agent-install: 0.0.5 conf: 15.1.0 confbox: 0.2.4 - deslop-js: 0.0.14 - effect: 4.0.0-beta.70 + deslop-js: 0.0.24 eslint-plugin-react-hooks: 7.1.1(eslint@10.4.1(jiti@2.7.0)) jiti: 2.7.0 magicast: 0.5.3 - oxlint: 1.68.0 - oxlint-plugin-react-doctor: 0.2.16 + oxlint: 1.66.0 + oxlint-plugin-react-doctor: 0.5.5 prompts: 2.4.2 typescript: 6.0.3 + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 transitivePeerDependencies: - '@opentelemetry/exporter-trace-otlp-http' - - bufferutil - eslint - oxlint-tsgolint - supports-color - - utf-8-validate - - vite-plus react-dom@19.2.7(react@19.2.7): dependencies: @@ -32012,7 +32097,7 @@ snapshots: optionalDependencies: react-dom: 19.2.7(react@19.2.7) - react-scan@0.5.7(bufferutil@4.1.0)(esbuild@0.28.0)(eslint@10.4.1(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.0)(utf-8-validate@6.0.6): + react-scan@0.5.7(esbuild@0.28.0)(eslint@10.4.1(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.0): dependencies: '@babel/core': 7.29.7 '@babel/types': 7.29.7 @@ -32024,7 +32109,7 @@ snapshots: preact: 10.29.2 prompts: 2.4.2 react: 19.2.7 - react-doctor: 0.2.16(bufferutil@4.1.0)(eslint@10.4.1(jiti@2.7.0))(utf-8-validate@6.0.6) + react-doctor: 0.5.5(eslint@10.4.1(jiti@2.7.0)) react-dom: 19.2.7(react@19.2.7) react-grab: 0.1.44(react@19.2.7) optionalDependencies: @@ -32032,13 +32117,10 @@ snapshots: unplugin: 3.0.0 transitivePeerDependencies: - '@opentelemetry/exporter-trace-otlp-http' - - bufferutil - eslint - oxlint-tsgolint - rollup - supports-color - - utf-8-validate - - vite-plus react-stately@3.47.0(react@19.2.7): dependencies: @@ -33177,8 +33259,6 @@ snapshots: toml@3.0.0: {} - toml@4.1.1: {} - toposort@2.0.2: {} totalist@3.0.1: {} @@ -33946,6 +34026,21 @@ snapshots: void-elements@3.1.0: {} + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + vscode-uri@3.1.0: {} vue-bundle-renderer@2.2.0: