@imksh/create 1.0.32 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imksh/create",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "A production-ready create for scaffolding modern applications.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
1
  {
2
- "name": "shivAi",
2
+ "name": "Mobile",
3
3
  "main": "expo-router/entry",
4
4
  "version": "1.0.0",
5
5
  "dependencies": {
6
- "@expo/ui": "~57.0.7",
6
+ "@expo/ui": "~57.0.22",
7
7
  "@expo/vector-icons": "^15.1.1",
8
8
  "@react-native-async-storage/async-storage": "2.2.0",
9
9
  "@react-native-community/slider": "^5.2.0",
@@ -12,53 +12,51 @@
12
12
  "@react-navigation/bottom-tabs": "^7.18.10",
13
13
  "axios": "^1.18.1",
14
14
  "babel-preset-expo": "^57.0.3",
15
+ "dayjs": "^1.11.13",
15
16
  "drizzle-orm": "^0.45.2",
16
- "expo": "~57.0.7",
17
- "expo-blur": "~57.0.2",
17
+ "expo": "~57.0.27",
18
+ "expo-blur": "~57.0.3",
18
19
  "expo-constants": "~57.0.6",
19
- "expo-device": "~57.0.1",
20
- "expo-file-system": "~18.0.7",
21
- "expo-sharing": "~13.0.1",
20
+ "expo-device": "~57.0.2",
21
+ "expo-file-system": "~57.0.7",
22
22
  "expo-font": "~57.0.1",
23
- "expo-glass-effect": "~57.0.1",
24
- "expo-image": "~57.0.1",
25
- "expo-image-picker": "^57.0.6",
26
- "expo-linear-gradient": "^57.0.1",
27
- "expo-linking": "~57.0.3",
28
- "expo-local-authentication": "~15.0.2",
29
- "expo-location": "~18.0.5",
30
- "expo-notifications": "~57.0.6",
31
- "dayjs": "^1.11.13",
32
- "expo-router": "~57.0.7",
33
- "expo-splash-screen": "~57.0.4",
34
- "expo-sqlite": "^57.0.1",
23
+ "expo-glass-effect": "~57.0.4",
24
+ "expo-image": "~57.0.5",
25
+ "expo-image-picker": "~57.0.20",
26
+ "expo-linear-gradient": "~57.0.2",
27
+ "expo-linking": "~57.0.12",
28
+ "expo-local-authentication": "~57.0.3",
29
+ "expo-location": "~57.0.20",
30
+ "expo-notifications": "~57.0.22",
31
+ "expo-router": "~57.0.25",
32
+ "expo-sharing": "~57.0.22",
33
+ "expo-splash-screen": "~57.0.9",
34
+ "expo-sqlite": "~57.0.4",
35
35
  "expo-status-bar": "~57.0.1",
36
- "expo-symbols": "~57.0.1",
37
- "expo-system-ui": "~57.0.1",
38
- "expo-web-browser": "~57.0.1",
39
- "lottie-react": "^2.4.1",
36
+ "expo-symbols": "~57.0.3",
37
+ "expo-system-ui": "~57.0.4",
38
+ "expo-web-browser": "~57.0.3",
40
39
  "lottie-react-native": "^7.3.8",
41
40
  "nativewind": "^4.2.6",
42
41
  "prettier-plugin-tailwindcss": "^0.5.14",
43
42
  "react": "19.2.3",
44
43
  "react-dom": "19.2.3",
45
- "react-native": "0.86.0",
44
+ "react-native": "0.86.3",
45
+ "react-native-blob-util": "^0.19.11",
46
46
  "react-native-gesture-handler": "~2.32.0",
47
47
  "react-native-image-viewing": "^0.2.2",
48
- "react-native-maps": "^1.18.0",
49
48
  "react-native-keyboard-aware-scroll-view": "^0.9.5",
50
49
  "react-native-otp-entry": "^1.8.6",
51
50
  "react-native-paper": "^5.15.3",
52
- "react-native-pdf": "^6.7.5",
53
- "react-native-blob-util": "^0.19.11",
51
+ "react-native-pdf": "^7.0.5",
54
52
  "react-native-permissions": "^5.6.0",
55
- "react-native-reanimated": "^4.5.2",
56
- "react-native-safe-area-context": "^5.8.0",
57
- "react-native-screens": "4.25.2",
53
+ "react-native-reanimated": "4.5.1",
54
+ "react-native-safe-area-context": "~5.7.0",
55
+ "react-native-screens": "~4.26.0",
58
56
  "react-native-toast-message": "^2.4.0",
59
57
  "react-native-web": "~0.21.0",
60
- "react-native-webview": "^13.12.5",
61
- "react-native-worklets": "0.10.0",
58
+ "react-native-webview": "13.16.1",
59
+ "react-native-worklets": "0.10.1",
62
60
  "socket.io-client": "^4.8.3",
63
61
  "tailwindcss": "^3.4.19",
64
62
  "zustand": "^5.0.14"
@@ -69,7 +67,7 @@
69
67
  "dpdm": "^3.14.0",
70
68
  "drizzle-kit": "^0.31.10",
71
69
  "eslint": "^9.0.0",
72
- "eslint-config-expo": "~57.0.0",
70
+ "eslint-config-expo": "~57.0.2",
73
71
  "prettier": "^3.4.2",
74
72
  "ts-prune": "^0.10.3",
75
73
  "typescript": "~6.0.3"
@@ -86,8 +84,7 @@
86
84
  "format": "prettier --write \"src/**/*.{ts,tsx,json}\"",
87
85
  "format:check": "prettier --check \"src/**/*.{ts,tsx,json}\"",
88
86
  "circular-check": "dpdm --circular --no-warning --no-tree --exit-code circular:1 \"src/**/*.{ts,tsx}\"",
89
- "ts-prune": "ts-prune",
90
- "ts-purns": "ts-prune",
87
+ "prune:unused": "ts-prune",
91
88
  "ts:check": "tsc --noEmit",
92
89
  "build": "eas build",
93
90
  "build:android": "eas build -p android",
@@ -30,7 +30,7 @@ export default function RootLayout() {
30
30
  const { getMe, isCheckingAuth, loadCachedUser, user } = useAuthStore();
31
31
  const { colorScheme, setColorScheme } = useColorScheme();
32
32
  const colors = Colors[colorScheme ?? "light"];
33
-
33
+
34
34
  const segments = useSegments();
35
35
  const router = useRouter();
36
36
 
@@ -93,10 +93,6 @@ export default function RootLayout() {
93
93
  name="(auth)"
94
94
  options={{ headerShown: false, animation: "fade" }}
95
95
  />
96
- <Stack.Screen
97
- name="doc-viewer"
98
- options={{ presentation: "fullScreenModal" }}
99
- />
100
96
  </Stack>
101
97
  </BiometricLock>
102
98
  </PaperProvider>
@@ -0,0 +1,378 @@
1
+ import React, { useState, useEffect, useMemo } from "react";
2
+ import {
3
+ View,
4
+ StyleSheet,
5
+ Dimensions,
6
+ Platform,
7
+ UIManager,
8
+ TouchableOpacity,
9
+ } from "react-native";
10
+ import { useLocalSearchParams, useRouter } from "expo-router";
11
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
12
+ import { useColorScheme } from "nativewind";
13
+ import { Colors } from "../../constants/Colors";
14
+ import ScreenHeader from "../../components/common/ScreenHeader";
15
+ import { Txt } from "../../components/common/Typography";
16
+ import Input from "../../components/ui/inputs/Input";
17
+ import Button from "../../components/ui/buttons/Button";
18
+ import Container from "../../components/ui/layout/Container";
19
+ import { Ionicons } from "@expo/vector-icons";
20
+ import ProgressBar from "../../components/ui/feedback/ProgressBar";
21
+ import * as FileSystem from "expo-file-system";
22
+ import * as WebBrowser from "expo-web-browser";
23
+ import * as Sharing from "expo-sharing";
24
+ import { isExpoGo, hasNativeModule } from "../../utils/env";
25
+
26
+ const hasPdfViewManager = (): boolean => {
27
+ try {
28
+ return Boolean(
29
+ UIManager.getViewManagerConfig &&
30
+ UIManager.getViewManagerConfig("RNPDFPdfView")
31
+ );
32
+ } catch {
33
+ return false;
34
+ }
35
+ };
36
+
37
+ const getPdfComponent = () => {
38
+ if (
39
+ isExpoGo() ||
40
+ !hasNativeModule("ReactNativeBlobUtil") ||
41
+ !hasPdfViewManager()
42
+ ) {
43
+ return null;
44
+ }
45
+ try {
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const mod = require("react-native-pdf");
48
+ return mod.default || mod;
49
+ } catch {
50
+ return null;
51
+ }
52
+ };
53
+
54
+ const getWebViewComponent = () => {
55
+ try {
56
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
57
+ const mod = require("react-native-webview");
58
+ return mod.WebView || mod.default || mod;
59
+ } catch {
60
+ return null;
61
+ }
62
+ };
63
+
64
+ export default function DocViewerScreen() {
65
+ const router = useRouter();
66
+ const { url, name } = useLocalSearchParams<{
67
+ url: string;
68
+ name?: string;
69
+ }>();
70
+
71
+ useSafeAreaInsets();
72
+ const { colorScheme } = useColorScheme();
73
+ const isDark = colorScheme === "dark";
74
+ const colors = Colors[isDark ? "dark" : "light"];
75
+
76
+ const [totalPages, setTotalPages] = useState(0);
77
+ const [currentPage, setCurrentPage] = useState(1);
78
+
79
+ const [password, setPassword] = useState("");
80
+ const [isPasswordRequired, setIsPasswordRequired] = useState(false);
81
+ const [inputPassword, setInputPassword] = useState("");
82
+ const [errorMsg, setErrorMsg] = useState("");
83
+
84
+ const [localUri, setLocalUri] = useState<string | null>(null);
85
+ const [downloadProgress, setDownloadProgress] = useState(0);
86
+ const [downloadError, setDownloadError] = useState<string | null>(null);
87
+
88
+ const PdfComponent = useMemo(() => getPdfComponent(), []);
89
+ const WebViewComponent = useMemo(() => getWebViewComponent(), []);
90
+
91
+ useEffect(() => {
92
+ if (!url) return;
93
+
94
+ if (url.startsWith("http")) {
95
+ const downloadFile = async () => {
96
+ try {
97
+ const filename = `doc_${Date.now()}.pdf`;
98
+
99
+ // Modern expo-file-system File API check
100
+ if (FileSystem.File?.downloadFileAsync && FileSystem.Paths?.cache) {
101
+ const destination = new FileSystem.File(FileSystem.Paths.cache, filename);
102
+ const downloaded = await FileSystem.File.downloadFileAsync(url, destination, {
103
+ onProgress: (progress) => {
104
+ if (progress.totalBytes > 0) {
105
+ setDownloadProgress(progress.bytesWritten / progress.totalBytes);
106
+ }
107
+ },
108
+ });
109
+ setLocalUri(downloaded.uri);
110
+ return;
111
+ }
112
+
113
+ // Legacy downloadResumable fallback if supported
114
+ if (typeof (FileSystem as any).createDownloadResumable === "function") {
115
+ const baseDir =
116
+ (FileSystem as any).documentDirectory ||
117
+ (FileSystem as any).cacheDirectory ||
118
+ "";
119
+ const fileUri = `${baseDir}${filename}`;
120
+ const downloadResumable = (FileSystem as any).createDownloadResumable(
121
+ url,
122
+ fileUri,
123
+ {},
124
+ (progressEvent: any) => {
125
+ const progress =
126
+ progressEvent.totalBytesExpectedToWrite > 0
127
+ ? progressEvent.totalBytesWritten / progressEvent.totalBytesExpectedToWrite
128
+ : 0;
129
+ setDownloadProgress(progress);
130
+ }
131
+ );
132
+
133
+ const result = await downloadResumable.downloadAsync();
134
+ if (result && result.uri) {
135
+ setLocalUri(result.uri);
136
+ return;
137
+ }
138
+ }
139
+
140
+ // Fallback to url directly if local download is unavailable
141
+ setLocalUri(url);
142
+ } catch (error) {
143
+ console.error("Download error:", error);
144
+ // Still fallback to original url
145
+ setLocalUri(url);
146
+ }
147
+ };
148
+ downloadFile();
149
+ } else {
150
+ setLocalUri(url);
151
+ }
152
+ }, [url]);
153
+
154
+ const handleShare = async () => {
155
+ try {
156
+ const targetUri = localUri || url;
157
+ if (targetUri && (await Sharing.isAvailableAsync())) {
158
+ await Sharing.shareAsync(targetUri, {
159
+ mimeType: "application/pdf",
160
+ dialogTitle: name || "Open Document",
161
+ UTI: "com.adobe.pdf",
162
+ });
163
+ } else if (url && url.startsWith("http")) {
164
+ await WebBrowser.openBrowserAsync(url);
165
+ }
166
+ } catch (e) {
167
+ console.warn("Failed to open or share document:", e);
168
+ }
169
+ };
170
+
171
+ if (!url) {
172
+ return (
173
+ <View style={{ flex: 1, backgroundColor: colors.base100 }}>
174
+ <ScreenHeader title="Document Viewer" onBack={() => router.back()} />
175
+ <View className="flex-1 items-center justify-center p-4">
176
+ <Ionicons
177
+ name="alert-circle-outline"
178
+ size={48}
179
+ color={colors.baseContent}
180
+ style={{ opacity: 0.5 }}
181
+ />
182
+ <Txt variant="base" className="mt-4 text-center opacity-70">
183
+ No document URL provided.
184
+ </Txt>
185
+ </View>
186
+ </View>
187
+ );
188
+ }
189
+
190
+ const handlePasswordSubmit = () => {
191
+ setPassword(inputPassword);
192
+ setIsPasswordRequired(false);
193
+ setErrorMsg("");
194
+ };
195
+
196
+ return (
197
+ <View style={{ flex: 1, backgroundColor: colors.base100 }}>
198
+ <ScreenHeader
199
+ title={name || "Document Viewer"}
200
+ subtitle={totalPages > 0 ? `Page ${currentPage} of ${totalPages}` : undefined}
201
+ onBack={() => router.back()}
202
+ actions={
203
+ <TouchableOpacity
204
+ onPress={handleShare}
205
+ activeOpacity={0.7}
206
+ className="w-10 h-10 rounded-full bg-base-200/80 items-center justify-center border border-base-300/10 shadow-sm"
207
+ >
208
+ <Ionicons name="share-outline" color={colors.baseContent} size={20} />
209
+ </TouchableOpacity>
210
+ }
211
+ />
212
+
213
+ {isPasswordRequired ? (
214
+ <Container className="flex-1 justify-center items-center px-6">
215
+ <View
216
+ className="p-6 rounded-3xl w-full"
217
+ style={{ backgroundColor: colors.base200 }}
218
+ >
219
+ <Txt variant="xl" className="font-bold text-center mb-2">
220
+ Password Protected
221
+ </Txt>
222
+ <Txt variant="base" className="text-center text-base-content/70 mb-6">
223
+ This document requires a password to open.
224
+ </Txt>
225
+ <Input
226
+ placeholder="Enter password"
227
+ value={inputPassword}
228
+ onChangeText={setInputPassword}
229
+ secureTextEntry
230
+ className="mb-4"
231
+ />
232
+ {errorMsg ? (
233
+ <Txt variant="caption" className="text-error text-center mb-4">
234
+ {errorMsg}
235
+ </Txt>
236
+ ) : null}
237
+ <Button
238
+ label="Unlock Document"
239
+ variant="primary"
240
+ isFullWidth
241
+ onPress={handlePasswordSubmit}
242
+ />
243
+ </View>
244
+ </Container>
245
+ ) : (
246
+ <View style={styles.pdfContainer}>
247
+ {!localUri && !downloadError ? (
248
+ <View className="flex-1 items-center justify-center w-full relative">
249
+ <View className="w-3/4 max-w-sm absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-base-100 p-6 rounded-2xl shadow-lg border border-base-300">
250
+ <ProgressBar
251
+ progress={downloadProgress * 100}
252
+ label="Downloading Document"
253
+ />
254
+ </View>
255
+ </View>
256
+ ) : downloadError ? (
257
+ <View className="flex-1 items-center justify-center p-4">
258
+ <Ionicons
259
+ name="close-circle-outline"
260
+ size={48}
261
+ color={colors.error}
262
+ style={{ opacity: 0.8 }}
263
+ />
264
+ <Txt
265
+ variant="base"
266
+ className="mt-4 text-center text-error font-semibold"
267
+ >
268
+ {downloadError}
269
+ </Txt>
270
+ <Button
271
+ label="Go Back"
272
+ variant="outline"
273
+ style={{ marginTop: 20 }}
274
+ onPress={() => router.back()}
275
+ />
276
+ </View>
277
+ ) : PdfComponent ? (
278
+ <PdfComponent
279
+ source={{ uri: localUri!, cache: false }}
280
+ password={password}
281
+ onLoadComplete={(numberOfPages: number) => {
282
+ setTotalPages(numberOfPages);
283
+ }}
284
+ onPageChanged={(page: number) => {
285
+ setCurrentPage(page);
286
+ }}
287
+ onError={(error: unknown) => {
288
+ const err = error as { message?: string };
289
+ if (err?.message?.toLowerCase().includes("password")) {
290
+ setIsPasswordRequired(true);
291
+ setErrorMsg("Incorrect password. Please try again.");
292
+ }
293
+ }}
294
+ trustAllCerts={false}
295
+ style={[styles.pdf, { backgroundColor: colors.base200 }]}
296
+ />
297
+ ) : Platform.OS === "ios" && WebViewComponent ? (
298
+ <View style={{ flex: 1, width: "100%" }}>
299
+ <WebViewComponent
300
+ source={{ uri: localUri || url }}
301
+ style={[styles.pdf, { backgroundColor: colors.base200 }]}
302
+ originWhitelist={["*"]}
303
+ scalesPageToFit={true}
304
+ />
305
+ </View>
306
+ ) : Platform.OS === "android" &&
307
+ WebViewComponent &&
308
+ url &&
309
+ url.startsWith("http") ? (
310
+ <View style={{ flex: 1, width: "100%" }}>
311
+ <WebViewComponent
312
+ source={{
313
+ uri: `https://docs.google.com/gview?embedded=true&url=${encodeURIComponent(url)}`,
314
+ }}
315
+ style={[styles.pdf, { backgroundColor: colors.base200 }]}
316
+ originWhitelist={["*"]}
317
+ scalesPageToFit={true}
318
+ />
319
+ </View>
320
+ ) : (
321
+ <View className="flex-1 items-center justify-center p-6 w-full">
322
+ <View
323
+ className="p-6 rounded-3xl w-full max-w-sm items-center border border-base-300"
324
+ style={{ backgroundColor: colors.base200 }}
325
+ >
326
+ <View
327
+ className="w-16 h-16 rounded-full items-center justify-center mb-4"
328
+ style={{ backgroundColor: `${colors.primary}20` }}
329
+ >
330
+ <Ionicons
331
+ name="document-text-outline"
332
+ size={36}
333
+ color={colors.primary}
334
+ />
335
+ </View>
336
+ <Txt variant="lg" className="font-bold text-center mb-1">
337
+ {name || "Document Ready"}
338
+ </Txt>
339
+ <Txt variant="caption" className="text-center opacity-70 mb-6">
340
+ Native PDF rendering requires a development build. You can open or
341
+ share this document in your device reader.
342
+ </Txt>
343
+ <Button
344
+ label="Open in Device Reader"
345
+ variant="primary"
346
+ isFullWidth
347
+ onPress={handleShare}
348
+ className="mb-3"
349
+ />
350
+ {url && url.startsWith("http") ? (
351
+ <Button
352
+ label="Open in Web Browser"
353
+ variant="outline"
354
+ isFullWidth
355
+ onPress={() => WebBrowser.openBrowserAsync(url)}
356
+ />
357
+ ) : null}
358
+ </View>
359
+ </View>
360
+ )}
361
+ </View>
362
+ )}
363
+ </View>
364
+ );
365
+ }
366
+
367
+ const styles = StyleSheet.create({
368
+ pdfContainer: {
369
+ flex: 1,
370
+ justifyContent: "flex-start",
371
+ alignItems: "center",
372
+ },
373
+ pdf: {
374
+ flex: 1,
375
+ width: Dimensions.get("window").width,
376
+ height: Dimensions.get("window").height,
377
+ },
378
+ });
@@ -4,8 +4,6 @@ import { Txt } from "./Typography";
4
4
 
5
5
  import { useColorScheme } from "nativewind";
6
6
  import { Colors } from "../../constants/Colors";
7
- import { useTranslation } from "react-i18next";
8
-
9
7
  import { Portal } from "react-native-paper";
10
8
 
11
9
  export interface ConfirmationToastProps {
@@ -25,9 +23,8 @@ export default function ConfirmationToast({
25
23
  cancleText,
26
24
  confirmText,
27
25
  }: ConfirmationToastProps) {
28
- const { t } = useTranslation();
29
- const cText = cancleText ?? t("common.cancel");
30
- const confText = confirmText ?? t("common.confirm");
26
+ const cText = cancleText ?? "Cancel";
27
+ const confText = confirmText ?? "Confirm";
31
28
  const { colorScheme } = useColorScheme();
32
29
  const colors = Colors[colorScheme ?? "light"];
33
30
 
@@ -1,39 +1,12 @@
1
- import { useEffect, useState } from "react";
1
+ import { useState } from "react";
2
2
  import { TouchableOpacity, View, ActivityIndicator } from "react-native";
3
- import Constants from "expo-constants";
4
3
  import { useColorScheme } from "nativewind";
5
- import { Colors } from "../../../constants/Colors";
4
+ import { Colors } from "./../../constants/Colors";
6
5
  import { Txt } from "./Typography";
7
- import { useAuthStore } from "../../../store/useAuthStore";
8
- import { toast } from "../../../utils/toast";
6
+ import { useAuthStore } from "./../../store/useAuthStore";
7
+ import { toast } from "./../../utils/toast";
9
8
  import { Ionicons } from "@expo/vector-icons";
10
-
11
- const isExpoGo = Constants.appOwnership === "expo";
12
-
13
- interface GoogleSigninType {
14
- configure: (options: Record<string, unknown>) => void;
15
- hasPlayServices: (options: { showPlayServicesUpdateDialog: boolean }) => Promise<boolean>;
16
- signIn: () => Promise<{ data?: { idToken?: string }; idToken?: string }>;
17
- }
18
-
19
- interface StatusCodesType {
20
- SIGN_IN_CANCELLED: string;
21
- IN_PROGRESS: string;
22
- PLAY_SERVICES_NOT_AVAILABLE: string;
23
- }
24
-
25
- let GoogleSignin: GoogleSigninType | undefined;
26
- let statusCodes: StatusCodesType | undefined;
27
-
28
- if (!isExpoGo) {
29
- try {
30
- const googleModule = require("@react-native-google-signin/google-signin");
31
- GoogleSignin = googleModule.GoogleSignin;
32
- statusCodes = googleModule.statusCodes;
33
- } catch {
34
- // Module not found or not supported in current environment
35
- }
36
- }
9
+ import { isExpoGo, hasNativeModule } from "../../utils/env";
37
10
 
38
11
  interface GoogleAuthButtonProps {
39
12
  onSuccess?: () => void;
@@ -51,32 +24,34 @@ export default function GoogleAuthButton({
51
24
  const { googleLogin } = useAuthStore();
52
25
  const [loading, setLoading] = useState(false);
53
26
 
54
- useEffect(() => {
55
- if (!isExpoGo && GoogleSignin) {
56
- GoogleSignin.configure({
57
- webClientId: process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID || "",
58
- iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID || "",
59
- });
60
- }
61
- }, []);
62
-
63
27
  const handlePress = async () => {
64
- if (isExpoGo) {
28
+ if (isExpoGo() || !hasNativeModule("RNGoogleSignin")) {
65
29
  toast.info(
66
- "Expo Go",
67
- "Google Sign-In is not supported in Expo Go. Please use a custom dev client."
30
+ "Development Build Required",
31
+ "Google Sign-In requires a custom development build (expo-dev-client)."
68
32
  );
69
33
  return;
70
34
  }
71
35
 
72
- if (!GoogleSignin) {
73
- toast.error("Error", "Google Sign-In module is not loaded.");
74
- return;
75
- }
76
-
77
36
  try {
78
37
  setLoading(true);
79
- await GoogleSignin.hasPlayServices();
38
+
39
+ // Lazily require Google Sign-In only when running in a native custom development build
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const googleModule = require("@react-native-google-signin/google-signin");
42
+ const { GoogleSignin, statusCodes } = googleModule;
43
+
44
+ if (!GoogleSignin) {
45
+ toast.error("Error", "Google Sign-In module is not available.");
46
+ return;
47
+ }
48
+
49
+ GoogleSignin.configure({
50
+ webClientId: process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID || "",
51
+ iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID || "",
52
+ });
53
+
54
+ await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
80
55
 
81
56
  // Force account selection every time
82
57
  try {
@@ -101,16 +76,20 @@ export default function GoogleAuthButton({
101
76
  toast.error("Error", "Google authentication failed");
102
77
  }
103
78
  } catch (error: unknown) {
79
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
80
+ const statusCodes = hasNativeModule("RNGoogleSignin")
81
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
82
+ ? require("@react-native-google-signin/google-signin").statusCodes
83
+ : undefined;
84
+
104
85
  const err = error as { code?: string; message?: string };
105
86
  if (err.code === statusCodes?.SIGN_IN_CANCELLED) {
106
- // user cancelled the login flow
87
+ // User cancelled the login flow
107
88
  } else if (err.code === statusCodes?.IN_PROGRESS) {
108
- // operation (e.g. sign in) is in progress already
89
+ // Operation is in progress already
109
90
  } else if (err.code === statusCodes?.PLAY_SERVICES_NOT_AVAILABLE) {
110
- // play services not available or outdated
111
91
  toast.error("Error", "Google Play Services not available");
112
92
  } else {
113
- // some other error happened
114
93
  toast.error("Error", err.message || "An unexpected error occurred");
115
94
  }
116
95
  } finally {
@@ -5,8 +5,8 @@ import { authenticateAsync } from "@/services/biometric.service";
5
5
  import { Ionicons } from "@expo/vector-icons";
6
6
  import { Colors } from "@/constants/Colors";
7
7
  import { useColorScheme } from "nativewind";
8
- import { Txt } from "./common/Typography";
9
- import Button from "./ui/buttons/Button";
8
+ import { Txt } from "../common/Typography";
9
+ import Button from "../ui/buttons/Button";
10
10
  import appConfig from "@/config/appConfig";
11
11
 
12
12
  export default function BiometricLock({ children }: { children: React.ReactNode }) {
@@ -60,7 +60,7 @@ export default function BiometricLock({ children }: { children: React.ReactNode
60
60
  if (isAuthenticating) return;
61
61
  setIsAuthenticating(true);
62
62
 
63
- const success = await authenticateAsync("Unlock HireMe");
63
+ const success = await authenticateAsync(`Unlock ${appConfig.app.name}`);
64
64
 
65
65
  if (success) {
66
66
  setIsAppLocked(false);