From 7a40640ddcb6a96a859279f3daef3c6633ebbda4 Mon Sep 17 00:00:00 2001 From: Olorunfemi20 Date: Mon, 27 Jul 2026 20:30:48 +0100 Subject: [PATCH 1/2] feat(mobile): register veil:// scheme and universal/app links Register the custom scheme and platform universal/app links so payment requests and other external entry points open the right screen from both a cold start and a warm resume. - app.config.ts replaces app.json so the deep-linking surface is derived from the same constants the runtime resolver uses. Declares the veil:// and web+stellar: schemes, iOS associated domains, an autoVerify Android intent filter, and the bundle identifier / package name both need. - lib/deepLinks.ts resolves an inbound URL to an in-app route. Inbound links are untrusted, so it matches a fixed allowlist of routes and copies only the query parameters each route declares; foreign hosts, unknown schemes, and over-long URLs fall back to the home route rather than navigating. Query parsing is hand-rolled because React Native's URLSearchParams accessors throw. - app/+native-intent.ts wires it into expo-router, which calls it for both launch paths, and can never throw during launch. - Placeholder /pay, /send, /receive, and /create-wallet routes give the links somewhere to land; /pay forwards a request into the send form and preserves the raw SEP-7 URI for the handler in backlog #38. - apple-app-site-association and assetlinks.json are served by the wallet web app, with next.config.js pinning their Content-Type. Both carry placeholders documented in the mobile README. --- .github/workflows/ci.yml | 17 + frontend/mobile/README.md | 84 +- frontend/mobile/app.config.ts | 98 + frontend/mobile/app.json | 42 - frontend/mobile/app/+native-intent.ts | 25 + frontend/mobile/app/create-wallet.tsx | 99 + frontend/mobile/app/index.tsx | 33 +- frontend/mobile/app/pay.tsx | 29 + frontend/mobile/app/receive.tsx | 65 + frontend/mobile/app/send.tsx | 96 + .../mobile/lib/__tests__/appConfig.test.ts | 96 + .../mobile/lib/__tests__/deepLinks.test.ts | 150 ++ frontend/mobile/lib/deepLinks.ts | 241 +++ frontend/mobile/lib/theme.ts | 9 + frontend/mobile/package-lock.json | 1728 +++++++++++++++-- frontend/mobile/package.json | 6 +- frontend/wallet/next.config.js | 16 + .../.well-known/apple-app-site-association | 19 + .../wallet/public/.well-known/assetlinks.json | 10 + 19 files changed, 2654 insertions(+), 209 deletions(-) create mode 100644 frontend/mobile/app.config.ts delete mode 100644 frontend/mobile/app.json create mode 100644 frontend/mobile/app/+native-intent.ts create mode 100644 frontend/mobile/app/create-wallet.tsx create mode 100644 frontend/mobile/app/pay.tsx create mode 100644 frontend/mobile/app/receive.tsx create mode 100644 frontend/mobile/app/send.tsx create mode 100644 frontend/mobile/lib/__tests__/appConfig.test.ts create mode 100644 frontend/mobile/lib/__tests__/deepLinks.test.ts create mode 100644 frontend/mobile/lib/deepLinks.ts create mode 100644 frontend/mobile/lib/theme.ts create mode 100644 frontend/wallet/public/.well-known/apple-app-site-association create mode 100644 frontend/wallet/public/.well-known/assetlinks.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67afa997..23dd5a63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,6 +190,23 @@ jobs: - run: npx tsc --noEmit - run: npm run build + mobile: + name: Mobile — typecheck & test + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend/mobile + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: frontend/mobile/package-lock.json + - run: npm ci || npm install + - run: npm run typecheck + - run: npm test + lighthouse-ci: name: Wallet PWA — Lighthouse performance budget runs-on: ubuntu-latest diff --git a/frontend/mobile/README.md b/frontend/mobile/README.md index 5f90808a..d889e436 100644 --- a/frontend/mobile/README.md +++ b/frontend/mobile/README.md @@ -1,8 +1,8 @@ # Veil Mobile -Expo (expo-router + TypeScript) mobile app for Veil. This is the bootable shell — -a single placeholder home route (`app/index.tsx`) proving the toolchain runs. No -wallet logic or SDK is wired up yet. +Expo (expo-router + TypeScript) mobile app for Veil. The screens are still +placeholders — no wallet SDK is wired up yet — but the routes exist so deep +links have somewhere to land. ## Getting started @@ -14,10 +14,86 @@ npx expo start Then press `i` for the iOS simulator, `a` for the Android emulator, or `w` for web. +```bash +npm run typecheck # tsc --noEmit +npm test # vitest run +``` + ## Structure - `app/_layout.tsx` — root Stack navigator (expo-router). -- `app/index.tsx` — placeholder home route. +- `app/index.tsx` — home route. +- `app/create-wallet.tsx`, `app/send.tsx`, `app/receive.tsx` — placeholder screens. +- `app/pay.tsx` — payment-request entry point; forwards into the send flow. +- `app/+native-intent.ts` — expo-router hook that normalises every inbound deep link. +- `lib/deepLinks.ts` — the pure resolver those two share. +- `app.config.ts` — Expo config, including the deep-linking surface. Native `ios/` and `android/` folders are generated on demand (via prebuild/EAS) and are gitignored, along with `node_modules/` and `.expo/`. + +## Deep linking + +Three URL families open the app, and all three resolve to the same in-app routes: + +| Incoming URL | Resolves to | +| --- | --- | +| `veil://pay?to=G…&amount=10` | `/pay` → `/send`, prefilled | +| `https://app.veil.xyz/receive` | `/receive` | +| `web+stellar:pay?destination=G…&amount=10` | `/pay`, raw URI preserved as `uri` | +| anything else | `/` | + +`app/+native-intent.ts` is called by expo-router for every inbound link, on a +cold start (`initial: true`) and on a warm resume (`initial: false`) alike, which +is what makes the two behave identically. It delegates to `resolveDeepLink()` in +`lib/deepLinks.ts`. + +Inbound links are untrusted — any app, web page, or QR code can send one — so the +resolver matches a fixed allowlist of routes and copies only the query parameters +each route declares. Foreign hosts, unknown schemes, unknown paths, and +over-long URLs all fall back to `/` instead of navigating. + +Full SEP-7 validation (address checksums, amount ranges, hostile callbacks) is +the job of the handler in backlog #38. `sdk/src/sep7.ts` already implements it +for the web wallet; the raw URI is forwarded to `/pay` as `uri` so that handler +can parse the original request unmodified. + +### Testing links locally + +The schemes are only registered in a dev-client or standalone build — deep links +do not reach the app through Expo Go. + +```bash +# Android (emulator or device) +adb shell am start -W -a android.intent.action.VIEW \ + -d "veil://pay?to=GABC&amount=10" xyz.veil.wallet +adb shell am start -W -a android.intent.action.VIEW \ + -d "https://app.veil.xyz/receive" xyz.veil.wallet + +# iOS simulator +xcrun simctl openurl booted "veil://pay?to=GABC&amount=10" +xcrun simctl openurl booted "https://app.veil.xyz/receive" +``` + +Run each command twice: once with the app force-quit (cold start) and once with +it backgrounded (warm resume). Both must land on the same screen. + +### Universal / app link setup + +Two verification files are served by the wallet web app from +`frontend/wallet/public/.well-known/`. Both currently carry placeholders that +must be replaced before a store build, or the platforms will silently keep +opening links in the browser: + +- `apple-app-site-association` — replace `APPLE_TEAM_ID` with the Apple Developer + Team ID that signs `xyz.veil.wallet`. The file must be served over HTTPS as + `application/json`, with no redirect and no `.json` extension. +- `assetlinks.json` — replace `ANDROID_RELEASE_CERT_SHA256_FINGERPRINT` with the + SHA-256 fingerprint of the release signing certificate + (`keytool -list -v -keystore -alias `). Add the Play App + Signing fingerprint too if the app is distributed through Google Play. + +`frontend/wallet/next.config.js` pins the `Content-Type` on both files. After +deploying, verify with Apple's CDN +(`https://app-site-association.cdn-apple.com/a/v1/app.veil.xyz`) and Google's +[Digital Asset Links API](https://developers.google.com/digital-asset-links/tools/generator). diff --git a/frontend/mobile/app.config.ts b/frontend/mobile/app.config.ts new file mode 100644 index 00000000..b9adef09 --- /dev/null +++ b/frontend/mobile/app.config.ts @@ -0,0 +1,98 @@ +import type { ExpoConfig } from 'expo/config'; + +/** + * Expo config, replacing `app.json` so the deep-linking surface can carry the + * reasoning behind it. + * + * The values below are duplicated from `lib/deepLinks.ts` rather than imported: + * Expo transpiles this file on its own and then `require`s it, so a relative + * import of a sibling TypeScript module fails to resolve at config-load time. + * `lib/__tests__/appConfig.test.ts` asserts the two stay in agreement — the + * config and the resolver drifting apart is exactly how deep links break + * silently. + * + * Universal links additionally require the two verification files served by the + * wallet web app from `frontend/wallet/public/.well-known/`; see + * `frontend/mobile/README.md` for the values to fill in before a store build. + */ + +/** Custom URL scheme registered by the app. Mirrors `DEEP_LINK_SCHEME`. */ +const DEEP_LINK_SCHEME = 'veil'; + +/** Hosts claimed as universal / app links. Mirrors `ASSOCIATED_DOMAINS`. */ +const ASSOCIATED_DOMAINS = ['app.veil.xyz']; + +/** SEP-7 URI scheme, without the trailing colon. Mirrors `SEP7_SCHEME`. */ +const SEP7_SCHEME = 'web+stellar'; + +const BUNDLE_IDENTIFIER = 'xyz.veil.wallet'; + +/** + * Paths claimed as universal / app links. Kept narrow on purpose: every path + * listed here stops opening in the browser once the app is installed, so the + * marketing site and docs must keep working as web pages. + */ +const LINKED_PATHS = ['pay', 'send', 'receive', 'create-wallet']; + +const config: ExpoConfig = { + name: 'Veil', + slug: 'veil-mobile', + version: '0.1.0', + orientation: 'portrait', + icon: './assets/images/icon.png', + // `veil://` is the app's own scheme; `web+stellar:` is claimed so SEP-7 + // payment requests (backlog #38) open here too. Expo turns both into + // CFBundleURLTypes on iOS and BROWSABLE intent filters on Android at prebuild. + scheme: [DEEP_LINK_SCHEME, SEP7_SCHEME], + userInterfaceStyle: 'automatic', + ios: { + icon: './assets/expo.icon', + bundleIdentifier: BUNDLE_IDENTIFIER, + associatedDomains: ASSOCIATED_DOMAINS.map((domain) => `applinks:${domain}`), + }, + android: { + package: BUNDLE_IDENTIFIER, + adaptiveIcon: { + backgroundColor: '#E6F4FE', + foregroundImage: './assets/images/android-icon-foreground.png', + backgroundImage: './assets/images/android-icon-background.png', + monochromeImage: './assets/images/android-icon-monochrome.png', + }, + predictiveBackGestureEnabled: false, + intentFilters: [ + // Verified app links. `autoVerify` makes Android fetch + // https://app.veil.xyz/.well-known/assetlinks.json at install time; if the + // fingerprint there does not match the installed build, links keep opening + // in the browser rather than failing outright. + { + action: 'VIEW', + autoVerify: true, + category: ['BROWSABLE', 'DEFAULT'], + data: ASSOCIATED_DOMAINS.flatMap((host) => + LINKED_PATHS.map((path) => ({ scheme: 'https', host, pathPrefix: `/${path}` })), + ), + }, + ], + }, + web: { + output: 'static', + favicon: './assets/images/favicon.png', + }, + plugins: [ + 'expo-router', + [ + 'expo-splash-screen', + { + backgroundColor: '#208AEF', + image: './assets/images/splash-icon.png', + imageWidth: 76, + }, + ], + ], + experiments: { + typedRoutes: true, + reactCompiler: true, + }, +}; + +export default config; diff --git a/frontend/mobile/app.json b/frontend/mobile/app.json deleted file mode 100644 index 415223e2..00000000 --- a/frontend/mobile/app.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "expo": { - "name": "Veil", - "slug": "veil-mobile", - "version": "0.1.0", - "orientation": "portrait", - "icon": "./assets/images/icon.png", - "scheme": "veil", - "userInterfaceStyle": "automatic", - "ios": { - "icon": "./assets/expo.icon" - }, - "android": { - "adaptiveIcon": { - "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/images/android-icon-foreground.png", - "backgroundImage": "./assets/images/android-icon-background.png", - "monochromeImage": "./assets/images/android-icon-monochrome.png" - }, - "predictiveBackGestureEnabled": false - }, - "web": { - "output": "static", - "favicon": "./assets/images/favicon.png" - }, - "plugins": [ - "expo-router", - [ - "expo-splash-screen", - { - "backgroundColor": "#208AEF", - "image": "./assets/images/splash-icon.png", - "imageWidth": 76 - } - ] - ], - "experiments": { - "typedRoutes": true, - "reactCompiler": true - } - } -} diff --git a/frontend/mobile/app/+native-intent.ts b/frontend/mobile/app/+native-intent.ts new file mode 100644 index 00000000..05ddcf6e --- /dev/null +++ b/frontend/mobile/app/+native-intent.ts @@ -0,0 +1,25 @@ +import { FALLBACK_ROUTE, resolveDeepLink } from '../lib/deepLinks'; + +/** + * expo-router calls this for every inbound deep link before it builds any + * navigation state — on a cold start (`initial: true`) and on a warm resume + * (`initial: false`) alike. Returning a normalised in-app path here is what + * makes both paths land on the same screen. + * + * It runs during launch, so it must never throw and must never block: + * {@link resolveDeepLink} is pure, synchronous, and falls back to the home + * route instead of raising. + */ +export function redirectSystemPath({ + path, + initial, +}: { + path: string; + initial: boolean; +}): string { + try { + return resolveDeepLink(path, { initial }); + } catch { + return FALLBACK_ROUTE; + } +} diff --git a/frontend/mobile/app/create-wallet.tsx b/frontend/mobile/app/create-wallet.tsx new file mode 100644 index 00000000..42365bf8 --- /dev/null +++ b/frontend/mobile/app/create-wallet.tsx @@ -0,0 +1,99 @@ +import { useRouter } from "expo-router"; +import { useState } from "react"; +import { Pressable, StyleSheet, Text, View } from "react-native"; + +import { colors } from "../lib/theme"; + +/** + * Placeholder wallet creation screen. The real passkey registration and + * factory deploy land with the SDK wiring; this route exists so onboarding + * links (`veil://create-wallet`) and the home screen have a destination. + */ +export default function CreateWallet() { + const router = useRouter(); + const [status, setStatus] = useState<"idle" | "creating" | "created">("idle"); + + const handleCreate = () => { + setStatus("creating"); + // Stands in for passkey registration + on-chain deploy. + setTimeout(() => setStatus("created"), 400); + }; + + return ( + + Create your wallet + + Your device biometrics guard the wallet. No seed phrase to write down. + + + {status === "created" ? ( + <> + + Wallet created + + router.replace("/")} + style={styles.button} + > + Continue + + + ) : ( + + + {status === "creating" ? "Waiting for biometric…" : "Create wallet"} + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.background, + gap: 16, + padding: 24, + }, + title: { + color: colors.text, + fontSize: 24, + fontWeight: "700", + }, + caption: { + color: colors.muted, + fontSize: 14, + textAlign: "center", + }, + success: { + color: colors.text, + fontSize: 18, + fontWeight: "600", + }, + button: { + backgroundColor: colors.accent, + borderRadius: 12, + marginTop: 8, + paddingHorizontal: 28, + paddingVertical: 14, + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: colors.text, + fontSize: 16, + fontWeight: "600", + }, +}); diff --git a/frontend/mobile/app/index.tsx b/frontend/mobile/app/index.tsx index cc7f3029..61b7b5be 100644 --- a/frontend/mobile/app/index.tsx +++ b/frontend/mobile/app/index.tsx @@ -1,10 +1,25 @@ +import { Link } from "expo-router"; import { StyleSheet, Text, View } from "react-native"; +import { colors } from "../lib/theme"; + export default function Home() { return ( - + Veil Mobile Placeholder home route — toolchain is live. + + + + Create wallet + + + Send + + + Receive + + ); } @@ -14,18 +29,28 @@ const styles = StyleSheet.create({ flex: 1, alignItems: "center", justifyContent: "center", - backgroundColor: "#0B0B0F", + backgroundColor: colors.background, padding: 24, }, title: { - color: "#FFFFFF", + color: colors.text, fontSize: 28, fontWeight: "700", }, subtitle: { - color: "#9BA1A6", + color: colors.muted, fontSize: 15, marginTop: 8, textAlign: "center", }, + nav: { + marginTop: 32, + gap: 16, + alignItems: "center", + }, + link: { + color: colors.accent, + fontSize: 17, + fontWeight: "600", + }, }); diff --git a/frontend/mobile/app/pay.tsx b/frontend/mobile/app/pay.tsx new file mode 100644 index 00000000..eabcc644 --- /dev/null +++ b/frontend/mobile/app/pay.tsx @@ -0,0 +1,29 @@ +import { Redirect, useLocalSearchParams } from "expo-router"; + +/** + * Entry point for inbound payment requests: `veil://pay?…`, + * `https://app.veil.xyz/pay?…`, and SEP-7 `web+stellar:pay?…` URIs, all of + * which `lib/deepLinks.ts` normalises onto this route. + * + * It deliberately holds no UI of its own — it hands the request to the send + * screen so the form arrives prefilled. Full SEP-7 validation of the original + * URI (address checksums, amount ranges, callback safety) is the job of the + * handler in backlog #38, which will read the raw `uri` parameter forwarded + * here and can then replace this redirect with a confirmation step. + */ +export default function Pay() { + const params = useLocalSearchParams<{ + to?: string; + amount?: string; + asset?: string; + memo?: string; + }>(); + + const forwarded: Record = {}; + for (const key of ["to", "amount", "asset", "memo"] as const) { + const value = params[key]; + if (typeof value === "string" && value) forwarded[key] = value; + } + + return ; +} diff --git a/frontend/mobile/app/receive.tsx b/frontend/mobile/app/receive.tsx new file mode 100644 index 00000000..f04b2a0a --- /dev/null +++ b/frontend/mobile/app/receive.tsx @@ -0,0 +1,65 @@ +import { useLocalSearchParams } from "expo-router"; +import { StyleSheet, Text, View } from "react-native"; + +import { colors } from "../lib/theme"; + +/** + * Placeholder receive screen. QR rendering and the real wallet address land + * with the SDK wiring; this route exists so `veil://receive` and + * `https://app.veil.xyz/receive` have somewhere to go. + */ +const PLACEHOLDER_ADDRESS = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; + +export default function Receive() { + const { amount, asset } = useLocalSearchParams<{ amount?: string; asset?: string }>(); + const requested = typeof amount === "string" ? amount : undefined; + + return ( + + Receive + Share this address to get paid. + + + {PLACEHOLDER_ADDRESS} + + + {requested ? ( + + Requesting {requested} {typeof asset === "string" && asset ? asset : "XLM"} + + ) : null} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.background, + gap: 16, + padding: 24, + }, + title: { + color: colors.text, + fontSize: 24, + fontWeight: "700", + }, + caption: { + color: colors.muted, + fontSize: 14, + textAlign: "center", + }, + address: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderWidth: 1, + borderRadius: 10, + color: colors.text, + fontFamily: "monospace", + fontSize: 13, + padding: 14, + textAlign: "center", + }, +}); diff --git a/frontend/mobile/app/send.tsx b/frontend/mobile/app/send.tsx new file mode 100644 index 00000000..1162f53e --- /dev/null +++ b/frontend/mobile/app/send.tsx @@ -0,0 +1,96 @@ +import { useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { StyleSheet, Text, TextInput, View } from "react-native"; + +import { colors } from "../lib/theme"; + +/** + * Placeholder send screen. Signing and submission land with the wallet SDK + * wiring; what matters here is that this route owns the prefill contract deep + * links depend on — `to`, `amount`, `asset`, and `memo` arrive as query + * parameters from `veil://send`, `https://app.veil.xyz/send`, or a SEP-7 + * request forwarded by `/pay`. + */ +export default function Send() { + const params = useLocalSearchParams<{ + to?: string; + amount?: string; + asset?: string; + memo?: string; + }>(); + + const [recipient, setRecipient] = useState(firstValue(params.to)); + const [amount, setAmount] = useState(firstValue(params.amount)); + const asset = firstValue(params.asset) || "XLM"; + const memo = firstValue(params.memo); + + return ( + + Send + + + + + + {memo ? ( + + Memo: {memo} + + ) : null} + + ); +} + +/** expo-router yields `string | string[]` for a repeated query key. */ +function firstValue(value: string | string[] | undefined): string { + if (Array.isArray(value)) return value[0] ?? ""; + return value ?? ""; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + backgroundColor: colors.background, + gap: 16, + padding: 24, + }, + title: { + color: colors.text, + fontSize: 24, + fontWeight: "700", + textAlign: "center", + }, + input: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderWidth: 1, + borderRadius: 10, + color: colors.text, + fontSize: 16, + padding: 14, + }, + memo: { + color: colors.muted, + fontSize: 14, + }, +}); diff --git a/frontend/mobile/lib/__tests__/appConfig.test.ts b/frontend/mobile/lib/__tests__/appConfig.test.ts new file mode 100644 index 00000000..b1c458a9 --- /dev/null +++ b/frontend/mobile/lib/__tests__/appConfig.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; + +import config from '../../app.config'; +import { + ASSOCIATED_DOMAINS, + DEEP_LINK_SCHEME, + FALLBACK_ROUTE, + SEP7_SCHEME, + resolveDeepLink, +} from '../deepLinks'; + +/** + * `app.config.ts` cannot import from `lib/deepLinks.ts` — Expo transpiles the + * config file alone and then requires it, so a relative TypeScript import fails + * to resolve at config-load time. These tests are what keeps the duplicated + * constants honest: a scheme registered natively but unhandled at runtime (or + * the reverse) is a deep link that silently does nothing. + */ + +const schemes = Array.isArray(config.scheme) + ? config.scheme + : config.scheme + ? [config.scheme] + : []; + +const appLinkFilter = (config.android?.intentFilters ?? []).find((filter) => filter.autoVerify); + +// Expo types `data` as one entry or an array of them; normalise to an array. +const rawData = appLinkFilter?.data; +const appLinkData = rawData ? (Array.isArray(rawData) ? rawData : [rawData]) : []; + +describe('app.config.ts — schemes', () => { + it('registers the scheme the resolver handles', () => { + expect(schemes).toContain(DEEP_LINK_SCHEME); + }); + + it('registers the SEP-7 scheme', () => { + expect(schemes).toContain(SEP7_SCHEME); + }); + + it('registers nothing the resolver would ignore', () => { + for (const scheme of schemes) { + expect(resolveDeepLink(`${scheme}:pay?destination=G`)).not.toBe(FALLBACK_ROUTE); + } + }); +}); + +describe('app.config.ts — associated domains', () => { + it('declares an applinks entry per associated domain', () => { + expect(config.ios?.associatedDomains).toEqual( + ASSOCIATED_DOMAINS.map((domain) => `applinks:${domain}`), + ); + }); + + it('verifies the same hosts on Android', () => { + const hosts = appLinkData.map((entry) => entry.host); + expect(new Set(hosts)).toEqual(new Set(ASSOCIATED_DOMAINS)); + }); + + it('claims https links only, and marks them for verification', () => { + expect(appLinkFilter?.autoVerify).toBe(true); + for (const entry of appLinkData) { + expect(entry.scheme).toBe('https'); + } + }); + + it('requires BROWSABLE so links from a browser reach the app', () => { + expect(appLinkFilter?.category).toContain('BROWSABLE'); + }); +}); + +describe('app.config.ts — claimed paths', () => { + it('claims only paths the resolver routes', () => { + for (const entry of appLinkData) { + const url = `https://${entry.host}${entry.pathPrefix}`; + expect(resolveDeepLink(url), `${url} is claimed natively but not routed`).not.toBe( + FALLBACK_ROUTE, + ); + } + }); + + it('claims the payment-request path, which is the point of the exercise', () => { + const prefixes = appLinkData.map((entry) => entry.pathPrefix); + expect(prefixes).toContain('/pay'); + }); +}); + +describe('app.config.ts — identifiers', () => { + it('uses the same identifier on both platforms', () => { + expect(config.ios?.bundleIdentifier).toBe(config.android?.package); + }); + + it('sets an identifier at all, without which neither link type can be claimed', () => { + expect(config.ios?.bundleIdentifier).toBeTruthy(); + }); +}); diff --git a/frontend/mobile/lib/__tests__/deepLinks.test.ts b/frontend/mobile/lib/__tests__/deepLinks.test.ts new file mode 100644 index 00000000..9432d3d2 --- /dev/null +++ b/frontend/mobile/lib/__tests__/deepLinks.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; + +import { FALLBACK_ROUTE, MAX_DEEP_LINK_LENGTH, resolveDeepLink } from '../deepLinks'; + +const DESTINATION = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF'; + +describe('resolveDeepLink — veil:// custom scheme', () => { + it('routes a bare screen link', () => { + expect(resolveDeepLink('veil://receive')).toBe('/receive'); + }); + + it('forwards known query parameters', () => { + expect(resolveDeepLink(`veil://pay?to=${DESTINATION}&amount=10`)).toBe( + `/pay?to=${DESTINATION}&amount=10`, + ); + }); + + it('accepts the host-less form', () => { + expect(resolveDeepLink('veil:send')).toBe('/send'); + }); + + it('tolerates a trailing slash and mixed case', () => { + expect(resolveDeepLink('veil://Receive/')).toBe('/receive'); + }); + + it('drops the fragment', () => { + expect(resolveDeepLink('veil://send?amount=5#top')).toBe('/send?amount=5'); + }); + + it('sends an unknown screen to the fallback route', () => { + expect(resolveDeepLink('veil://settings/admin')).toBe(FALLBACK_ROUTE); + }); + + it('sends the scheme root to the fallback route', () => { + expect(resolveDeepLink('veil://')).toBe(FALLBACK_ROUTE); + }); +}); + +describe('resolveDeepLink — universal / app links', () => { + it('routes an associated-domain link', () => { + expect(resolveDeepLink(`https://app.veil.xyz/pay?to=${DESTINATION}`)).toBe( + `/pay?to=${DESTINATION}`, + ); + }); + + it('ignores the port', () => { + expect(resolveDeepLink('https://app.veil.xyz:443/receive')).toBe('/receive'); + }); + + it('matches the host case-insensitively', () => { + expect(resolveDeepLink('https://APP.VEIL.XYZ/send')).toBe('/send'); + }); + + it('rejects a foreign host', () => { + expect(resolveDeepLink('https://evil.example/pay?to=ATTACKER')).toBe(FALLBACK_ROUTE); + }); + + it('rejects a look-alike subdomain', () => { + expect(resolveDeepLink('https://app.veil.xyz.evil.example/pay')).toBe(FALLBACK_ROUTE); + }); + + it('rejects userinfo smuggling', () => { + expect(resolveDeepLink('https://app.veil.xyz@evil.example/pay')).toBe(FALLBACK_ROUTE); + }); +}); + +describe('resolveDeepLink — SEP-7 payment requests', () => { + it('maps SEP-7 fields onto the pay route and keeps the raw URI', () => { + const uri = `web+stellar:pay?destination=${DESTINATION}&amount=12.5&asset_code=USDC`; + const target = resolveDeepLink(uri); + + expect(target.startsWith('/pay?')).toBe(true); + + const query = new URLSearchParams(target.slice(target.indexOf('?') + 1)); + expect(query.get('to')).toBe(DESTINATION); + expect(query.get('amount')).toBe('12.5'); + expect(query.get('asset')).toBe('USDC'); + expect(query.get('uri')).toBe(uri); + }); + + it('forwards the raw URI even when no fields map', () => { + expect(resolveDeepLink('web+stellar:pay')).toBe( + `/pay?uri=${encodeURIComponent('web+stellar:pay')}`, + ); + }); + + it('rejects a non-pay SEP-7 operation', () => { + expect(resolveDeepLink('web+stellar:tx?xdr=AAAA')).toBe(FALLBACK_ROUTE); + }); +}); + +describe('resolveDeepLink — paths handed over by expo-router', () => { + it('accepts an already-stripped path', () => { + expect(resolveDeepLink('/send?amount=3')).toBe('/send?amount=3'); + }); + + it('resolves an alias', () => { + expect(resolveDeepLink('/request?amount=3')).toBe('/receive?amount=3'); + }); +}); + +describe('resolveDeepLink — hostile and malformed input', () => { + it.each([ + ['empty string', ''], + ['whitespace', ' '], + ['no scheme', 'pay?to=GABC'], + ['unknown scheme', 'javascript:alert(1)'], + ['file scheme', 'file:///etc/passwd'], + ])('sends %s to the fallback route', (_label, input) => { + expect(resolveDeepLink(input)).toBe(FALLBACK_ROUTE); + }); + + it('rejects an over-long URL without parsing it', () => { + const oversized = `veil://pay?to=${'G'.repeat(MAX_DEEP_LINK_LENGTH)}`; + expect(resolveDeepLink(oversized)).toBe(FALLBACK_ROUTE); + }); + + it('drops parameters the target route does not accept', () => { + expect(resolveDeepLink('veil://receive?amount=5&callback=https://evil.example')).toBe( + '/receive?amount=5', + ); + }); + + it('keeps the first value of a repeated parameter', () => { + expect(resolveDeepLink('veil://send?amount=1&amount=999')).toBe('/send?amount=1'); + }); + + it('survives a malformed percent-escape', () => { + expect(resolveDeepLink('veil://%E0%A4%A')).toBe(FALLBACK_ROUTE); + }); + + it('ignores a non-string input', () => { + expect(resolveDeepLink(undefined as unknown as string)).toBe(FALLBACK_ROUTE); + }); +}); + +describe('resolveDeepLink — cold start vs warm resume', () => { + const links = [ + 'veil://pay?to=' + DESTINATION, + 'https://app.veil.xyz/receive', + 'web+stellar:pay?destination=' + DESTINATION, + 'veil://create-wallet', + ]; + + it.each(links)('resolves %s identically on both launch paths', (link) => { + expect(resolveDeepLink(link, { initial: true })).toBe( + resolveDeepLink(link, { initial: false }), + ); + }); +}); diff --git a/frontend/mobile/lib/deepLinks.ts b/frontend/mobile/lib/deepLinks.ts new file mode 100644 index 00000000..b01496a3 --- /dev/null +++ b/frontend/mobile/lib/deepLinks.ts @@ -0,0 +1,241 @@ +/** + * Inbound deep-link resolution for the Veil mobile app. + * + * Three families of URL reach the app, and all three must land on the same + * in-app routes whether the app was cold-started by the link or was already + * running in the background (warm resume): + * + * 1. `veil://pay?...` custom scheme (app.config.ts `scheme`) + * 2. `https://app.veil.xyz/pay?...` iOS universal link / Android app link + * 3. `web+stellar:pay?destination=...` SEP-7 payment request + * + * Everything here is pure string handling with no React Native or Expo imports, + * so it can be unit-tested directly and reused by the SEP-7 handler in + * backlog #38. + * + * Inbound links are untrusted input: any app, web page, or QR code can send one. + * {@link resolveDeepLink} therefore never echoes an arbitrary path back to the + * router — it matches against a fixed allowlist of routes and copies only known + * query parameters, falling back to the home route for anything unrecognised. + */ + +/** Custom URL scheme registered by the app (kept in sync with `app.config.ts`). */ +export const DEEP_LINK_SCHEME = 'veil'; + +/** Hosts whose `https://` links are claimed as universal / app links. */ +export const ASSOCIATED_DOMAINS = ['app.veil.xyz'] as const; + +/** SEP-7 URI scheme, without the trailing colon. */ +export const SEP7_SCHEME = 'web+stellar'; + +/** Route used whenever a link is missing, malformed, or unrecognised. */ +export const FALLBACK_ROUTE = '/'; + +/** + * Hard cap on the length of an inbound URL. SEP-7 caps its URIs at 7168 chars; + * anything past that is not a real request and is rejected without parsing. + */ +export const MAX_DEEP_LINK_LENGTH = 7168; + +/** + * Routes reachable from outside the app, and the query parameters each one + * accepts. Parameters outside this list are dropped rather than forwarded, so a + * crafted link cannot smuggle state into a screen that never expected it. + */ +const LINKABLE_ROUTES: Record = { + '/pay': ['to', 'amount', 'asset', 'memo', 'msg', 'uri'], + '/send': ['to', 'amount', 'asset', 'memo'], + '/receive': ['amount', 'asset'], + '/create-wallet': [], +}; + +/** Aliases for paths that read naturally in a shared link but are not routes. */ +const PATH_ALIASES: Record = { + '': '/', + '/': '/', + '/request': '/receive', + '/payment-request': '/pay', +}; + +/** + * SEP-7 `pay` field names mapped onto the query parameters the `/pay` route + * understands. Full validation of the URI (address checksums, amount ranges, + * hostile callbacks) belongs to the SEP-7 handler in backlog #38 — this mapping + * only decides *where* the link goes, and the raw URI is forwarded as `uri` so + * that handler can re-parse the original, unmodified request. + */ +const SEP7_PARAM_MAP: Record = { + destination: 'to', + amount: 'amount', + asset_code: 'asset', + memo: 'memo', + msg: 'msg', +}; + +/** Context supplied by expo-router when it hands the app an inbound link. */ +export type DeepLinkContext = { + /** + * True when the link cold-started the app, false on a warm resume. Resolution + * is identical for both — the flag is threaded through only so callers can + * distinguish the two when logging. + */ + initial?: boolean; +}; + +function isAssociatedDomain(host: string): boolean { + return (ASSOCIATED_DOMAINS as readonly string[]).includes(host.toLowerCase()); +} + +/** + * Split a URL into its path and query without using the `URL` constructor, + * which rejects non-special schemes like `web+stellar:` inconsistently across + * Hermes, Node, and browsers. + */ +function splitPathAndQuery(rest: string): { path: string; query: string } { + const withoutFragment = rest.split('#')[0] ?? ''; + const queryIndex = withoutFragment.indexOf('?'); + return queryIndex === -1 + ? { path: withoutFragment, query: '' } + : { + path: withoutFragment.slice(0, queryIndex), + query: withoutFragment.slice(queryIndex + 1), + }; +} + +/** Normalise a raw path to a single leading slash and no trailing slash. */ +function normalizePath(path: string): string { + const decoded = decodeURIComponent(path.trim()); + const trimmed = `/${decoded.replace(/^\/+/, '').replace(/\/+$/, '')}`; + return trimmed === '/' ? '/' : trimmed.toLowerCase(); +} + +/** + * Decode one query component. React Native's own `URLSearchParams` is a stub + * whose accessors throw, so query strings are parsed by hand here rather than + * depending on which polyfill happens to be installed at launch time. + */ +function decodeComponent(value: string): string { + try { + return decodeURIComponent(value.replace(/\+/g, ' ')); + } catch { + // A malformed percent-escape is not worth discarding the whole link over. + return value; + } +} + +function parseQuery(query: string): Array<[string, string]> { + const pairs: Array<[string, string]> = []; + for (const part of query.split('&')) { + if (!part) continue; + const equalsIndex = part.indexOf('='); + const key = equalsIndex === -1 ? part : part.slice(0, equalsIndex); + const value = equalsIndex === -1 ? '' : part.slice(equalsIndex + 1); + pairs.push([decodeComponent(key), decodeComponent(value)]); + } + return pairs; +} + +function buildTarget(route: string, query: string, rename?: Record): string { + const allowed = LINKABLE_ROUTES[route]; + if (!allowed) return FALLBACK_ROUTE; + + const forwarded: Array<[string, string]> = []; + const seen = new Set(); + + for (const [rawKey, value] of parseQuery(query)) { + const key = rename ? rename[rawKey] : rawKey; + // First occurrence wins; a repeated key must not overwrite it. + if (!key || !allowed.includes(key) || seen.has(key)) continue; + const trimmed = value.trim(); + if (!trimmed) continue; + seen.add(key); + forwarded.push([key, trimmed]); + } + + if (forwarded.length === 0) return route; + const serialized = forwarded + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); + return `${route}?${serialized}`; +} + +/** + * Resolve an inbound URL to an in-app route. + * + * Always returns a path the router can navigate to; unrecognised, malformed, or + * foreign links resolve to {@link FALLBACK_ROUTE} rather than throwing, because + * a throw here happens during launch and would take the app down with it. + * + * @example + * resolveDeepLink('veil://pay?to=GABC&amount=10') // '/pay?to=GABC&amount=10' + * resolveDeepLink('https://app.veil.xyz/receive') // '/receive' + * resolveDeepLink('web+stellar:pay?destination=GABC') // '/pay?to=GABC&uri=…' + * resolveDeepLink('https://evil.example/pay') // '/' + */ +export function resolveDeepLink(url: string, _context: DeepLinkContext = {}): string { + if (typeof url !== 'string') return FALLBACK_ROUTE; + const raw = url.trim(); + if (!raw || raw.length > MAX_DEEP_LINK_LENGTH) return FALLBACK_ROUTE; + + try { + // expo-router hands `+native-intent` an already-stripped path (e.g. `/pay?x=1`) + // when the link matched the app's own scheme, so accept that shape too. + if (raw.startsWith('/')) { + return resolveInAppPath(raw); + } + + const schemeEnd = raw.indexOf(':'); + if (schemeEnd === -1) return FALLBACK_ROUTE; + const scheme = raw.slice(0, schemeEnd).toLowerCase(); + const rest = raw.slice(schemeEnd + 1); + + if (scheme === SEP7_SCHEME) { + return resolveSep7Uri(rest, raw); + } + + if (scheme === DEEP_LINK_SCHEME) { + // `veil://pay?x=1` and the host-less `veil:pay?x=1` are both valid. + return resolveInAppPath(rest.replace(/^\/\//, '/')); + } + + if (scheme === 'https' || scheme === 'http') { + return resolveWebLink(rest); + } + + return FALLBACK_ROUTE; + } catch { + return FALLBACK_ROUTE; + } +} + +function resolveInAppPath(pathAndQuery: string): string { + const { path, query } = splitPathAndQuery(pathAndQuery); + const normalized = normalizePath(path); + const route = PATH_ALIASES[normalized] ?? normalized; + if (!(route in LINKABLE_ROUTES)) return FALLBACK_ROUTE; + return buildTarget(route, query); +} + +function resolveWebLink(rest: string): string { + // `rest` is everything after `https:` — strip the `//` and peel off the host. + const authorityAndPath = rest.replace(/^\/\//, ''); + const slashIndex = authorityAndPath.search(/[/?#]/); + const authority = slashIndex === -1 ? authorityAndPath : authorityAndPath.slice(0, slashIndex); + // Drop any userinfo (`user@host`) and port so `evil.com@app.veil.xyz` cannot + // masquerade as an associated domain. + const host = authority.split('@').pop()?.split(':')[0] ?? ''; + if (!isAssociatedDomain(host)) return FALLBACK_ROUTE; + + const pathAndQuery = slashIndex === -1 ? '/' : authorityAndPath.slice(slashIndex); + return resolveInAppPath(pathAndQuery); +} + +function resolveSep7Uri(rest: string, originalUri: string): string { + const { path, query } = splitPathAndQuery(rest); + // SEP-7 puts the operation where a path would go: `web+stellar:pay?…`. + if (path.replace(/^\/+/, '').toLowerCase() !== 'pay') return FALLBACK_ROUTE; + + const target = buildTarget('/pay', query, SEP7_PARAM_MAP); + const separator = target.includes('?') ? '&' : '?'; + return `${target}${separator}uri=${encodeURIComponent(originalUri)}`; +} diff --git a/frontend/mobile/lib/theme.ts b/frontend/mobile/lib/theme.ts new file mode 100644 index 00000000..d1db045e --- /dev/null +++ b/frontend/mobile/lib/theme.ts @@ -0,0 +1,9 @@ +/** Shared colour tokens for the mobile shell, matching the wallet's dark surface. */ +export const colors = { + background: '#0B0B0F', + surface: '#16161C', + border: '#2A2A33', + text: '#FFFFFF', + muted: '#9BA1A6', + accent: '#208AEF', +} as const; diff --git a/frontend/mobile/package-lock.json b/frontend/mobile/package-lock.json index 5f634409..54d8d51b 100644 --- a/frontend/mobile/package-lock.json +++ b/frontend/mobile/package-lock.json @@ -28,7 +28,8 @@ }, "devDependencies": { "@types/react": "~19.2.2", - "typescript": "~6.0.3" + "typescript": "~6.0.3", + "vitest": "~3.2.4" } }, "node_modules/@adobe/css-tools": { @@ -1187,6 +1188,448 @@ "node": ">=0.8.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@expo-google-fonts/material-symbols": { "version": "0.4.41", "resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.41.tgz", @@ -2306,133 +2749,483 @@ } } }, - "node_modules/@react-native/debugger-frontend": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.0.tgz", - "integrity": "sha512-7Mb3nDfyJeys+ELF75Ageu7VKERlnIMoO+aNPoXqTXvz+b41L6l2CqMyLpDHxkBSlenij6gEepPNgaIyWHbJZw==", - "license": "BSD-3-Clause", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } + "node_modules/@react-native/debugger-frontend": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.0.tgz", + "integrity": "sha512-7Mb3nDfyJeys+ELF75Ageu7VKERlnIMoO+aNPoXqTXvz+b41L6l2CqMyLpDHxkBSlenij6gEepPNgaIyWHbJZw==", + "license": "BSD-3-Clause", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/debugger-shell": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.0.tgz", + "integrity": "sha512-Y0zEkZzLz8ou6o/VLml1A31X/rMgc6DRjwxwzPMa94qRTMY070WeBCNTITQo4kKTBAUgbxh07oXPQqp0Tpja8w==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "fb-dotslash": "0.5.8" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.0.tgz", + "integrity": "sha512-20pTO6yTybmvXvro520H6C7jydIQnLKOl5qFtVEcHSdFrY63r3OGei+Rx9bILgSRmH6jgnfEcijcMx7pwWuQtw==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.86.0", + "@react-native/debugger-shell": "0.86.0", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.3.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^7.5.10" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.0.tgz", + "integrity": "sha512-a1RcfaEDqWExCGfCwadIxt4l8FvKYgFqeMf2uzeKyAOnb+vTGNIeCvifFL2MqvgaeYxlER437HbMIajGcuJ1pQ==", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.0.tgz", + "integrity": "sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.0.tgz", + "integrity": "sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.86.0", + "hermes-parser": "0.36.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.0.tgz", + "integrity": "sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/js-polyfills": "0.86.0", + "@react-native/metro-babel-transformer": "0.86.0", + "metro-config": "^0.84.3", + "metro-runtime": "^0.84.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.0.tgz", + "integrity": "sha512-kG0wfCGghUKlfxkJyyHCDVutWVYWK7/DG58ojA/4v9EfulgF+osuSQmlbNb3rcKX58qutm7JcldSeVLgGFha9g==", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.0.tgz", + "integrity": "sha512-4/ZLXdf/OSpPDVO0AsQ1SJdRIzt5t9BNQ46QwGgxvX7/cirYR5k8KXctNGGgW8lQo2gZChEfY2zFCZg9nM/jiw==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@types/react": "^19.2.0", + "react": "*", + "react-native": "0.86.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@react-native/debugger-shell": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.0.tgz", - "integrity": "sha512-Y0zEkZzLz8ou6o/VLml1A31X/rMgc6DRjwxwzPMa94qRTMY070WeBCNTITQo4kKTBAUgbxh07oXPQqp0Tpja8w==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@react-native/dev-middleware": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.0.tgz", - "integrity": "sha512-20pTO6yTybmvXvro520H6C7jydIQnLKOl5qFtVEcHSdFrY63r3OGei+Rx9bILgSRmH6jgnfEcijcMx7pwWuQtw==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.86.0", - "@react-native/debugger-shell": "0.86.0", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.3.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.0.tgz", - "integrity": "sha512-a1RcfaEDqWExCGfCwadIxt4l8FvKYgFqeMf2uzeKyAOnb+vTGNIeCvifFL2MqvgaeYxlER437HbMIajGcuJ1pQ==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@react-native/js-polyfills": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.0.tgz", - "integrity": "sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@react-native/metro-babel-transformer": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.0.tgz", - "integrity": "sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.86.0", - "hermes-parser": "0.36.0", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@react-native/metro-config": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.0.tgz", - "integrity": "sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@react-native/js-polyfills": "0.86.0", - "@react-native/metro-babel-transformer": "0.86.0", - "metro-config": "^0.84.3", - "metro-runtime": "^0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@react-native/normalize-colors": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.0.tgz", - "integrity": "sha512-kG0wfCGghUKlfxkJyyHCDVutWVYWK7/DG58ojA/4v9EfulgF+osuSQmlbNb3rcKX58qutm7JcldSeVLgGFha9g==", - "license": "MIT" + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@react-native/virtualized-lists": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.0.tgz", - "integrity": "sha512-4/ZLXdf/OSpPDVO0AsQ1SJdRIzt5t9BNQ46QwGgxvX7/cirYR5k8KXctNGGgW8lQo2gZChEfY2zFCZg9nM/jiw==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@types/react": "^19.2.0", - "react": "*", - "react-native": "0.86.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@sinclair/typebox": { "version": "0.27.12", @@ -2540,6 +3333,31 @@ "license": "MIT", "peer": true }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hammerjs": { "version": "2.0.46", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", @@ -2570,54 +3388,169 @@ "@types/istanbul-lib-report": "*" } }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-test-renderer": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/react-test-renderer": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", - "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/react": "*" + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", - "license": "ISC" - }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", @@ -2781,6 +3714,16 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", @@ -3081,6 +4024,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -3113,6 +4066,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3129,6 +4099,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", @@ -3442,6 +4422,16 @@ "node": ">=0.10" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3564,6 +4554,55 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3591,6 +4630,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3609,6 +4658,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/expo": { "version": "57.0.8", "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.8.tgz", @@ -4312,6 +5371,21 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4984,9 +6058,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5007,9 +6078,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5030,9 +6098,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5053,9 +6118,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5216,6 +6278,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5235,6 +6304,16 @@ "lz-string": "bin/bin.js" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -6020,6 +7099,23 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6682,6 +7778,51 @@ "node": ">=4" } }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6897,6 +8038,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6984,6 +8132,13 @@ "node": ">=6" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -7017,6 +8172,13 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", @@ -7073,6 +8235,26 @@ "node": ">=8" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/structured-headers": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", @@ -7168,6 +8350,20 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -7184,6 +8380,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7465,6 +8691,177 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -7538,6 +8935,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/frontend/mobile/package.json b/frontend/mobile/package.json index 5dbb7338..70c1b069 100644 --- a/frontend/mobile/package.json +++ b/frontend/mobile/package.json @@ -9,7 +9,8 @@ "ios": "expo start --ios", "web": "expo start --web", "lint": "expo lint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "expo": "~57.0.8", @@ -32,6 +33,7 @@ }, "devDependencies": { "@types/react": "~19.2.2", - "typescript": "~6.0.3" + "typescript": "~6.0.3", + "vitest": "~3.2.4" } } diff --git a/frontend/wallet/next.config.js b/frontend/wallet/next.config.js index 1da0a4ff..c241ec6b 100644 --- a/frontend/wallet/next.config.js +++ b/frontend/wallet/next.config.js @@ -8,6 +8,22 @@ const nextConfig = { // Allow imports from outside the Next.js project root (e.g. ../../sdk/src) externalDir: true, }, + async headers() { + // iOS refuses an apple-app-site-association file that is not served as + // JSON, and the file has no extension so Next cannot infer the type. + // Android is stricter still: assetlinks.json must be reachable over HTTPS + // with no redirect. Both files live in public/.well-known/. + return [ + { + source: '/.well-known/apple-app-site-association', + headers: [{ key: 'Content-Type', value: 'application/json' }], + }, + { + source: '/.well-known/assetlinks.json', + headers: [{ key: 'Content-Type', value: 'application/json' }], + }, + ] + }, webpack: (config) => { // When webpack compiles SDK source files from ../../sdk/src/, it resolves // node_modules going up from that directory and misses the wallet's diff --git a/frontend/wallet/public/.well-known/apple-app-site-association b/frontend/wallet/public/.well-known/apple-app-site-association new file mode 100644 index 00000000..2df9c7e3 --- /dev/null +++ b/frontend/wallet/public/.well-known/apple-app-site-association @@ -0,0 +1,19 @@ +{ + "applinks": { + "details": [ + { + "appIDs": ["APPLE_TEAM_ID.xyz.veil.wallet"], + "components": [ + { "/": "/pay", "comment": "Payment requests" }, + { "/": "/pay/*", "comment": "Payment requests" }, + { "/": "/send", "comment": "Send flow" }, + { "/": "/send/*", "comment": "Send flow" }, + { "/": "/receive", "comment": "Receive flow" }, + { "/": "/receive/*", "comment": "Receive flow" }, + { "/": "/create-wallet", "comment": "Onboarding" }, + { "/": "/*", "exclude": true, "comment": "Everything else stays on the web" } + ] + } + ] + } +} diff --git a/frontend/wallet/public/.well-known/assetlinks.json b/frontend/wallet/public/.well-known/assetlinks.json new file mode 100644 index 00000000..160ed80e --- /dev/null +++ b/frontend/wallet/public/.well-known/assetlinks.json @@ -0,0 +1,10 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "xyz.veil.wallet", + "sha256_cert_fingerprints": ["ANDROID_RELEASE_CERT_SHA256_FINGERPRINT"] + } + } +] From 65fe56446f0033ef4a0ca4800a9a6852625fe357 Mon Sep 17 00:00:00 2001 From: ezedike-evan <120946193+ezedike-evan@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:50:07 +0100 Subject: [PATCH 2/2] ci(mobile): use npm ci without the npm install fallback `npm ci || npm install` would silently paper over lockfile drift, which is the one thing the job is there to catch. Verified `npm ci` resolves cleanly against frontend/mobile/package-lock.json. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 631d8ba4..b4681f62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,7 +202,7 @@ jobs: node-version: '20' cache: npm cache-dependency-path: frontend/mobile/package-lock.json - - run: npm ci || npm install + - run: npm ci - run: npm run typecheck - run: npm test