Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions components/molecules/ScanCameraView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { CameraView, type BarcodeScanningResult } from "expo-camera";
import { useTranslation } from "react-i18next";
import NavBar from "@/components/molecules/NavBar";
import ScanFrame from "@/components/molecules/ScanFrame";
import { SCANNABLE_BARCODE_TYPES } from "@/constants/scan";
import { useAppTheme } from "@/hooks/use-app-theme";

Check failure on line 8 in components/molecules/ScanCameraView.tsx

View workflow job for this annotation

GitHub Actions / Lint code

Unable to resolve path to module '@/hooks/use-app-theme'
import type { ScannedBarcode } from "@/types/scan";

type ScanCameraViewProps = {
scannedBarcode: ScannedBarcode | null;
onBarcodeScanned: (result: BarcodeScanningResult) => void;
onScanAgain: () => void;
};

export default function ScanCameraView({
scannedBarcode,
onBarcodeScanned,
}: ScanCameraViewProps) {
const { t } = useTranslation();
const { colors } = useAppTheme();

return (
<View className="flex-1" style={{ backgroundColor: colors.shell }}>
<CameraView
className="absolute inset-0"
barcodeScannerSettings={{ barcodeTypes: SCANNABLE_BARCODE_TYPES }}
onBarcodeScanned={scannedBarcode ? undefined : onBarcodeScanned}
/>

<View
className="absolute inset-0"
style={{ backgroundColor: colors.cameraOverlay }}
/>

<SafeAreaView className="flex-1" edges={["top"]}>
<View className="flex-1 px-5 pt-4">
<View className="flex-1 items-center justify-center">
{!scannedBarcode ? (
<>
<ScanFrame />
<View
className="mt-3 rounded-full px-4 py-3"
style={{ backgroundColor: colors.scanHintBackground }}
>
<Text
className="font-sans-medium text-sm"
style={{ color: colors.textOnOverlay }}
>
{t("scan.placeBarcode")}
</Text>
</View>
</>
) : null}
</View>
</View>
</SafeAreaView>

<NavBar activeTab="scan" />
</View>
);
}
31 changes: 31 additions & 0 deletions components/molecules/ScanFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { View } from "react-native";
import { useAppTheme } from "@/hooks/use-app-theme";

Check failure on line 2 in components/molecules/ScanFrame.tsx

View workflow job for this annotation

GitHub Actions / Lint code

Unable to resolve path to module '@/hooks/use-app-theme'

export default function ScanFrame() {
const { colors } = useAppTheme();

return (
<View className="relative h-72 w-full max-w-xs items-center justify-center">
<View
className="absolute left-8 top-10 h-12 w-12 rounded-tl-[22px] border-l-[3px] border-t-[3px]"
style={{ borderColor: colors.scanFrameBorder }}
/>
<View
className="absolute right-8 top-10 h-12 w-12 rounded-tr-[22px] border-r-[3px] border-t-[3px]"
style={{ borderColor: colors.scanFrameBorder }}
/>
<View
className="absolute bottom-10 left-8 h-12 w-12 rounded-bl-[22px] border-b-[3px] border-l-[3px]"
style={{ borderColor: colors.scanFrameBorder }}
/>
<View
className="absolute bottom-10 right-8 h-12 w-12 rounded-br-[22px] border-b-[3px] border-r-[3px]"
style={{ borderColor: colors.scanFrameBorder }}
/>
<View
className="absolute left-12 right-12 top-1/2 h-px"
style={{ backgroundColor: colors.scanGuide }}
/>
</View>
);
}
76 changes: 76 additions & 0 deletions components/molecules/ScanPermissionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Text, View } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next";
import AppScreen from "@/components/layouts/AppScreen";

Check failure on line 4 in components/molecules/ScanPermissionCard.tsx

View workflow job for this annotation

GitHub Actions / Lint code

Unable to resolve path to module '@/components/layouts/AppScreen'
import PrimaryButton from "@/components/atoms/PrimaryButton";
import { useAppTheme } from "@/hooks/use-app-theme";

Check failure on line 6 in components/molecules/ScanPermissionCard.tsx

View workflow job for this annotation

GitHub Actions / Lint code

Unable to resolve path to module '@/hooks/use-app-theme'

type ScanPermissionCardProps = {
title: string;
detail?: string;
actionLabel?: string;
onPress?: () => void;
};

export default function ScanPermissionCard({
title,
detail,
actionLabel,
onPress,
}: ScanPermissionCardProps) {
const { t } = useTranslation();
const { colors, shadows } = useAppTheme();

return (
<AppScreen activeTab="scan" contentClassName="justify-center">
<View
className="rounded-[38px] border p-6"
style={[
shadows.floating,
{ borderColor: colors.border, backgroundColor: colors.panelSoft },
]}
>
<View
className="h-14 w-14 items-center justify-center rounded-full"
style={{ backgroundColor: colors.panelMuted }}
>
<Ionicons name="scan-outline" size={24} color={colors.primary} />
</View>

<Text
className="mt-6 font-sans-medium text-[11px] uppercase tracking-[2px]"
style={{ color: colors.primary }}
>
{t("nav.scan")}
</Text>
<Text
className="mt-3 font-lora text-[34px] leading-10"
style={{ color: colors.text }}
>
{t("scan.title")}
</Text>
<Text
className="mt-3 font-sans text-base leading-6"
style={{ color: colors.textMuted }}
>
{title}
</Text>

{detail ? (
<Text
className="mt-4 font-sans text-sm leading-6"
style={{ color: colors.error }}
>
{detail}
</Text>
) : null}

{actionLabel && onPress ? (
<View className="mt-6">
<PrimaryButton title={actionLabel} onPress={onPress} />
</View>
) : null}
</View>
</AppScreen>
);
}
10 changes: 10 additions & 0 deletions constants/scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { BarcodeType } from "expo-camera";

export const SCANNABLE_BARCODE_TYPES: BarcodeType[] = [
"ean13",
"ean8",
"upc_a",
"upc_e",
"code128",
"itf14",
];
91 changes: 91 additions & 0 deletions hooks/scan/useScanController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useEffect, useRef, useState } from "react";
import { Linking, Platform } from "react-native";
import { type BarcodeScanningResult, useCameraPermissions } from "expo-camera";
import { useTranslation } from "react-i18next";
import type { ScannedBarcode, ScanPermissionState } from "@/types/scan";

function getPermissionState(
permission: ReturnType<typeof useCameraPermissions>[0],
): ScanPermissionState {
if (!permission) {
return "checking";
}

if (permission.granted) {
return "granted";
}

return permission.canAskAgain ? "requestable" : "blocked";
}

export function useScanController() {
const { t } = useTranslation();
const [permission, requestPermission] = useCameraPermissions();
const [scannedBarcode, setScannedBarcode] = useState<ScannedBarcode | null>(
null,
);
const [settingsError, setSettingsError] = useState<string | null>(null);
const scanLockedRef = useRef(false);
const permissionState = getPermissionState(permission);
const canOpenSettings = Platform.OS !== "web";

useEffect(() => {
if (permission?.status === "undetermined") {
void requestPermission();
}
}, [permission?.status, requestPermission]);

const handleRequestPermission = () => {
setSettingsError(null);
void requestPermission();
};

const handleOpenSettings = async () => {
setSettingsError(null);

if (!canOpenSettings) {
setSettingsError(t("scan.settingsUnavailable"));
return;
}

try {
await Linking.openSettings();
} catch {
setSettingsError(t("scan.settingsUnavailable"));
}
};

const handleBarcodeScanned = (result: BarcodeScanningResult) => {
if (scanLockedRef.current) {
return;
}

const data = result.data?.trim();

if (!data) {
return;
}

scanLockedRef.current = true;
setScannedBarcode({
data,
type: result.type,
});
};

const handleScanAgain = () => {
scanLockedRef.current = false;
setScannedBarcode(null);
};

return {
canOpenSettings,
handleBarcodeScanned,
handleOpenSettings,
handleRequestPermission,
handleScanAgain,
permissionState,
scannedBarcode,
settingsError,
};
}
9 changes: 9 additions & 0 deletions types/scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { BarcodeScanningResult } from "expo-camera";

export type ScanPermissionState =
| "checking"
| "requestable"
| "blocked"
| "granted";

export type ScannedBarcode = Pick<BarcodeScanningResult, "data" | "type">;
Loading