diff --git a/artifacts/mobile/app/(tabs)/index.tsx b/artifacts/mobile/app/(tabs)/index.tsx index 5545de7..9727755 100644 --- a/artifacts/mobile/app/(tabs)/index.tsx +++ b/artifacts/mobile/app/(tabs)/index.tsx @@ -14,6 +14,7 @@ import { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ConnectionBadge } from "@/components/ConnectionBadge"; +import { JobSubmissionSheet } from "@/components/JobSubmissionSheet"; import { TimmyFace } from "@/components/TimmyFace"; import { Colors } from "@/constants/colors"; import { useTimmy } from "@/context/TimmyContext"; @@ -64,6 +65,7 @@ export default function FaceScreen() { const [isListening, setIsListening] = useState(false); const [transcript, setTranscript] = useState(""); const [lastReply, setLastReply] = useState(""); + const [jobSheetVisible, setJobSheetVisible] = useState(false); const micScale = useRef(new Animated.Value(1)).current; const micPulseRef = useRef(null); const webRecognitionRef = useRef(null); @@ -273,31 +275,48 @@ export default function FaceScreen() { ) : null} - {/* Mic button */} + {/* Action buttons */} - - + - - - + + + + + + setJobSheetVisible(true)} + accessibilityRole="button" + accessibilityLabel="Submit paid job" + > + + + + + - {isListening ? "Listening..." : "Tap to speak to Timmy"} + {isListening ? "Listening..." : "Tap mic to speak \u00B7 bolt to submit a job"} + + setJobSheetVisible(false)} + /> ); } @@ -405,6 +424,26 @@ const styles = StyleSheet.create({ paddingTop: 16, gap: 10, }, + actionRow: { + flexDirection: "row", + alignItems: "center", + gap: 20, + }, + jobButton: { + width: 52, + height: 52, + borderRadius: 26, + backgroundColor: C.surface, + borderWidth: 1.5, + borderColor: C.jobStarted + "66", + alignItems: "center", + justifyContent: "center", + shadowColor: C.jobStarted, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 6, + elevation: 4, + }, micButton: { width: 72, height: 72, diff --git a/artifacts/mobile/components/JobSubmissionSheet.tsx b/artifacts/mobile/components/JobSubmissionSheet.tsx new file mode 100644 index 0000000..269372b --- /dev/null +++ b/artifacts/mobile/components/JobSubmissionSheet.tsx @@ -0,0 +1,737 @@ +import { Ionicons } from "@expo/vector-icons"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + ActivityIndicator, + Animated, + Dimensions, + Keyboard, + Modal, + Platform, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import QRCode from "react-native-qrcode-svg"; + +import { Colors } from "@/constants/colors"; + +const C = Colors.dark; +const POLL_INTERVAL = 3000; +const SCREEN_WIDTH = Dimensions.get("window").width; +const QR_SIZE = Math.min(SCREEN_WIDTH - 80, 240); + +const BASE_URL = process.env.EXPO_PUBLIC_DOMAIN ?? ""; + +function getApiBase(): string { + let domain = BASE_URL; + if (!domain) domain = "localhost:8080"; + domain = domain.replace(/\/$/, ""); + if (!/^https?:\/\//.test(domain)) { + const proto = domain.startsWith("localhost") ? "http" : "https"; + domain = `${proto}://${domain}`; + } + return domain; +} + +type JobState = + | "awaiting_eval_payment" + | "evaluating" + | "rejected" + | "awaiting_work_payment" + | "executing" + | "complete" + | "failed"; + +type InvoiceInfo = { + paymentRequest: string; + amountSats: number; +}; + +type JobStatus = { + jobId: string; + state: JobState; + evalInvoice?: InvoiceInfo; + workInvoice?: InvoiceInfo; + result?: string; + reason?: string; + errorMessage?: string; +}; + +type CreateJobResponse = { + jobId: string; + evalInvoice: InvoiceInfo; +}; + +type EstimateResponse = { + estimatedCostSats: number; + estimatedCostUsd: number; + btcPriceUsd: number; +}; + +const STATE_LABELS: Record = { + awaiting_eval_payment: "Awaiting eval payment", + evaluating: "Evaluating your request...", + rejected: "Request rejected", + awaiting_work_payment: "Awaiting work payment", + executing: "Executing job...", + complete: "Complete!", + failed: "Job failed", +}; + +const STATE_ICONS: Record = { + awaiting_eval_payment: "flash-outline", + evaluating: "hourglass-outline", + rejected: "close-circle-outline", + awaiting_work_payment: "flash-outline", + executing: "cog-outline", + complete: "checkmark-circle-outline", + failed: "alert-circle-outline", +}; + +type Props = { + visible: boolean; + onClose: () => void; +}; + +export function JobSubmissionSheet({ visible, onClose }: Props) { + const insets = useSafeAreaInsets(); + const [prompt, setPrompt] = useState(""); + const [estimate, setEstimate] = useState(null); + const [estimateLoading, setEstimateLoading] = useState(false); + const [estimateError, setEstimateError] = useState(""); + const [jobId, setJobId] = useState(null); + const [jobStatus, setJobStatus] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(""); + const [resultExpanded, setResultExpanded] = useState(false); + const slideAnim = useRef(new Animated.Value(0)).current; + const pollRef = useRef | null>(null); + + // Slide animation + useEffect(() => { + Animated.timing(slideAnim, { + toValue: visible ? 1 : 0, + duration: 300, + useNativeDriver: true, + }).start(); + }, [visible, slideAnim]); + + // Polling for job status + useEffect(() => { + if (!jobId) return; + + const poll = async () => { + try { + const res = await fetch(`${getApiBase()}/api/jobs/${jobId}`); + if (!res.ok) return; + const data = (await res.json()) as JobStatus; + setJobStatus(data); + + // Stop polling on terminal states + if ( + data.state === "complete" || + data.state === "failed" || + data.state === "rejected" + ) { + if (pollRef.current) { + clearInterval(pollRef.current); + pollRef.current = null; + } + } + } catch { + // ignore poll errors + } + }; + + // Immediately fetch once + void poll(); + pollRef.current = setInterval(poll, POLL_INTERVAL); + + return () => { + if (pollRef.current) { + clearInterval(pollRef.current); + pollRef.current = null; + } + }; + }, [jobId]); + + const resetState = useCallback(() => { + setPrompt(""); + setEstimate(null); + setEstimateError(""); + setJobId(null); + setJobStatus(null); + setSubmitting(false); + setSubmitError(""); + setResultExpanded(false); + if (pollRef.current) { + clearInterval(pollRef.current); + pollRef.current = null; + } + }, []); + + const handleClose = useCallback(() => { + resetState(); + onClose(); + }, [onClose, resetState]); + + const handleEstimate = useCallback(async () => { + if (!prompt.trim()) return; + Keyboard.dismiss(); + setEstimateLoading(true); + setEstimateError(""); + setEstimate(null); + try { + const res = await fetch(`${getApiBase()}/api/jobs/estimate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ request: prompt.trim() }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: "Request failed" })); + setEstimateError( + (err as { error?: string }).error ?? `HTTP ${res.status}` + ); + return; + } + const data = (await res.json()) as EstimateResponse; + setEstimate(data); + } catch (e) { + setEstimateError( + e instanceof Error ? e.message : "Failed to get estimate" + ); + } finally { + setEstimateLoading(false); + } + }, [prompt]); + + const handleSubmit = useCallback(async () => { + if (!prompt.trim()) return; + Keyboard.dismiss(); + setSubmitting(true); + setSubmitError(""); + try { + const res = await fetch(`${getApiBase()}/api/jobs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ request: prompt.trim() }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: "Request failed" })); + setSubmitError( + (err as { error?: string }).error ?? `HTTP ${res.status}` + ); + return; + } + const data = (await res.json()) as CreateJobResponse; + setJobId(data.jobId); + setJobStatus({ + jobId: data.jobId, + state: "awaiting_eval_payment", + evalInvoice: data.evalInvoice, + }); + } catch (e) { + setSubmitError( + e instanceof Error ? e.message : "Failed to submit job" + ); + } finally { + setSubmitting(false); + } + }, [prompt]); + + // Determine which invoice to show + const activeInvoice: InvoiceInfo | undefined = + jobStatus?.state === "awaiting_work_payment" + ? jobStatus.workInvoice + : jobStatus?.state === "awaiting_eval_payment" + ? jobStatus.evalInvoice + : undefined; + + const isTerminal = + jobStatus?.state === "complete" || + jobStatus?.state === "failed" || + jobStatus?.state === "rejected"; + + const isPolling = jobId != null && !isTerminal; + + const renderContent = () => { + // === JOB IN PROGRESS / COMPLETE === + if (jobId && jobStatus) { + return ( + + {/* Status badge */} + + ["name"] + } + size={20} + color={ + jobStatus.state === "complete" + ? C.jobCompleted + : jobStatus.state === "failed" || jobStatus.state === "rejected" + ? C.error + : C.jobStarted + } + /> + + {STATE_LABELS[jobStatus.state] ?? jobStatus.state} + + {isPolling && ( + + )} + + + {/* Invoice QR */} + {activeInvoice && ( + + + {jobStatus.state === "awaiting_eval_payment" + ? "Pay eval invoice" + : "Pay work invoice"} + + + + + + {activeInvoice.amountSats} sats + + + Scan with your Lightning wallet + + + )} + + {/* Rejection reason */} + {jobStatus.state === "rejected" && jobStatus.reason && ( + + + {jobStatus.reason} + + )} + + {/* Error message */} + {jobStatus.state === "failed" && jobStatus.errorMessage && ( + + + + {jobStatus.errorMessage} + + + )} + + {/* Result */} + {jobStatus.state === "complete" && jobStatus.result && ( + setResultExpanded((v) => !v)} + > + + + Result + + + + {jobStatus.result} + + + )} + + {/* New job button for terminal states */} + {isTerminal && ( + + Submit another job + + )} + + ); + } + + // === INPUT FORM === + return ( + + What should Timmy do? + + + {/* Estimate result */} + {estimate && ( + + + + Estimated cost: {estimate.estimatedCostSats} sats (~$ + {estimate.estimatedCostUsd.toFixed(4)}) + + + )} + + {estimateError ? ( + + + {estimateError} + + ) : null} + + {submitError ? ( + + + {submitError} + + ) : null} + + {/* Action buttons */} + + + {estimateLoading ? ( + + ) : ( + <> + + Estimate + + )} + + + + {submitting ? ( + + ) : ( + <> + + Submit Job + + )} + + + + ); + }; + + return ( + + + + + {/* Handle bar */} + + + + + {/* Header */} + + + Submit Job + + + + + + {renderContent()} + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: "flex-end", + }, + overlayBackdrop: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(0,0,0,0.5)", + }, + sheet: { + backgroundColor: C.surface, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + maxHeight: "85%", + borderWidth: 1, + borderBottomWidth: 0, + borderColor: C.border, + }, + handleRow: { + alignItems: "center", + paddingTop: 10, + paddingBottom: 4, + }, + handle: { + width: 36, + height: 4, + borderRadius: 2, + backgroundColor: C.textMuted, + }, + sheetHeader: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 20, + paddingVertical: 12, + gap: 10, + borderBottomWidth: 1, + borderBottomColor: C.border, + }, + sheetTitle: { + flex: 1, + fontSize: 18, + fontFamily: "Inter_600SemiBold", + color: C.text, + }, + sheetScroll: { + flex: 1, + }, + sheetScrollContent: { + padding: 20, + gap: 16, + }, + inputLabel: { + fontSize: 14, + fontFamily: "Inter_500Medium", + color: C.textSecondary, + }, + textInput: { + backgroundColor: C.surfaceElevated, + borderRadius: 12, + borderWidth: 1, + borderColor: C.border, + color: C.text, + fontFamily: "Inter_400Regular", + fontSize: 15, + padding: 14, + minHeight: 100, + ...(Platform.OS === "web" ? { outlineStyle: "none" as unknown as undefined } : {}), + }, + estimateCard: { + flexDirection: "row", + alignItems: "center", + gap: 8, + backgroundColor: C.surfaceElevated, + borderRadius: 10, + padding: 12, + borderWidth: 1, + borderColor: C.jobStarted + "44", + }, + estimateText: { + fontSize: 14, + fontFamily: "Inter_500Medium", + color: C.jobStarted, + flex: 1, + }, + errorCard: { + flexDirection: "row", + alignItems: "flex-start", + gap: 8, + backgroundColor: C.error + "18", + borderRadius: 10, + padding: 12, + borderWidth: 1, + borderColor: C.error + "44", + }, + errorCardText: { + fontSize: 13, + fontFamily: "Inter_400Regular", + color: C.error, + flex: 1, + }, + buttonRow: { + flexDirection: "row", + gap: 12, + }, + estimateButton: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 6, + backgroundColor: C.surfaceElevated, + borderRadius: 12, + paddingVertical: 14, + borderWidth: 1, + borderColor: C.accent + "44", + }, + estimateButtonText: { + fontSize: 15, + fontFamily: "Inter_600SemiBold", + color: C.accent, + }, + submitButton: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 6, + backgroundColor: C.accent, + borderRadius: 12, + paddingVertical: 14, + }, + submitButtonText: { + fontSize: 15, + fontFamily: "Inter_600SemiBold", + color: "#fff", + }, + buttonDisabled: { + opacity: 0.5, + }, + statusRow: { + flexDirection: "row", + alignItems: "center", + gap: 8, + backgroundColor: C.surfaceElevated, + borderRadius: 10, + padding: 12, + borderWidth: 1, + borderColor: C.border, + }, + statusText: { + flex: 1, + fontSize: 14, + fontFamily: "Inter_500Medium", + color: C.text, + }, + qrContainer: { + alignItems: "center", + gap: 12, + }, + qrLabel: { + fontSize: 14, + fontFamily: "Inter_500Medium", + color: C.textSecondary, + }, + qrWrapper: { + backgroundColor: "#FFFFFF", + padding: 16, + borderRadius: 12, + }, + satsLabel: { + fontSize: 20, + fontFamily: "Inter_700Bold", + color: C.jobStarted, + }, + invoiceHint: { + fontSize: 12, + fontFamily: "Inter_400Regular", + color: C.textMuted, + }, + resultCard: { + backgroundColor: C.surfaceElevated, + borderRadius: 12, + padding: 14, + borderWidth: 1, + borderColor: C.jobCompleted + "44", + gap: 10, + }, + resultHeader: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + resultTitle: { + flex: 1, + fontSize: 15, + fontFamily: "Inter_600SemiBold", + color: C.text, + }, + resultText: { + fontSize: 14, + fontFamily: "Inter_400Regular", + color: C.text, + lineHeight: 20, + }, + newJobButton: { + alignItems: "center", + paddingVertical: 14, + backgroundColor: C.surfaceElevated, + borderRadius: 12, + borderWidth: 1, + borderColor: C.border, + }, + newJobButtonText: { + fontSize: 15, + fontFamily: "Inter_500Medium", + color: C.accent, + }, +}); diff --git a/artifacts/mobile/package.json b/artifacts/mobile/package.json index c1bd9ae..397d016 100644 --- a/artifacts/mobile/package.json +++ b/artifacts/mobile/package.json @@ -58,6 +58,7 @@ "dependencies": { "@react-native-voice/voice": "^3.2.4", "expo-speech": "^14.0.8", + "react-native-qrcode-svg": "^6.3.21", "react-native-webview": "^13.15.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1007a83..eda8996 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,6 +233,9 @@ importers: expo-speech: specifier: ^14.0.8 version: 14.0.8(expo@54.0.33) + react-native-qrcode-svg: + specifier: ^6.3.21 + version: 6.3.21(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-webview: specifier: ^13.15.0 version: 13.15.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -3024,6 +3027,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3241,6 +3247,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -3289,6 +3299,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -4960,6 +4973,10 @@ packages: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -5052,6 +5069,11 @@ packages: resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} hasBin: true + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -5139,6 +5161,13 @@ packages: react-native: '*' react-native-reanimated: '>=3.0.0' + react-native-qrcode-svg@6.3.21: + resolution: {integrity: sha512-6vcj4rcdpWedvphDR+NSJcudJykNuLgNGFwm2p4xYjR8RdyTzlrELKI5LkO4ANS9cQUbqsfkpippPv64Q2tUtA==} + peerDependencies: + react: '*' + react-native: '>=0.63.4' + react-native-svg: '>=14.0.0' + react-native-reanimated@4.1.6: resolution: {integrity: sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==} peerDependencies: @@ -5305,6 +5334,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requireg@0.2.2: resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} engines: {node: '>= 4.0.0'} @@ -5421,6 +5453,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -5636,6 +5671,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-encoding@0.7.0: + resolution: {integrity: sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==} + deprecated: no longer maintained + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -5943,6 +5982,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5955,6 +5997,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6036,6 +6082,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6056,10 +6105,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -9313,6 +9370,12 @@ snapshots: client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -9517,6 +9580,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} decode-uri-component@0.2.2: {} @@ -9547,6 +9612,8 @@ snapshots: detect-node-es@1.1.0: {} + dijkstrajs@1.0.3: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.6 @@ -11432,6 +11499,8 @@ snapshots: pngjs@3.4.0: {} + pngjs@5.0.0: {} + postcss-value-parser@4.2.0: {} postcss@8.4.49: @@ -11526,6 +11595,12 @@ snapshots: qrcode-terminal@0.11.0: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -11618,6 +11693,15 @@ snapshots: react-native-is-edge-to-edge: 1.3.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.6(@babel/core@7.29.0)(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-qrcode-svg@6.3.21(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + prop-types: 15.8.1 + qrcode: 1.5.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + react-native-svg: 15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + text-encoding: 0.7.0 + react-native-reanimated@4.1.6(@babel/core@7.29.0)(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: '@babel/core': 7.29.0 @@ -11868,6 +11952,8 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + requireg@0.2.2: dependencies: nested-error-stacks: 2.0.1 @@ -12004,6 +12090,8 @@ snapshots: server-only@0.0.1: {} + set-blocking@2.0.0: {} + setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -12199,6 +12287,8 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + text-encoding@0.7.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -12465,6 +12555,8 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -12473,6 +12565,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -12525,6 +12623,8 @@ snapshots: xtend@4.0.2: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -12535,8 +12635,27 @@ snapshots: yaml@2.8.2: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1