2 Commits

Author SHA1 Message Date
edb3668548 refactor(scanner): added toasts in to make it look better
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m25s
2026-05-06 19:42:52 -05:00
87803eed43 feat(scanner): added in lanechecks 2026-05-06 19:42:22 -05:00
16 changed files with 3569 additions and 2511 deletions

View File

@@ -1,7 +1,5 @@
import { Router } from "express"; import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js"; import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router(); const router = Router();
@@ -9,27 +7,26 @@ const router = Router();
router.post("/", async (req, res) => { router.post("/", async (req, res) => {
const body = req.body; const body = req.body;
const newLog = await db const lane = body.lane.split("#");
.insert(scanLog)
.values({ console.log(lane[2]);
scannerId: body.scannerId, const laneData = await runProdApi({
message: body.message, method: "post",
prompt: body.prompt, endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
commandDescription: body.commandDescription, data: [
status: body.status, {
lines: body.lines, laneIds: [lane[2]],
user: body.user, },
runningNumber: body.runningNumber, ],
}) });
.returning();
return apiReturn(res, { return apiReturn(res, {
success: true, success: true,
level: "info", level: "info",
module: "mobile", module: "mobile",
subModule: "scan logs", subModule: "lane check",
message: `New log from ${body.scannerId}`, message: `all data for lane Id: ${lane}`,
data: newLog, data: laneData?.data ?? [],
status: 200, status: 200,
}); });
}); });

View File

@@ -1,5 +1,6 @@
import type { Express } from "express"; import type { Express } from "express";
import downloads from "./donwloadApps.route.js"; import downloads from "./donwloadApps.route.js";
import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js"; import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js"; import newPin from "./mobilePin.route.js";
import logs from "./scanLogs.route.js"; import logs from "./scanLogs.route.js";
@@ -12,6 +13,7 @@ export const setupMobileRoutes = (baseUrl: string, app: Express) => {
app.use(`${baseUrl}/api/mobile/logs`, logs); app.use(`${baseUrl}/api/mobile/logs`, logs);
app.use(`${baseUrl}/api/mobile/auth`, authPin); app.use(`${baseUrl}/api/mobile/auth`, authPin);
app.use(`${baseUrl}/api/mobile/pin`, newPin); app.use(`${baseUrl}/api/mobile/pin`, newPin);
app.use(`${baseUrl}/api/mobile/laneCheck`, lanes);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -15,7 +15,7 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 30, "versionCode": 31,
"minSupportedVersionCode": 26, "minSupportedVersionCode": 26,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile"

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"ios": "expo run:ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"lint": "expo lint", "lint": "expo lint",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat clean && gradlew.bat assembleRelease && npm run copy:apk", "build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk", "build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:mobile": "cd scripts && node runBuild.ts", "build:mobile": "cd scripts && node runBuild.ts",
"build:mobile:bump": "cd scripts && node runBuild.ts --bump", "build:mobile:bump": "cd scripts && node runBuild.ts --bump",
@@ -22,6 +22,7 @@
"@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10", "@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33", "@react-navigation/native": "^7.1.33",
"@rn-primitives/dialog": "^1.4.0",
"@rn-primitives/portal": "^1.4.0", "@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0", "@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0", "@rn-primitives/slot": "^1.4.0",
@@ -56,10 +57,11 @@
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "^4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0", "react-native-screens": "~4.23.0",
"react-native-tcp-socket": "^6.4.1", "react-native-tcp-socket": "^6.4.1",
"react-native-toast-message": "^2.3.3",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2", "react-native-worklets": "0.7.2",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",

View File

@@ -1,15 +1,97 @@
import React, { useCallback, useEffect } from "react"; import axios from "axios";
import { Text, View } from "react-native"; import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import { ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { GlobalFooter } from "../../components/UpdateFooter";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { useAppStore } from "../../hooks/useAppStore";
import { scannerFeedback } from "../../lib/feedbackScan";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner"; import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
export default function LaneCheck() { const InfoRow = ({
const handleScan = useCallback(async (scan: ZebraScanResult) => { label,
console.log(scan); value,
}, []); }: {
label: string;
value: React.ReactNode;
}) => {
return (
<View className="flex-row justify-between gap-4 py-2 border-b border-gray-200">
<Text className="text-sm text-gray-500">{label}</Text>
<Text className="text-sm font-medium text-gray-900 text-right flex-1">
{value}
</Text>
</View>
);
};
useEffect(() => { export default function LaneCheck() {
zebraScanner.ensureProfile(); const [units, setUnits] = useState<any>(null);
const serverIp = useAppStore((s) => s.serverIp);
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
setUnits(null);
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
if (!scan.data.startsWith("loc")) {
Toast.show({
type: "error",
text1: "Scan error",
text2: "The last scan was not a lane please try again",
});
return;
}
try {
const res = await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/lanecheck`,
{
lane: scan.data,
},
{
timeout: 5000,
},
);
if (res.status === 200) {
setUnits(res.data);
Toast.show({
type: "info",
text1: "Lane Data",
text2: "All Loading Units from this lane will be listed below",
});
}
} catch (error) {
console.log(error);
Toast.show({
type: "error",
text1: "Lane Data",
text2: "Error getting lane data please try again",
});
}
},
[serverIp.trim],
);
useFocusEffect(
useCallback(() => {
zebraScanner.startListening(); zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => { const sub = zebraScanner.addScanListener((scan) => {
@@ -20,18 +102,109 @@ export default function LaneCheck() {
return () => { return () => {
sub.remove(); sub.remove();
zebraScanner.stopListening(); zebraScanner.stopListening();
//setUnits(null);
}; };
}, [handleScan]); }, [handleScan]),
);
return ( return (
<View <View
style={{ style={{
flex: 1,
//justifyContent: "center", //justifyContent: "center",
alignItems: "center", alignItems: "center",
marginTop: 50, marginTop: 50,
}} }}
> >
<Text>LaneChecks</Text> {units ? (
// <SafeAreaView className={`flex-1 w-full items-center`}>
// <ScrollView className="w-full flex-1">
// <View className="flex items-center gap-2 w-full">
// {units.data?.map((i: any, index: any) => (
// <View key={`${i.runningNumber}-${index}`}>
// <Text>example</Text>
// </View>
// ))}
// </View>
// </ScrollView>
// </SafeAreaView>
<SafeAreaView className={`w-full items-center`}>
<View style={{ padding: 2 }}>
<Text>There Are {units.data.length} units in this lane</Text>
</View>
<ScrollView className="w-full" style={{ marginBottom: 20 }}>
<View>
{units.data.map((i, index) => (
<View
key={`${i.runningNumber}-${index}`}
style={{
justifyContent: "center",
margin: 2,
}}
>
<Dialog>
<DialogTrigger>
<Card
className="w-full"
style={{
borderColor:
i.state === "QualityBlocked" ? "red" : undefined,
borderWidth: 4,
}}
>
<CardContent>
<Text>
{i.articleId} - {i.articleName}
</Text>
<Text>
Running Number: {i.runningNumber ?? "Non barcoded"}
</Text>
</CardContent>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Details for Article {i.articleId}, Rn:
{i.runningNumber ?? "Non barcoded"}{" "}
</DialogTitle>
<DialogDescription>
<InfoRow
label="Production Date"
value={format(i.productionDate, "MM/dd/yyyy HH:mm")}
/>
<InfoRow label="Quantity" value={i.quantity} />
{i.state === "QualityBlocked" && (
<InfoRow
label="Defect"
value={i.mainDefectGroupDescription}
/>
)}
{i.state === "QualityBlocked" && (
<InfoRow
label="Description"
value={i.mainDefectDescription}
/>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</View>
))}
</View>
</ScrollView>
</SafeAreaView>
) : (
<View className="mt-50">
<Text className="text-2xl text-center">
Please scan a lane to see all Units that are in the lane.
</Text>
</View>
)}
<View>
<GlobalFooter />
</View>
</View> </View>
); );
} }

View File

@@ -2,10 +2,16 @@ import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import "../../global.css"; import "../../global.css";
import { useEffect } from "react";
import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck"; import useDeviceLock from "../hooks/useDeviceCheck";
import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() { export default function RootLayout() {
useDeviceLock(); useDeviceLock();
useEffect(() => {
zebraScanner.ensureProfile();
}, []);
return ( return (
<> <>
@@ -18,6 +24,7 @@ export default function RootLayout() {
<Stack.Screen name="(tabs)" /> <Stack.Screen name="(tabs)" />
</Stack> </Stack>
<PortalHost /> <PortalHost />
<Toast />
</> </>
); );
} }

View File

@@ -3,10 +3,14 @@ import axios from "axios";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { Alert, Button, Text, View } from "react-native"; import { Alert, Button, Text, View } from "react-native";
import Toast from "react-native-toast-message";
import { Input } from "../components/ui/input"; import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth"; import { useMobileAuthStore } from "../hooks/useMobileAuth";
const formatName = (name?: string) =>
name ? name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() : "";
export default function Login() { export default function Login() {
// doing this causes rerender and sub // doing this causes rerender and sub
//const { setUser } = useMobileAuthStore(); //const { setUser } = useMobileAuthStore();
@@ -33,12 +37,18 @@ export default function Login() {
if (res.status === 200) { if (res.status === 200) {
// this way to set the user is direct and basically a 1 shot // this way to set the user is direct and basically a 1 shot
Toast.show({
type: "success",
text1: `Welcome back ${formatName(res.data.data.name)}`,
});
useMobileAuthStore.getState().setUser(res.data.data); useMobileAuthStore.getState().setUser(res.data.data);
return router.replace("/(tabs)/scanner"); return router.replace("/(tabs)/scanner");
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
Alert.alert("Login Error", `Invalid pin please try again`); //Alert.alert("Login Error", `Invalid pin please try again`);
Toast.show({ type: "error", text1: `Invalid pin please try again` });
} }
}; };

View File

@@ -2,6 +2,7 @@ import Constants from "expo-constants";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { Alert, Button, Text, TextInput, View } from "react-native"; import { Alert, Button, Text, TextInput, View } from "react-native";
import Toast from "react-native-toast-message";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck"; import { useServerStore } from "../hooks/useServerCheck";
@@ -31,14 +32,23 @@ export default function Setup() {
if (pin === "6971") { if (pin === "6971") {
setAuth(true); setAuth(true);
} else { } else {
Alert.alert("Incorrect pin entered please try again"); //Alert.alert("Incorrect pin entered please try again");
Toast.show({
type: "error",
text1: "Incorrect pin entered please try again",
});
setPin(""); setPin("");
} }
}; };
const handleSave = async () => { const handleSave = async () => {
if (!serverIp.trim() || !serverPort.trim()) { if (!serverIp.trim() || !serverPort.trim()) {
Alert.alert("Missing info", "Please fill in both fields."); //Alert.alert("Missing info", "Please fill in both fields.");
Toast.show({
type: "error",
text1: "Missing info",
text2: "Please fill in both fields.",
});
return; return;
} }
@@ -50,7 +60,12 @@ export default function Setup() {
isRegistered: true, isRegistered: true,
}); });
Alert.alert("Saved", "Config saved to device."); //Alert.alert("Saved", "Config saved to device.");
Toast.show({
type: "info",
text1: "Saved",
text2: "Config saved to device.",
});
//router.replace("/"); //router.replace("/");
}; };
return ( return (

View File

@@ -1,5 +1,6 @@
import axios from "axios"; import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Text, View } from "react-native"; import { Alert, Button, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
@@ -90,6 +91,8 @@ export default function LSTScanner() {
user: user?.name ?? "prodScan", user: user?.name ?? "prodScan",
runningNumber: scan.data.startsWith("000") runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString() ? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: scan.data.startsWith("loc")
? scan.data
: "0", : "0",
}); });
} catch (error) { } catch (error) {
@@ -153,8 +156,8 @@ export default function LSTScanner() {
//console.log(lastScan); //console.log(lastScan);
useEffect(() => { useFocusEffect(
zebraScanner.ensureProfile(); useCallback(() => {
zebraScanner.startListening(); zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => { const sub = zebraScanner.addScanListener((scan) => {
@@ -166,7 +169,8 @@ export default function LSTScanner() {
sub.remove(); sub.remove();
zebraScanner.stopListening(); zebraScanner.stopListening();
}; };
}, [handleScan]); }, [handleScan]),
);
return ( return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}> <View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View style={{ alignItems: "center", margin: 5 }}> <View style={{ alignItems: "center", margin: 5 }}>

View File

@@ -1,5 +1,6 @@
import axios from "axios"; import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
@@ -58,6 +59,8 @@ export default function ProdScanner() {
...scanned.data, ...scanned.data,
runningNumber: scan.data.startsWith("000") runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString() ? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: scan.data.startsWith("loc")
? scan.data
: "0", : "0",
}; };
try { try {
@@ -118,8 +121,8 @@ export default function ProdScanner() {
//console.log(lastScan); //console.log(lastScan);
useEffect(() => { useFocusEffect(
zebraScanner.ensureProfile(); useCallback(() => {
zebraScanner.startListening(); zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => { const sub = zebraScanner.addScanListener((scan) => {
@@ -131,7 +134,8 @@ export default function ProdScanner() {
sub.remove(); sub.remove();
zebraScanner.stopListening(); zebraScanner.stopListening();
}; };
}, [handleScan]); }, [handleScan]),
);
return ( return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}> <View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View> <View>

View File

@@ -0,0 +1,140 @@
import { Icon } from '@/components/ui/icon';
import { NativeOnlyAnimatedView } from '@/components/ui/native-only-animated-view';
import { cn } from '@/lib/utils';
import * as DialogPrimitive from '@rn-primitives/dialog';
import { X } from 'lucide-react-native';
import * as React from 'react';
import { Platform, Text, View, type ViewProps } from 'react-native';
import { FadeIn, FadeOut } from 'react-native-reanimated';
import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const FullWindowOverlay = Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment;
function DialogOverlay({
className,
children,
...props
}: Omit<React.ComponentProps<typeof DialogPrimitive.Overlay>, 'asChild'> & {
children?: React.ReactNode;
}) {
return (
<FullWindowOverlay>
<DialogPrimitive.Overlay
className={cn(
'absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center bg-black/50 p-2',
Platform.select({
web: 'animate-in fade-in-0 fixed cursor-default [&>*]:cursor-auto',
}),
className
)}
{...props}
asChild={Platform.OS !== 'web'}>
<NativeOnlyAnimatedView entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)}>
<NativeOnlyAnimatedView entering={FadeIn.delay(50)} exiting={FadeOut.duration(150)}>
<>{children}</>
</NativeOnlyAnimatedView>
</NativeOnlyAnimatedView>
</DialogPrimitive.Overlay>
</FullWindowOverlay>
);
}
function DialogContent({
className,
portalHost,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
portalHost?: string;
}) {
return (
<DialogPortal hostName={portalHost}>
<DialogOverlay>
<DialogPrimitive.Content
className={cn(
'bg-background border-border z-50 mx-auto flex w-full max-w-[calc(100%-2rem)] flex-col gap-4 rounded-lg border p-6 shadow-lg shadow-black/5 sm:max-w-lg',
Platform.select({
web: 'animate-in fade-in-0 zoom-in-95 duration-200',
}),
className
)}
{...props}>
<>{children}</>
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded opacity-70 active:opacity-100',
Platform.select({
web: 'ring-offset-background focus:ring-ring data-[state=open]:bg-accent transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2',
})
)}
hitSlop={12}>
<Icon
as={X}
className={cn('text-accent-foreground web:pointer-events-none size-4 shrink-0')}
/>
<Text className="sr-only">Close</Text>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogOverlay>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: ViewProps) {
return (
<View className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />
);
}
function DialogFooter({ className, ...props }: ViewProps) {
return (
<View
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn('text-foreground text-lg font-semibold leading-none', className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -0,0 +1,57 @@
import { TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import type { LucideIcon, LucideProps } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
import * as React from 'react';
type IconProps = LucideProps & {
as: LucideIcon;
} & React.RefAttributes<LucideIcon>;
function IconImpl({ as: IconComponent, ...props }: IconProps) {
return <IconComponent {...props} />;
}
cssInterop(IconImpl, {
className: {
target: 'style',
nativeStyleToProp: {
height: 'size',
width: 'size',
},
},
});
/**
* A wrapper component for Lucide icons with Nativewind `className` support via `cssInterop`.
*
* This component allows you to render any Lucide icon while applying utility classes
* using `nativewind`. It avoids the need to wrap or configure each icon individually.
*
* @component
* @example
* ```tsx
* import { ArrowRight } from 'lucide-react-native';
* import { Icon } from '@/registry/components/ui/icon';
*
* <Icon as={ArrowRight} className="text-red-500" size={16} />
* ```
*
* @param {LucideIcon} as - The Lucide icon component to render.
* @param {string} className - Utility classes to style the icon using Nativewind.
* @param {number} size - Icon size (defaults to 14).
* @param {...LucideProps} ...props - Additional Lucide icon props passed to the "as" icon.
*/
function Icon({ as: IconComponent, className, size = 14, ...props }: IconProps) {
const textClass = React.useContext(TextClassContext);
return (
<IconImpl
as={IconComponent}
className={cn('text-foreground', textClass, className)}
size={size}
{...props}
/>
);
}
export { Icon };

View File

@@ -0,0 +1,23 @@
import { Platform } from 'react-native';
import Animated from 'react-native-reanimated';
/**
* This component is used to wrap animated views that should only be animated on native.
* @param props - The props for the animated view.
* @returns The animated view if the platform is native, otherwise the children.
* @example
* <NativeOnlyAnimatedView entering={FadeIn} exiting={FadeOut}>
* <Text>I am only animated on native</Text>
* </NativeOnlyAnimatedView>
*/
function NativeOnlyAnimatedView(
props: React.ComponentProps<typeof Animated.View> & React.RefAttributes<typeof Animated.View>
) {
if (Platform.OS === 'web') {
return <>{props.children as React.ReactNode}</>;
} else {
return <Animated.View {...props} />;
}
}
export { NativeOnlyAnimatedView };

View File

@@ -1,5 +1,5 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot'; import { Slot } from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react'; import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native'; import { Platform, Text as RNText, type Role } from 'react-native';
@@ -70,11 +70,12 @@ function Text({
variant = 'default', variant = 'default',
...props ...props
}: React.ComponentProps<typeof RNText> & }: React.ComponentProps<typeof RNText> &
React.RefAttributes<typeof RNText> &
TextVariantProps & { TextVariantProps & {
asChild?: boolean; asChild?: boolean;
}) { }) {
const textClass = React.useContext(TextClassContext); const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText; const Component = asChild ? Slot : RNText;
return ( return (
<Component <Component
className={cn(textVariants({ variant }), textClass, className)} className={cn(textVariants({ variant }), textClass, className)}

View File

@@ -37,4 +37,7 @@ export const zebraScanner = {
): EmitterSubscription { ): EmitterSubscription {
return scannerEmitter.addListener("barcodeScanned", callback); return scannerEmitter.addListener("barcodeScanned", callback);
}, },
disableScannerInput() {
ZebraScanner.disableScannerInput();
},
}; };