This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/app/(root)/(screens)/prescription/scan.tsx
kirukib 7310a349c5 Add Gishen mobile app with complete demo shopping and care flows.
Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 01:21:32 +03:00

197 lines
6.4 KiB
TypeScript

import { CameraView, useCameraPermissions } from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import { router } from "expo-router";
import { useRef, useState } from "react";
import { Alert, Image, Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Screen } from "@/components/ui/Screen";
import { Text } from "@/components/ui/Text";
import { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
import { colors, gutter } from "@/lib/theme";
import { routes } from "@/lib/routes";
export default function RxScanScreen() {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const [permission, requestPermission] = useCameraPermissions();
const cameraRef = useRef<CameraView>(null);
const draftImages = usePrescriptionStore((s) => s.draftImages);
const setDraftImages = usePrescriptionStore((s) => s.setDraftImages);
const [ready, setReady] = useState(false);
const capture = async () => {
const photo = await cameraRef.current?.takePictureAsync({ quality: 0.7 });
if (photo?.uri) setDraftImages([...draftImages, photo.uri]);
};
const pick = async () => {
const res = await ImagePicker.launchImageLibraryAsync({
allowsMultipleSelection: true,
quality: 0.7,
});
if (!res.canceled) {
setDraftImages([...draftImages, ...res.assets.map((a) => a.uri)]);
}
};
const removeAt = (uri: string) => {
setDraftImages(draftImages.filter((u) => u !== uri));
};
if (!permission?.granted) {
return (
<Screen>
<View
style={{
flex: 1,
paddingTop: insets.top + 24,
paddingHorizontal: gutter,
justifyContent: "center",
gap: 14,
}}
>
<Text variant="h1">Scan prescription</Text>
<Text variant="body" color={colors.muted}>
Allow camera access to photograph your prescription, or upload existing photos.
</Text>
<Card elevated={false}>
<Text variant="bodySm" color={colors.muted}>
Tips: place the Rx on a flat surface, include the full page, and avoid glare.
</Text>
</Card>
<Button title="Grant camera permission" onPress={requestPermission} />
<Button title="Upload from photos" variant="outline" onPress={pick} />
{draftImages.length ? (
<Button
title={`Continue with ${draftImages.length} photo${draftImages.length === 1 ? "" : "s"}`}
variant="soft"
onPress={() => router.push(routes.rxReview)}
/>
) : null}
<Button title="Cancel" variant="ghost" onPress={() => router.back()} />
</View>
</Screen>
);
}
return (
<View style={{ flex: 1, backgroundColor: "#000" }}>
<CameraView
ref={cameraRef}
style={{ flex: 1 }}
facing="back"
onCameraReady={() => setReady(true)}
>
<View
style={{
position: "absolute",
top: insets.top + 8,
left: 16,
right: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Pressable onPress={() => router.back()} hitSlop={8}>
<Ionicons name="close" size={28} color="#fff" />
</Pressable>
<Text variant="label" color="#fff">
{t("common.scanRx")}
</Text>
<Text variant="caption" color="rgba(255,255,255,0.75)">
{draftImages.length} saved
</Text>
</View>
<View
style={{
position: "absolute",
top: "20%",
left: "8%",
right: "8%",
bottom: "30%",
borderWidth: 2,
borderColor: colors.brand,
borderRadius: 16,
}}
/>
<Text
variant="caption"
color="rgba(255,255,255,0.85)"
center
style={{ position: "absolute", top: "16%", alignSelf: "center", width: "80%" }}
>
Align the full prescription inside the frame
</Text>
<View
style={{
position: "absolute",
bottom: 36,
left: 0,
right: 0,
alignItems: "center",
gap: 12,
}}
>
<ScrollView horizontal contentContainerStyle={{ gap: 8, paddingHorizontal: 16 }}>
{draftImages.map((uri) => (
<Pressable key={uri} onLongPress={() => removeAt(uri)}>
<Image
source={{ uri }}
style={{ width: 56, height: 56, borderRadius: 8 }}
/>
</Pressable>
))}
</ScrollView>
{draftImages.length ? (
<Text variant="micro" color="rgba(255,255,255,0.7)">
Long-press a thumbnail to remove
</Text>
) : null}
<View style={{ flexDirection: "row", gap: 24, alignItems: "center" }}>
<Pressable onPress={pick} accessibilityLabel="Upload photos">
<Ionicons name="images" size={28} color="#fff" />
</Pressable>
<Pressable
disabled={!ready}
onPress={capture}
accessibilityLabel="Capture"
style={{
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: colors.brand,
borderWidth: 4,
borderColor: "#fff",
opacity: ready ? 1 : 0.5,
}}
/>
<Pressable
onPress={() => {
if (!draftImages.length) {
Alert.alert("Add a photo", "Capture or upload at least one image first.");
return;
}
router.push(routes.rxReview);
}}
accessibilityLabel="Continue to review"
>
<Ionicons
name="checkmark-circle"
size={34}
color={draftImages.length ? colors.brand : "#666"}
/>
</Pressable>
</View>
</View>
</CameraView>
</View>
);
}