Amba-Agent-App/app/(root)/(screens)/qrscreen.tsx
2026-01-16 00:22:35 +03:00

365 lines
12 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import {
View,
Text,
TouchableOpacity,
Share,
ActivityIndicator,
Platform,
} from "react-native";
import { ArrowLeft } from "lucide-react-native";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import { Button } from "~/components/ui/button";
import { router } from "expo-router";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
import QRCode from "react-native-qrcode-svg";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { UserQrService } from "~/lib/services/userQrService";
import {
CameraView,
useCameraPermissions,
type BarcodeScanningResult,
} from "expo-camera";
import { ROUTES } from "~/lib/routes";
import BackButton from "~/components/ui/backButton";
export default function QRScreen() {
const { t } = useTranslation();
const { user, profile, wallet } = useAuthWithProfile();
const [activeTab, setActiveTab] = useState<"scan" | "my">("scan");
const [toastVisible, setToastVisible] = useState(false);
const [toastTitle, setToastTitle] = useState("");
const [toastDescription, setToastDescription] = useState<string | undefined>(
undefined
);
const [toastVariant, setToastVariant] = useState<
"success" | "error" | "warning" | "info"
>("info");
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Scanner state
const [hasPermission, setHasPermission] = useState<null | boolean>(null);
const [scanned, setScanned] = useState(false);
const [permission, requestPermission] = useCameraPermissions();
const displayName =
profile?.fullName || user?.displayName || t("profile.usernamePlaceholder");
const phoneNumber = profile?.phoneNumber || (user as any)?.phoneNumber || "";
const accountId = wallet?.uid || user?.uid || "";
const [qrPayload, setQrPayload] = useState<string | null>(null);
const showToast = (
title: string,
description?: string,
variant: "success" | "error" | "warning" | "info" = "info"
) => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
setToastTitle(title);
setToastDescription(description);
setToastVariant(variant);
setToastVisible(true);
toastTimeoutRef.current = setTimeout(() => {
setToastVisible(false);
toastTimeoutRef.current = null;
}, 2500);
};
useEffect(() => {
return () => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
// Sync camera permission state for Scan tab
useEffect(() => {
if (Platform.OS === "web") {
setHasPermission(false);
return;
}
if (!permission) {
return;
}
setHasPermission(permission.granted);
}, [permission]);
useEffect(() => {
let isMounted = true;
const loadQr = async () => {
if (!user?.uid) return;
try {
const payload = await UserQrService.getOrCreateProfileQr({
uid: user.uid,
accountId,
name: displayName,
phoneNumber,
});
if (isMounted) {
setQrPayload(payload);
}
} catch (error) {
console.error("[QRScreen] Failed to load/create profile QR", error);
}
};
loadQr();
return () => {
isMounted = false;
};
}, [user?.uid, accountId, displayName, phoneNumber]);
const handleBarCodeScanned = ({ data }: BarcodeScanningResult) => {
if (scanned) return;
try {
setScanned(true);
const parsed = JSON.parse(data);
if (!parsed || parsed.type !== "AMBA_PROFILE") {
showToast(
"Invalid QR",
"This is not a valid Amba profile QR.",
"error"
);
setScanned(false);
return;
}
const accountIdFromQr: string | undefined = parsed.accountId;
const nameFromQr: string | undefined = parsed.name;
const phoneNumberFromQr: string | undefined = parsed.phoneNumber;
if (!phoneNumberFromQr) {
showToast(
"Invalid QR",
"This profile QR does not contain a phone number.",
"error"
);
setScanned(false);
return;
}
router.push({
pathname: ROUTES.SEND_OR_REQUEST_MONEY,
params: {
selectedContactId: accountIdFromQr || phoneNumberFromQr,
selectedContactName: nameFromQr || "User",
selectedContactPhone: phoneNumberFromQr,
},
});
} catch (error) {
console.warn("[QRScreen] Failed to parse QR payload", error);
showToast("Scan failed", "Could not read this QR code.", "error");
setScanned(false);
}
};
const handleShare = async () => {
try {
await Share.share({
message: t("qrscreen.shareMessage"),
});
} catch (error) {
console.log("Error sharing QR:", error);
showToast(
t("qrscreen.toastErrorTitle"),
t("qrscreen.toastShareError"),
"error"
);
}
};
const handleClose = () => {
router.back();
};
return (
<ScreenWrapper edges={[]}>
<View className="flex-1 bg-white">
{/* Top back button */}
<View className="flex-row justify-start">
<BackButton />
</View>
{/* Tabs - match KYC style */}
<View className="flex-row mt-5 mb-4 mx-6 bg-[#F3F4F6] rounded-full p-1">
<TouchableOpacity
onPress={() => {
setActiveTab("scan");
setScanned(false);
}}
className={`flex-1 items-center py-2 rounded-full ${
activeTab === "scan" ? "bg-white" : "bg-transparent"
}`}
activeOpacity={0.8}
>
<Text
className={`text-base font-dmsans-medium ${
activeTab === "scan" ? "text-[#0F7B4A]" : "text-gray-500"
}`}
>
{t("qrscreen.scanTabLabel", "Scan QR")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setActiveTab("my")}
className={`flex-1 items-center py-2 rounded-full ${
activeTab === "my" ? "bg-white" : "bg-transparent"
}`}
activeOpacity={0.8}
>
<Text
className={`text-base font-dmsans-medium ${
activeTab === "my" ? "text-[#0F7B4A]" : "text-gray-500"
}`}
>
{t("qrscreen.myTabLabel", "My QR")}
</Text>
</TouchableOpacity>
</View>
{/* Tab content */}
{activeTab === "my" ? (
<>
<View className="flex-1 items-center justify-center px-5">
{qrPayload ? (
<QRCode
value={qrPayload}
size={260}
color="#0F7B4A"
backgroundColor="white"
/>
) : (
<View className="items-center justify-center">
<ActivityIndicator size="large" color="#0F7B4A" />
<View className="h-3" />
<Text className="text-sm font-dmsans text-gray-600">
{t("qrscreen.loadingLabel", "Preparing your QR code...")}
</Text>
</View>
)}
</View>
<View className="w-full flex flex-col px-5 pb-10 gap-4">
<Button
className="h-13 pb-6 rounded-full bg-[#FFB668]"
onPress={handleShare}
>
<Text className="text-white font-dmsans-bold text-base">
{t("qrscreen.shareButton")}
</Text>
</Button>
<Button
className="h-13 rounded-full bg-[#0F7B4A]"
onPress={handleClose}
>
<Text className="text-white font-dmsans-bold text-base">
{t("qrscreen.goBackButton")}
</Text>
</Button>
</View>
</>
) : (
<View className="flex-1 items-center justify-center px-5">
{Platform.OS === "web" ? (
<View className="flex-1 items-center justify-center px-5">
<Text className="text-base font-dmsans text-gray-600 text-center">
QR scanning is not supported on web. Please use a mobile
device.
</Text>
</View>
) : hasPermission === null ? (
<View className="flex-1 items-center justify-center px-5">
<ActivityIndicator size="large" color="#0F7B4A" />
<View className="h-4" />
<Text className="text-base font-dmsans text-gray-600">
Requesting camera permission...
</Text>
</View>
) : hasPermission === false ? (
<View className="flex-1 items-center justify-center px-5">
<Text className="text-base font-dmsans text-gray-800 mb-2 text-center">
Camera permission needed
</Text>
<Text className="text-sm font-dmsans text-gray-500 mb-4 text-center">
Please grant camera access to scan profile QR codes.
</Text>
<Button
className="rounded-full bg-primary px-8"
onPress={async () => {
try {
const result = await requestPermission();
if (result) {
setHasPermission(result.granted);
}
} catch (error) {
console.warn(
"[QRScreen] Failed to re-request camera permission",
error
);
showToast(
"Permission error",
"Could not update camera permission.",
"error"
);
}
}}
>
<Text className="text-white font-dmsans-medium">
Grant permission
</Text>
</Button>
</View>
) : (
<View className="flex-1 items-center justify-center px-5">
<View
style={{ overflow: "hidden", borderRadius: 24 }}
className="w-full aspect-square max-w-[320px] bg-black"
>
<CameraView
style={{ width: "100%", height: "100%" }}
barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
onBarcodeScanned={
scanned ? undefined : handleBarCodeScanned
}
active={true}
/>
</View>
<View className="h-6" />
<Text className="text-base font-dmsans text-gray-800 mb-1 text-center">
Scan profile QR code
</Text>
<Text className="text-sm font-dmsans text-gray-500 text-center px-4">
Align the QR code inside the frame. We will automatically
select the account and take you to the amount screen.
</Text>
</View>
)}
</View>
)}
</View>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}