[claude] Mobile: Paid job submission with inline Lightning invoice (#25) #88

Merged
Rockachopa merged 1 commits from claude/issue-25 into main 2026-03-23 20:20:53 +00:00
4 changed files with 916 additions and 20 deletions

View File

@@ -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<Animated.CompositeAnimation | null>(null);
const webRecognitionRef = useRef<WebSpeechRecognition | null>(null);
@@ -273,31 +275,48 @@ export default function FaceScreen() {
</View>
) : null}
{/* Mic button */}
{/* Action buttons */}
<View style={[styles.micArea, { paddingBottom: bottomPad }]}>
<Pressable
onPress={handleMicPress}
accessibilityRole="button"
accessibilityLabel={isListening ? "Stop listening" : "Start voice"}
>
<Animated.View
style={[
styles.micButton,
isListening && styles.micButtonActive,
{ transform: [{ scale: micScale }] },
]}
<View style={styles.actionRow}>
<Pressable
onPress={handleMicPress}
accessibilityRole="button"
accessibilityLabel={isListening ? "Stop listening" : "Start voice"}
>
<Ionicons
name={isListening ? "mic" : "mic-outline"}
size={32}
color={isListening ? "#fff" : C.textSecondary}
/>
</Animated.View>
</Pressable>
<Animated.View
style={[
styles.micButton,
isListening && styles.micButtonActive,
{ transform: [{ scale: micScale }] },
]}
>
<Ionicons
name={isListening ? "mic" : "mic-outline"}
size={32}
color={isListening ? "#fff" : C.textSecondary}
/>
</Animated.View>
</Pressable>
<Pressable
onPress={() => setJobSheetVisible(true)}
accessibilityRole="button"
accessibilityLabel="Submit paid job"
>
<View style={styles.jobButton}>
<Ionicons name="flash" size={26} color={C.jobStarted} />
</View>
</Pressable>
</View>
<Text style={styles.micHint}>
{isListening ? "Listening..." : "Tap to speak to Timmy"}
{isListening ? "Listening..." : "Tap mic to speak \u00B7 bolt to submit a job"}
</Text>
</View>
<JobSubmissionSheet
visible={jobSheetVisible}
onClose={() => setJobSheetVisible(false)}
/>
</View>
);
}
@@ -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,

View File

@@ -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<string, string> = {
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<string, string> = {
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<EstimateResponse | null>(null);
const [estimateLoading, setEstimateLoading] = useState(false);
const [estimateError, setEstimateError] = useState("");
const [jobId, setJobId] = useState<string | null>(null);
const [jobStatus, setJobStatus] = useState<JobStatus | null>(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<ReturnType<typeof setInterval> | 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 (
<ScrollView
style={styles.sheetScroll}
contentContainerStyle={styles.sheetScrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Status badge */}
<View style={styles.statusRow}>
<Ionicons
name={
(STATE_ICONS[jobStatus.state] ?? "help-outline") as React.ComponentProps<typeof Ionicons>["name"]
}
size={20}
color={
jobStatus.state === "complete"
? C.jobCompleted
: jobStatus.state === "failed" || jobStatus.state === "rejected"
? C.error
: C.jobStarted
}
/>
<Text
style={[
styles.statusText,
jobStatus.state === "complete" && { color: C.jobCompleted },
(jobStatus.state === "failed" ||
jobStatus.state === "rejected") && { color: C.error },
]}
>
{STATE_LABELS[jobStatus.state] ?? jobStatus.state}
</Text>
{isPolling && (
<ActivityIndicator size="small" color={C.accent} />
)}
</View>
{/* Invoice QR */}
{activeInvoice && (
<View style={styles.qrContainer}>
<Text style={styles.qrLabel}>
{jobStatus.state === "awaiting_eval_payment"
? "Pay eval invoice"
: "Pay work invoice"}
</Text>
<View style={styles.qrWrapper}>
<QRCode
value={activeInvoice.paymentRequest}
size={QR_SIZE}
backgroundColor="#FFFFFF"
color="#000000"
/>
</View>
<Text style={styles.satsLabel}>
{activeInvoice.amountSats} sats
</Text>
<Text style={styles.invoiceHint}>
Scan with your Lightning wallet
</Text>
</View>
)}
{/* Rejection reason */}
{jobStatus.state === "rejected" && jobStatus.reason && (
<View style={styles.errorCard}>
<Ionicons name="close-circle" size={16} color={C.error} />
<Text style={styles.errorCardText}>{jobStatus.reason}</Text>
</View>
)}
{/* Error message */}
{jobStatus.state === "failed" && jobStatus.errorMessage && (
<View style={styles.errorCard}>
<Ionicons name="alert-circle" size={16} color={C.error} />
<Text style={styles.errorCardText}>
{jobStatus.errorMessage}
</Text>
</View>
)}
{/* Result */}
{jobStatus.state === "complete" && jobStatus.result && (
<Pressable
style={styles.resultCard}
onPress={() => setResultExpanded((v) => !v)}
>
<View style={styles.resultHeader}>
<Ionicons
name="checkmark-circle"
size={18}
color={C.jobCompleted}
/>
<Text style={styles.resultTitle}>Result</Text>
<Ionicons
name={resultExpanded ? "chevron-up" : "chevron-down"}
size={16}
color={C.textSecondary}
/>
</View>
<Text
style={styles.resultText}
numberOfLines={resultExpanded ? undefined : 6}
>
{jobStatus.result}
</Text>
</Pressable>
)}
{/* New job button for terminal states */}
{isTerminal && (
<Pressable style={styles.newJobButton} onPress={resetState}>
<Text style={styles.newJobButtonText}>Submit another job</Text>
</Pressable>
)}
</ScrollView>
);
}
// === INPUT FORM ===
return (
<ScrollView
style={styles.sheetScroll}
contentContainerStyle={styles.sheetScrollContent}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.inputLabel}>What should Timmy do?</Text>
<TextInput
style={styles.textInput}
placeholder="Describe your job..."
placeholderTextColor={C.textMuted}
value={prompt}
onChangeText={setPrompt}
multiline
numberOfLines={4}
textAlignVertical="top"
maxLength={2000}
autoFocus
/>
{/* Estimate result */}
{estimate && (
<View style={styles.estimateCard}>
<Ionicons name="flash" size={16} color={C.jobStarted} />
<Text style={styles.estimateText}>
Estimated cost: {estimate.estimatedCostSats} sats (~$
{estimate.estimatedCostUsd.toFixed(4)})
</Text>
</View>
)}
{estimateError ? (
<View style={styles.errorCard}>
<Ionicons name="alert-circle" size={16} color={C.error} />
<Text style={styles.errorCardText}>{estimateError}</Text>
</View>
) : null}
{submitError ? (
<View style={styles.errorCard}>
<Ionicons name="alert-circle" size={16} color={C.error} />
<Text style={styles.errorCardText}>{submitError}</Text>
</View>
) : null}
{/* Action buttons */}
<View style={styles.buttonRow}>
<Pressable
style={[
styles.estimateButton,
(!prompt.trim() || estimateLoading) && styles.buttonDisabled,
]}
onPress={handleEstimate}
disabled={!prompt.trim() || estimateLoading}
>
{estimateLoading ? (
<ActivityIndicator size="small" color={C.accent} />
) : (
<>
<Ionicons name="calculator-outline" size={18} color={C.accent} />
<Text style={styles.estimateButtonText}>Estimate</Text>
</>
)}
</Pressable>
<Pressable
style={[
styles.submitButton,
(!prompt.trim() || submitting) && styles.buttonDisabled,
]}
onPress={handleSubmit}
disabled={!prompt.trim() || submitting}
>
{submitting ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons name="flash" size={18} color="#fff" />
<Text style={styles.submitButtonText}>Submit Job</Text>
</>
)}
</Pressable>
</View>
</ScrollView>
);
};
return (
<Modal
visible={visible}
animationType="slide"
transparent
onRequestClose={handleClose}
>
<View style={styles.overlay}>
<Pressable style={styles.overlayBackdrop} onPress={handleClose} />
<Animated.View
style={[
styles.sheet,
{
paddingBottom: Math.max(insets.bottom, 16),
transform: [
{
translateY: slideAnim.interpolate({
inputRange: [0, 1],
outputRange: [600, 0],
}),
},
],
},
]}
>
{/* Handle bar */}
<View style={styles.handleRow}>
<View style={styles.handle} />
</View>
{/* Header */}
<View style={styles.sheetHeader}>
<Ionicons name="flash" size={22} color={C.jobStarted} />
<Text style={styles.sheetTitle}>Submit Job</Text>
<Pressable
onPress={handleClose}
hitSlop={12}
accessibilityRole="button"
accessibilityLabel="Close"
>
<Ionicons name="close" size={24} color={C.textSecondary} />
</Pressable>
</View>
{renderContent()}
</Animated.View>
</View>
</Modal>
);
}
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,
},
});

View File

@@ -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"
}
}

119
pnpm-lock.yaml generated
View File

@@ -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