From fb0ad7f8cbcfd297e00b70143cbadfa3dc0cc813 Mon Sep 17 00:00:00 2001 From: BigManly4 <294554482+BigManly4@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:39:00 -0700 Subject: [PATCH] feat(mobile): add SEP-24 buy (on-ramp) screen Adds a mobile buy screen that lets users bring fiat into the wallet through a SEP-24 anchor. lib/sep24.ts ports the deposit-side protocol logic from frontend/wallet/lib/sep24.ts (anchor TOML discovery, SEP-10 challenge validation/signing, interactive deposit request, status polling) as pure, injectable-signer functions with no browser-only APIs. app/buy.tsx drives the flow: enter anchor domain/amount/asset, kick off the interactive deposit, launch the returned URL with expo-web-browser's openAuthSessionAsync, and poll for the resulting transaction status once the user returns to the app. Adds expo-web-browser as a dependency (installed via `expo install` for SDK-compatible versioning). --- frontend/mobile/app.json | 3 +- frontend/mobile/app/buy.tsx | 367 ++++++++++++++++++++++++++++++ frontend/mobile/lib/sep24.ts | 281 +++++++++++++++++++++++ frontend/mobile/package-lock.json | 50 ++-- frontend/mobile/package.json | 1 + 5 files changed, 678 insertions(+), 24 deletions(-) create mode 100644 frontend/mobile/app/buy.tsx create mode 100644 frontend/mobile/lib/sep24.ts diff --git a/frontend/mobile/app.json b/frontend/mobile/app.json index 415223e2..2fa72c43 100644 --- a/frontend/mobile/app.json +++ b/frontend/mobile/app.json @@ -32,7 +32,8 @@ "image": "./assets/images/splash-icon.png", "imageWidth": 76 } - ] + ], + "expo-web-browser" ], "experiments": { "typedRoutes": true, diff --git a/frontend/mobile/app/buy.tsx b/frontend/mobile/app/buy.tsx new file mode 100644 index 00000000..c1f87743 --- /dev/null +++ b/frontend/mobile/app/buy.tsx @@ -0,0 +1,367 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import * as Linking from 'expo-linking'; +import * as WebBrowser from 'expo-web-browser'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { + discoverAnchorInfo, + getTransactionStatus, + initiateDeposit, + isSep24Complete, + type Sep24TransactionStatus, +} from '../lib/sep24'; + +// Mobile has no signing infra ported yet — the fee payer address is read from +// env, matching the stub pattern used in swap.tsx. Once wallet signing lands, +// this can be replaced with the real spending address. +const FEE_PAYER_ADDRESS = process.env['EXPO_PUBLIC_FEE_PAYER_ADDRESS']?.trim() || ''; +const DEFAULT_ANCHOR_DOMAIN = process.env['EXPO_PUBLIC_SEP24_ANCHOR_DOMAIN']?.trim() || 'testanchor.stellar.org'; + +type Step = 'form' | 'connecting' | 'pending' | 'success' | 'error'; + +const POLL_INTERVAL_MS = 5_000; + +export default function BuyScreen() { + const [anchorDomain, setAnchorDomain] = useState(DEFAULT_ANCHOR_DOMAIN); + const [assetCode, setAssetCode] = useState('XLM'); + const [amount, setAmount] = useState(''); + const [step, setStep] = useState('form'); + const [error, setError] = useState(null); + + const [transferServerUrl, setTransferServerUrl] = useState(null); + const [txnId, setTxnId] = useState(null); + const [txnStatus, setTxnStatus] = useState(null); + + const pollRef = useRef | null>(null); + + const stopPolling = useCallback(() => { + if (pollRef.current) { + clearInterval(pollRef.current); + pollRef.current = null; + } + }, []); + + const startPolling = useCallback( + (server: string, id: string) => { + stopPolling(); + pollRef.current = setInterval(async () => { + try { + const status = await getTransactionStatus(server, id); + setTxnStatus(status); + if (status.status === 'completed') { + stopPolling(); + setStep('success'); + } else if (isSep24Complete(status.status)) { + stopPolling(); + } + } catch { + // Polling errors are soft — keep trying until the user backs out. + } + }, POLL_INTERVAL_MS); + }, + [stopPolling] + ); + + useEffect(() => () => stopPolling(), [stopPolling]); + + const handleBuy = async () => { + if (!FEE_PAYER_ADDRESS) { + setError('Spending wallet not set up yet. Fund your wallet first.'); + setStep('error'); + return; + } + if (!anchorDomain.trim()) { + setError('Enter the anchor domain.'); + setStep('error'); + return; + } + + setStep('connecting'); + setError(null); + try { + const { transferServerUrl: server } = await discoverAnchorInfo(anchorDomain.trim()); + setTransferServerUrl(server); + + const deposit = await initiateDeposit(server, { + assetCode: assetCode.trim() || 'XLM', + account: FEE_PAYER_ADDRESS, + amount: amount.trim() || undefined, + }); + setTxnId(deposit.id); + + // Launch the anchor's interactive flow in an in-app browser session and + // return to the app once the user finishes (or dismisses) it. + const redirectUrl = Linking.createURL('buy'); + await WebBrowser.openAuthSessionAsync(deposit.url, redirectUrl); + + setStep('pending'); + startPolling(server, deposit.id); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Could not connect to anchor.'); + setStep('error'); + } + }; + + const checkStatusNow = async () => { + if (!transferServerUrl || !txnId) return; + try { + const status = await getTransactionStatus(transferServerUrl, txnId); + setTxnStatus(status); + if (status.status === 'completed') { + stopPolling(); + setStep('success'); + } + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Could not fetch transaction status.'); + } + }; + + const reset = () => { + stopPolling(); + setStep('form'); + setError(null); + setTxnId(null); + setTxnStatus(null); + setTransferServerUrl(null); + }; + + if (step === 'success') { + return ( + + Deposit confirmed + + Your funds are on their way. It may take a few minutes for the balance to appear. + + {txnStatus?.amount_in && ( + + +{txnStatus.amount_in} {txnStatus.amount_in_asset ?? assetCode} + + )} + + Buy again + + + ); + } + + if (step === 'pending') { + return ( + + Waiting for the anchor + + Complete the deposit in the browser window. Come back here when you're done. + + {txnStatus && ( + + Status + {txnStatus.status} + + )} + + + Check status + + + Cancel + + + ); + } + + return ( + + Buy + Bring fiat into your wallet through a SEP-24 anchor. + + {!FEE_PAYER_ADDRESS && ( + + + Spending wallet not set up yet. Fund your wallet first so the anchor knows where to + send your deposit. + + + )} + + + ANCHOR DOMAIN + + + + + AMOUNT (OPTIONAL) + + + + ASSET + + + + + + {step === 'connecting' ? ( + + ) : ( + Connect to anchor + )} + + + + {step === 'error' && error && {error}} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + backgroundColor: '#0B0B0F', + padding: 24, + gap: 16, + }, + title: { + color: '#FFFFFF', + fontSize: 28, + fontWeight: '700', + }, + subtitle: { + color: '#9BA1A6', + fontSize: 15, + }, + notice: { + backgroundColor: '#1e293b', + borderRadius: 10, + borderWidth: 1, + borderColor: '#334155', + padding: 14, + }, + noticeText: { + color: '#94a3b8', + fontSize: 13, + lineHeight: 18, + }, + form: { + gap: 10, + }, + label: { + color: '#64748b', + fontSize: 11, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 1, + }, + row: { + flexDirection: 'row', + gap: 10, + }, + rowFieldFlex: { + flex: 2, + gap: 6, + }, + rowFieldAsset: { + flex: 1, + gap: 6, + }, + input: { + backgroundColor: '#1e293b', + borderRadius: 10, + padding: 14, + color: '#f1f5f9', + fontSize: 16, + borderWidth: 1, + borderColor: '#334155', + }, + btn: { + borderRadius: 10, + paddingVertical: 14, + alignItems: 'center', + }, + btnPrimary: { + backgroundColor: '#6366f1', + }, + btnSecondary: { + backgroundColor: '#334155', + }, + btnDisabled: { + opacity: 0.4, + }, + btnText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + linkBtn: { + alignItems: 'center', + paddingVertical: 8, + }, + linkText: { + color: '#94a3b8', + fontSize: 14, + }, + card: { + backgroundColor: '#1e293b', + borderRadius: 10, + padding: 14, + gap: 4, + }, + rowLabel: { + color: '#64748b', + fontSize: 11, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 1, + }, + rowValue: { + color: '#f1f5f9', + fontSize: 14, + fontWeight: '600', + }, + spinner: { + marginVertical: 4, + }, + amount: { + color: '#a5b4fc', + fontFamily: 'monospace', + fontSize: 20, + fontWeight: '700', + }, + error: { + color: '#f87171', + fontSize: 13, + backgroundColor: '#450a0a', + borderRadius: 8, + padding: 10, + }, +}); diff --git a/frontend/mobile/lib/sep24.ts b/frontend/mobile/lib/sep24.ts new file mode 100644 index 00000000..11b23f45 --- /dev/null +++ b/frontend/mobile/lib/sep24.ts @@ -0,0 +1,281 @@ +/** + * SEP-24 Hosted Deposit utility (mobile). + * https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md + * + * Ported from frontend/wallet/lib/sep24.ts. Only the deposit (on-ramp) path is + * implemented for now, matching the mobile buy screen's scope. + * + * Pure / testable — no browser-only APIs (no `window`, `document`, + * `localStorage`, `sessionStorage`, WebAuthn). Where the web version signs + * the SEP-10 challenge with a passkey stored in browser storage, this module + * takes an injectable `signChallenge` async function instead — same pattern + * as `executeBulkPayout(rows, submitBatch)` in `lib/bulkPayout.ts`. Mobile has + * no signing infra ported yet, so screens pass a stub today. + */ + +import { Networks, Transaction, TransactionBuilder, type Keypair } from '@stellar/stellar-sdk'; + +// ── Anchor config ──────────────────────────────────────────────────────────── + +export interface AnchorInfo { + transferServerUrl: string; + webAuthEndpoint: string; + networkPassphrase: string; +} + +// ── TOML discovery ─────────────────────────────────────────────────────────── + +/** Fetch and parse `/.well-known/stellar.toml` for SEP-24 endpoints. */ +export async function discoverAnchorInfo(anchorDomain: string): Promise { + const res = await fetch(`https://${anchorDomain}/.well-known/stellar.toml`, { + signal: AbortSignal.timeout(10_000), + }); + if (!res.ok) { + throw new Error(`Could not fetch stellar.toml from ${anchorDomain} (HTTP ${res.status})`); + } + + const text = await res.text(); + + const transferMatch = text.match(/TRANSFER_SERVER_SEP0024\s*=\s*"([^"]+)"/); + if (!transferMatch) { + throw new Error(`TRANSFER_SERVER_SEP0024 not found in ${anchorDomain}/.well-known/stellar.toml`); + } + + const webAuthMatch = text.match(/WEB_AUTH_ENDPOINT\s*=\s*"([^"]+)"/); + if (!webAuthMatch) { + throw new Error(`WEB_AUTH_ENDPOINT not found in ${anchorDomain}/.well-known/stellar.toml`); + } + + const networkMatch = text.match(/NETWORK_PASSPHRASE\s*=\s*"([^"]+)"/); + const networkPassphrase = networkMatch ? networkMatch[1] : Networks.TESTNET; + + return { + transferServerUrl: transferMatch[1].replace(/\/$/, ''), + webAuthEndpoint: webAuthMatch[1].replace(/\/$/, ''), + networkPassphrase, + }; +} + +// ── SEP-10 challenge signing (pure / testable) ────────────────────────────── + +/** Error codes for typed SEP-10 validation failures. */ +export type Sep10ErrorCode = 'MISSING_MANAGE_DATA' | 'INVALID_HOME_DOMAIN' | 'EXPIRED' | 'MALFORMED'; + +/** + * Typed error thrown by {@link signSep10Challenge} when a challenge is invalid. + * Consumers can narrow on `error.code` for programmatic handling. + */ +export class Sep10ChallengeError extends Error { + readonly code: Sep10ErrorCode; + constructor(message: string, code: Sep10ErrorCode) { + super(message); + this.name = 'Sep10ChallengeError'; + this.code = code; + } +} + +/** + * Validate and sign a SEP-10 challenge transaction with a classic Stellar keypair. + * + * SEP-10 rules enforced: + * - The transaction MUST contain at least one `manage_data` operation. + * - The `manage_data` key MUST end with " auth" (the standard anchor suffix). + * - The transaction MUST have `timeBounds` set (minTime / maxTime). + * - `maxTime` MUST be in the future (challenge not expired). + * + * @throws {Sep10ChallengeError} if any SEP-10 validation rule is violated. + */ +export function signSep10Challenge( + challengeXdr: string, + networkPassphrase: string, + signerKeypair: Keypair +): string { + let tx: Transaction; + try { + tx = new Transaction(challengeXdr, networkPassphrase); + } catch (err) { + throw new Sep10ChallengeError(`Failed to parse challenge XDR: ${(err as Error).message}`, 'MALFORMED'); + } + + const manageDataOps = tx.operations.filter((op) => op.type === 'manageData'); + if (manageDataOps.length === 0) { + throw new Sep10ChallengeError( + 'SEP-10 challenge must contain at least one manage_data operation', + 'MISSING_MANAGE_DATA' + ); + } + + const firstKey = (manageDataOps[0] as { name: string }).name; + if (!firstKey.endsWith(' auth')) { + throw new Sep10ChallengeError( + `manage_data key "${firstKey}" does not follow the " auth" SEP-10 convention`, + 'INVALID_HOME_DOMAIN' + ); + } + + if (!tx.timeBounds) { + throw new Sep10ChallengeError('SEP-10 challenge must have timeBounds set', 'MALFORMED'); + } + + const nowSec = Math.floor(Date.now() / 1000); + const maxTime = Number(tx.timeBounds.maxTime); + if (maxTime > 0 && nowSec > maxTime) { + throw new Sep10ChallengeError( + `SEP-10 challenge has expired (maxTime=${maxTime}, now=${nowSec})`, + 'EXPIRED' + ); + } + + const rebuilt = TransactionBuilder.cloneFrom(tx).build(); + rebuilt.sign(signerKeypair); + return rebuilt.toXDR(); +} + +// ── SEP-10 Web Auth ────────────────────────────────────────────────────────── + +/** + * Injectable signer for the SEP-10 challenge. The web app signs with a + * browser passkey; mobile has no signing infra ported yet, so callers pass a + * stub (or, once wired up, a function backed by the mobile wallet's signer). + * Returns the signed (or otherwise anchor-acceptable) transaction XDR. + */ +export type Sep10ChallengeSigner = (params: { + challengeXdr: string; + networkPassphrase: string; + account: string; +}) => Promise; + +/** + * Obtain a SEP-10 JWT by: + * 1. Fetching the challenge transaction from the anchor's WEB_AUTH_ENDPOINT + * 2. Signing it via the injected `signChallenge` function + * 3. Posting the signed transaction back to get a JWT + */ +export async function getSep10Jwt( + webAuthEndpoint: string, + account: string, + networkPassphrase: string, + signChallenge: Sep10ChallengeSigner +): Promise { + const challengeRes = await fetch(`${webAuthEndpoint}?account=${encodeURIComponent(account)}`, { + signal: AbortSignal.timeout(10_000), + }); + if (!challengeRes.ok) { + const errText = await challengeRes.text().catch(() => challengeRes.statusText); + throw new Error(`SEP-10 challenge fetch failed (HTTP ${challengeRes.status}): ${errText}`); + } + const { transaction: challengeXdr, network_passphrase } = (await challengeRes.json()) as { + transaction: string; + network_passphrase?: string; + }; + + const effectivePassphrase = network_passphrase ?? networkPassphrase; + + const signedXdr = await signChallenge({ + challengeXdr, + networkPassphrase: effectivePassphrase, + account, + }); + + const tokenRes = await fetch(webAuthEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ transaction: signedXdr }), + signal: AbortSignal.timeout(15_000), + }); + if (!tokenRes.ok) { + const errText = await tokenRes.text().catch(() => tokenRes.statusText); + throw new Error(`SEP-10 token exchange failed (HTTP ${tokenRes.status}): ${errText}`); + } + + const { token } = (await tokenRes.json()) as { token?: string }; + if (!token) throw new Error('Anchor did not return a JWT token.'); + return token; +} + +// ── Shared types ───────────────────────────────────────────────────────────── + +export interface Sep24InteractiveResult { + /** URL to open in the anchor's interactive (KYC / payment) flow. */ + url: string; + /** Anchor-assigned transaction ID — use this to poll status. */ + id: string; +} + +export interface Sep24TransactionStatus { + id: string; + /** SEP-24 status: pending_user_transfer_start | pending_anchor | completed | error | … */ + status: string; + stellar_transaction_id?: string; + message?: string; + amount_in?: string; + amount_in_asset?: string; + amount_out?: string; + amount_out_asset?: string; +} + +// ── Deposit ────────────────────────────────────────────────────────────────── + +export interface InitiateDepositParams { + assetCode: string; + account: string; + amount?: string; + lang?: string; +} + +/** Start a SEP-24 interactive deposit. Returns the interactive URL and txn id to poll. */ +export async function initiateDeposit( + transferServerUrl: string, + params: InitiateDepositParams, + jwt?: string +): Promise { + const body = new URLSearchParams({ + asset_code: params.assetCode, + account: params.account, + lang: params.lang ?? 'en', + ...(params.amount ? { amount: params.amount } : {}), + }); + + const res = await fetch(`${transferServerUrl}/transactions/deposit/interactive`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}), + }, + body: body.toString(), + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + const errText = await res.text().catch(() => res.statusText); + throw new Error(`Deposit initiation failed (HTTP ${res.status}): ${errText}`); + } + + const data = (await res.json()) as { url?: string; id?: string }; + if (!data.url || !data.id) throw new Error('Anchor returned an invalid response (missing url or id)'); + return { url: data.url, id: data.id }; +} + +// ── Status polling ─────────────────────────────────────────────────────────── + +/** Fetch the current status of a SEP-24 transaction from the anchor. */ +export async function getTransactionStatus( + transferServerUrl: string, + txnId: string, + jwt?: string +): Promise { + const res = await fetch(`${transferServerUrl}/transaction?id=${encodeURIComponent(txnId)}`, { + headers: jwt ? { Authorization: `Bearer ${jwt}` } : {}, + signal: AbortSignal.timeout(10_000), + }); + if (!res.ok) throw new Error(`Failed to fetch transaction status (HTTP ${res.status})`); + + const data = (await res.json()) as { transaction?: Sep24TransactionStatus }; + if (!data.transaction) throw new Error('Anchor response missing transaction object'); + return data.transaction; +} + +/** Returns true once a SEP-24 status no longer requires polling. */ +export function isSep24Complete(status: string): boolean { + return ['completed', 'error', 'refunded', 'expired'].includes(status); +} diff --git a/frontend/mobile/package-lock.json b/frontend/mobile/package-lock.json index b31f3525..e783440b 100644 --- a/frontend/mobile/package-lock.json +++ b/frontend/mobile/package-lock.json @@ -18,6 +18,7 @@ "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", "expo-system-ui": "~57.0.1", + "expo-web-browser": "~57.0.2", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.86.0", @@ -67,7 +68,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -952,6 +952,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -967,6 +968,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -998,6 +1000,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -2239,6 +2242,7 @@ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.0.tgz", "integrity": "sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", @@ -2401,6 +2405,7 @@ "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", @@ -2419,6 +2424,7 @@ "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", @@ -2537,6 +2543,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2556,6 +2563,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2568,6 +2576,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -2581,7 +2590,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -2625,7 +2635,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/hammerjs": { "version": "2.0.46", @@ -2671,7 +2682,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3207,7 +3217,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", @@ -3790,7 +3799,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -3946,7 +3956,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.8.tgz", "integrity": "sha512-0IxxoPZbT54IH4fHL5NihkvED9HBVQx3uNdPvyv8pFUHWJ81RdFjL0aJys1IB7hbo09KTz4xW4I2aWVOXKAcJQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "^57.0.10", @@ -4024,7 +4033,6 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.7.tgz", "integrity": "sha512-ShDwaKnh3UieCQ/dG0kO8PuicTTatn3WDGmXbq/fukyzPXWhdUxf37DINVDQS6D3DDG7nfqItij+QvnDHSQhTg==", "license": "MIT", - "peer": true, "dependencies": { "@expo/env": "~2.4.2" }, @@ -4048,7 +4056,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz", "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -4084,7 +4091,6 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.4.tgz", "integrity": "sha512-e1alfHNJdywIfJkCuKMc6M3hBfAGPd2gKMeF/6V7qwFWzHCS2mTBqU+KaO4FLpltA5Nt6CYEx6zmUlGfUF+8lA==", "license": "MIT", - "peer": true, "dependencies": { "expo-constants": "~57.0.7", "invariant": "^2.2.4" @@ -4144,7 +4150,6 @@ "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.8.tgz", "integrity": "sha512-xAyTnZl597G9/r17GOuyTy6VlhjYCVmgzgmP00bhZ9b+VstPl3tTrOOhSFagVpeln47nKp7x7vgkANNheCv4eQ==", "license": "MIT", - "peer": true, "dependencies": { "@expo/log-box": "^57.0.1", "@expo/metro-runtime": "^57.0.7", @@ -4219,7 +4224,6 @@ "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz", "integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.16.0" } @@ -4285,6 +4289,16 @@ } } }, + "node_modules/expo-web-browser": { + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-57.0.2.tgz", + "integrity": "sha512-3vl5kvd7PB48ub6PpNIJUuPxO8xVa6D8RnIgNba6SXRwqFprOfeEZgwTgtm41kz0AAtvMOztUVNEUkwrHKjqMQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo/node_modules/@expo/cli": { "version": "57.0.10", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.10.tgz", @@ -4473,7 +4487,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5791,6 +5804,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6813,7 +6827,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6833,7 +6846,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6870,7 +6882,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.0.tgz", "integrity": "sha512-17ALh/dd6AO4pgOVmOO5Axll5PbErEo3XFyLokyzW6usyi+OShIEPwUW26wLPlhVifgSOIfECCH0WN+0IqtJ1w==", "license": "MIT", - "peer": true, "dependencies": { "@react-native/assets-registry": "0.86.0", "@react-native/codegen": "0.86.0", @@ -6946,7 +6957,6 @@ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz", "integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==", "license": "MIT", - "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", @@ -6973,7 +6983,6 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.0.tgz", "integrity": "sha512-+iPfvK34PKKYP/p/4TaBliFkbfvjGDIvXuiiaxvISP5ip7sWegvlacwU/uAV6zNDSSmX0tDyER7PurPMKGDipA==", "license": "MIT", - "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" @@ -6989,7 +6998,6 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -7014,7 +7022,6 @@ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -7047,7 +7054,6 @@ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.0.tgz", "integrity": "sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", @@ -7083,7 +7089,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7920,7 +7925,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/frontend/mobile/package.json b/frontend/mobile/package.json index 4bb69023..233ef407 100644 --- a/frontend/mobile/package.json +++ b/frontend/mobile/package.json @@ -22,6 +22,7 @@ "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", "expo-system-ui": "~57.0.1", + "expo-web-browser": "~57.0.2", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.86.0",