@imksh/create 1.0.31 → 1.0.33

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.
Files changed (158) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/commands/create.js +2 -0
  4. package/src/index.js +1 -1
  5. package/templates/layers/next/javascript/package.json +8 -2
  6. package/templates/layers/next/typescript/.eslintrc.json +12 -0
  7. package/templates/layers/next/typescript/package.json +14 -3
  8. package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -1
  9. package/templates/layers/next/typescript/src/components/common/LottieIcon.tsx +2 -2
  10. package/templates/layers/next/typescript/src/components/common/Table.tsx +4 -4
  11. package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +2 -1
  12. package/templates/layers/next/typescript/src/components/ui/Header.tsx +2 -2
  13. package/templates/layers/next/typescript/src/hooks/usePwaInstall.ts +10 -4
  14. package/templates/layers/next/typescript/src/services/authService.ts +1 -1
  15. package/templates/layers/next/typescript/src/store/useAuthStore.ts +24 -9
  16. package/templates/layers/node/javascript/.env.example +3 -1
  17. package/templates/layers/node/javascript/package.json +12 -3
  18. package/templates/layers/node/javascript/src/app.js +2 -1
  19. package/templates/layers/node/javascript/src/controllers/auth.controller.js +106 -168
  20. package/templates/layers/node/javascript/src/controllers/push.controller.js +51 -58
  21. package/templates/layers/node/javascript/src/controllers/upload.controller.js +12 -19
  22. package/templates/layers/node/javascript/src/middleware/auth.middleware.js +5 -11
  23. package/templates/layers/node/javascript/src/middleware/errorHandler.js +6 -4
  24. package/templates/layers/node/javascript/src/routes/auth.route.js +2 -0
  25. package/templates/layers/node/javascript/src/services/auth.service.js +159 -164
  26. package/templates/layers/node/javascript/src/utils/ApiError.js +20 -0
  27. package/templates/layers/node/javascript/src/utils/SuccessResponse.js +15 -0
  28. package/templates/layers/node/javascript/src/utils/apiResponse.js +4 -0
  29. package/templates/layers/node/javascript/src/utils/asyncHandler.js +5 -0
  30. package/templates/layers/node/typescript/.env.example +3 -1
  31. package/templates/layers/node/typescript/.eslintrc.cjs +28 -0
  32. package/templates/layers/node/typescript/package.json +16 -2
  33. package/templates/layers/node/typescript/src/app.ts +3 -2
  34. package/templates/layers/node/typescript/src/config/db.ts +3 -2
  35. package/templates/layers/node/typescript/src/config/socket.ts +5 -4
  36. package/templates/layers/node/typescript/src/controllers/auth.controller.ts +104 -132
  37. package/templates/layers/node/typescript/src/controllers/push.controller.ts +52 -59
  38. package/templates/layers/node/typescript/src/controllers/upload.controller.ts +12 -19
  39. package/templates/layers/node/typescript/src/middleware/auth.middleware.ts +13 -7
  40. package/templates/layers/node/typescript/src/middleware/errorHandler.ts +18 -5
  41. package/templates/layers/node/typescript/src/middleware/rateLimiter.ts +2 -2
  42. package/templates/layers/node/typescript/src/routes/auth.route.ts +2 -0
  43. package/templates/layers/node/typescript/src/services/auth.service.ts +86 -43
  44. package/templates/layers/node/typescript/src/types/express.d.ts +5 -3
  45. package/templates/layers/node/typescript/src/utils/ApiError.ts +25 -0
  46. package/templates/layers/node/typescript/src/utils/SuccessResponse.ts +17 -0
  47. package/templates/layers/node/typescript/src/utils/apiResponse.ts +4 -0
  48. package/templates/layers/node/typescript/src/utils/asyncHandler.ts +13 -0
  49. package/templates/layers/node/typescript/src/utils/cloudinaryUpload.ts +18 -7
  50. package/templates/layers/node/typescript/src/utils/expoNotification.ts +1 -1
  51. package/templates/layers/node/typescript/src/utils/genOtpToken.ts +7 -1
  52. package/templates/layers/node/typescript/src/utils/jwt.ts +8 -2
  53. package/templates/layers/node/typescript/src/utils/logger.ts +2 -1
  54. package/templates/layers/node/typescript/src/utils/pushHelper.ts +13 -9
  55. package/templates/layers/node-prisma/javascript/.env.example +3 -1
  56. package/templates/layers/node-prisma/javascript/package.json +11 -2
  57. package/templates/layers/node-prisma/javascript/src/app.js +2 -1
  58. package/templates/layers/node-prisma/javascript/src/controllers/auth.controller.js +90 -135
  59. package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +64 -76
  60. package/templates/layers/node-prisma/javascript/src/controllers/upload.controller.js +12 -19
  61. package/templates/layers/node-prisma/javascript/src/middleware/auth.middleware.js +5 -4
  62. package/templates/layers/node-prisma/javascript/src/middleware/errorHandler.js +6 -4
  63. package/templates/layers/node-prisma/javascript/src/routes/auth.route.js +2 -0
  64. package/templates/layers/node-prisma/javascript/src/services/auth.service.js +145 -122
  65. package/templates/layers/node-prisma/javascript/src/utils/ApiError.js +20 -0
  66. package/templates/layers/node-prisma/javascript/src/utils/SuccessResponse.js +15 -0
  67. package/templates/layers/node-prisma/javascript/src/utils/apiResponse.js +4 -0
  68. package/templates/layers/node-prisma/javascript/src/utils/asyncHandler.js +5 -0
  69. package/templates/layers/node-prisma/typescript/.env.example +3 -1
  70. package/templates/layers/node-prisma/typescript/.eslintrc.cjs +28 -0
  71. package/templates/layers/node-prisma/typescript/package.json +19 -5
  72. package/templates/layers/node-prisma/typescript/src/app.ts +3 -2
  73. package/templates/layers/node-prisma/typescript/src/config/prisma.ts +3 -2
  74. package/templates/layers/node-prisma/typescript/src/config/socket.ts +5 -4
  75. package/templates/layers/node-prisma/typescript/src/controllers/auth.controller.ts +89 -105
  76. package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -83
  77. package/templates/layers/node-prisma/typescript/src/controllers/upload.controller.ts +12 -19
  78. package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +9 -13
  79. package/templates/layers/node-prisma/typescript/src/middleware/errorHandler.ts +18 -5
  80. package/templates/layers/node-prisma/typescript/src/middleware/rateLimiter.ts +2 -2
  81. package/templates/layers/node-prisma/typescript/src/routes/auth.route.ts +2 -0
  82. package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +83 -31
  83. package/templates/layers/node-prisma/typescript/src/types/express.d.ts +5 -3
  84. package/templates/layers/node-prisma/typescript/src/utils/ApiError.ts +25 -0
  85. package/templates/layers/node-prisma/typescript/src/utils/SuccessResponse.ts +17 -0
  86. package/templates/layers/node-prisma/typescript/src/utils/apiResponse.ts +4 -0
  87. package/templates/layers/node-prisma/typescript/src/utils/asyncHandler.ts +13 -0
  88. package/templates/layers/node-prisma/typescript/src/utils/cloudinaryUpload.ts +18 -7
  89. package/templates/layers/node-prisma/typescript/src/utils/genOtpToken.ts +7 -1
  90. package/templates/layers/node-prisma/typescript/src/utils/jwt.ts +8 -2
  91. package/templates/layers/node-prisma/typescript/src/utils/logger.ts +2 -1
  92. package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +10 -6
  93. package/templates/layers/prisma/javascript/package.json +8 -0
  94. package/templates/layers/prisma/typescript/package.json +14 -1
  95. package/templates/layers/prisma/typescript/src/config/db.ts +3 -2
  96. package/templates/layers/react/javascript/package.json +6 -0
  97. package/templates/layers/react/typescript/.eslintrc.cjs +20 -0
  98. package/templates/layers/react/typescript/package.json +11 -2
  99. package/templates/layers/react/typescript/src/components/common/Table.tsx +4 -4
  100. package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +1 -1
  101. package/templates/layers/react/typescript/src/main.tsx +11 -11
  102. package/templates/layers/react/typescript/src/services/authService.ts +1 -1
  103. package/templates/layers/react/typescript/src/store/useAuthStore.ts +24 -9
  104. package/templates/layers/react-native/base/app.json +9 -22
  105. package/templates/layers/react-native/base/eslint.config.js +5 -1
  106. package/templates/layers/react-native/base/package.json +40 -33
  107. package/templates/layers/react-native/base/src/app/(auth)/edit-profile.tsx +7 -7
  108. package/templates/layers/react-native/base/src/app/(auth)/login.tsx +31 -7
  109. package/templates/layers/react-native/base/src/app/(tabs)/_layout.tsx +3 -3
  110. package/templates/layers/react-native/base/src/app/(tabs)/index.tsx +6 -5
  111. package/templates/layers/react-native/base/src/app/_layout.tsx +4 -8
  112. package/templates/layers/react-native/base/src/app/screens/doc-viewer.tsx +378 -0
  113. package/templates/layers/react-native/base/src/components/common/ConfirmationToast.tsx +58 -40
  114. package/templates/layers/react-native/base/src/components/common/GoogleAuthButton.tsx +123 -0
  115. package/templates/layers/react-native/base/src/components/common/LocationLoader.tsx +64 -9
  116. package/templates/layers/react-native/base/src/components/common/Typography.tsx +54 -19
  117. package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +3 -3
  118. package/templates/layers/react-native/base/src/components/ui/ClassicTabBar.tsx +15 -12
  119. package/templates/layers/react-native/base/src/components/ui/CustomTabBar.tsx +90 -91
  120. package/templates/layers/react-native/base/src/components/ui/Drawer.tsx +344 -0
  121. package/templates/layers/react-native/base/src/components/ui/Sidebar.tsx +4 -4
  122. package/templates/layers/react-native/base/src/components/ui/buttons/Button.tsx +29 -6
  123. package/templates/layers/react-native/base/src/components/ui/data-display/Accordion.tsx +2 -2
  124. package/templates/layers/react-native/base/src/components/ui/data-display/Badge.tsx +1 -2
  125. package/templates/layers/react-native/base/src/components/ui/data-display/Image.tsx +3 -3
  126. package/templates/layers/react-native/base/src/components/ui/data-display/SwipeableRow.tsx +28 -7
  127. package/templates/layers/react-native/base/src/components/ui/feedback/GestureWrapper.tsx +11 -4
  128. package/templates/layers/react-native/base/src/components/ui/inputs/DatePicker.tsx +3 -3
  129. package/templates/layers/react-native/base/src/components/ui/inputs/ImagePickerModal.tsx +45 -19
  130. package/templates/layers/react-native/base/src/components/ui/inputs/Input.tsx +28 -9
  131. package/templates/layers/react-native/base/src/components/ui/inputs/SearchablePicker.tsx +199 -0
  132. package/templates/layers/react-native/base/src/components/ui/inputs/Switch.tsx +7 -7
  133. package/templates/layers/react-native/base/src/components/ui/inputs/WebLocationPicker.tsx +146 -72
  134. package/templates/layers/react-native/base/src/config/api.ts +5 -5
  135. package/templates/layers/react-native/base/src/services/auth.service.ts +3 -1
  136. package/templates/layers/react-native/base/src/services/permission.service.ts +168 -44
  137. package/templates/layers/react-native/base/src/services/socket.service.ts +0 -7
  138. package/templates/layers/react-native/base/src/store/useAuthStore.ts +61 -16
  139. package/templates/layers/react-native/base/src/store/useNotificationStore.ts +36 -42
  140. package/templates/layers/react-native/base/src/types/images.d.ts +7 -5
  141. package/templates/layers/react-native/base/src/utils/env.ts +47 -0
  142. package/templates/layers/react-native/base/src/utils/fileDownload.ts +206 -0
  143. package/templates/layers/react-native/base/src/utils/storage.ts +1 -1
  144. package/templates/presets/production-fullstack/overrides/client/package.json +9 -0
  145. package/templates/presets/production-fullstack/overrides/mobile/app.json +2 -15
  146. package/templates/presets/production-fullstack/overrides/mobile/package.json +39 -29
  147. package/templates/presets/production-fullstack/overrides/mobile/src/app/_layout.tsx +3 -3
  148. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/Drawer.tsx +344 -0
  149. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/DatePicker.tsx +3 -3
  150. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/ImagePickerModal.tsx +3 -3
  151. package/templates/presets/production-fullstack/overrides/mobile/src/services/permission.service.ts +168 -44
  152. package/templates/presets/production-fullstack/overrides/mobile/src/store/useNotificationStore.ts +16 -0
  153. package/templates/presets/production-fullstack/overrides/mobile/src/utils/env.ts +47 -0
  154. package/templates/presets/production-fullstack/overrides/server/package.json +11 -2
  155. package/templates/layers/react-native/base/src/app/doc-viewer.tsx +0 -193
  156. package/templates/layers/react-native/base/src/components/ui/BottomModal.tsx +0 -176
  157. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/BottomModal.tsx +0 -159
  158. /package/templates/layers/react-native/base/assets/images/logo/{logoWithBg.png → icon.png} +0 -0
@@ -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
+ });
@@ -1,17 +1,33 @@
1
1
  import { View, TouchableOpacity } from "react-native";
2
- import React from "react";
2
+
3
3
  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
+ import { Portal } from "react-native-paper";
10
+
11
+ export interface ConfirmationToastProps {
12
+ name?: string;
13
+ message: string;
14
+ fun: () => void;
15
+ setShow: (show: boolean) => void;
16
+ cancleText?: string;
17
+ confirmText?: string;
18
+ }
7
19
 
8
20
  export default function ConfirmationToast({
9
21
  name,
10
22
  message,
11
23
  fun,
12
- icon,
13
24
  setShow,
14
- }) {
25
+ cancleText,
26
+ confirmText,
27
+ }: ConfirmationToastProps) {
28
+ const { t } = useTranslation();
29
+ const cText = cancleText ?? t("common.cancel");
30
+ const confText = confirmText ?? t("common.confirm");
15
31
  const { colorScheme } = useColorScheme();
16
32
  const colors = Colors[colorScheme ?? "light"];
17
33
 
@@ -21,47 +37,49 @@ export default function ConfirmationToast({
21
37
  };
22
38
 
23
39
  return (
24
- <View className="absolute top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] w-[90%]">
25
- <View
26
- className="my-4 pt-8 bg-base-100"
27
- style={{
28
- borderRadius: 20,
29
- shadowColor: "#000",
30
- shadowOffset: {
31
- width: 0,
32
- height: 4,
33
- },
34
- shadowOpacity: 0.1,
35
- shadowRadius: 8,
36
- elevation: 8,
37
- }}
38
- >
39
- <View className="justify-center items-center px-8">
40
- <Txt variant="h1">{name}</Txt>
41
- <Txt variant="body" className="mt-4" style={{ textAlign: "center" }}>
42
- {message}
43
- </Txt>
44
- </View>
40
+ <Portal>
41
+ <View className="absolute inset-0 justify-center items-center bg-black/40 z-50">
45
42
  <View
46
- className="flex-row mt-8"
47
- style={{ borderTopWidth: 1, borderTopColor: colors.base300 }}
43
+ className="w-[90%] pt-8 bg-base-100"
44
+ style={{
45
+ borderRadius: 20,
46
+ shadowColor: "#000",
47
+ shadowOffset: {
48
+ width: 0,
49
+ height: 4,
50
+ },
51
+ shadowOpacity: 0.1,
52
+ shadowRadius: 8,
53
+ elevation: 8,
54
+ }}
48
55
  >
49
- <TouchableOpacity
50
- className="w-[50%] justify-center items-center h-20"
51
- style={{ borderRightWidth: 1, borderRightColor: colors.base300 }}
52
- onPress={() => setShow(false)}
53
- >
54
- <Txt variant="mid">Cancle</Txt>
55
- </TouchableOpacity>
56
- <TouchableOpacity
57
- className="w-[50%] justify-center items-center"
58
- style={{ borderLeftWidth: 1, borderLeftColor: colors.base300 }}
59
- onPress={ok}
56
+ <View className="justify-center items-center px-8">
57
+ <Txt variant="xl">{name}</Txt>
58
+ <Txt variant="sm" className="mt-4" style={{ textAlign: "center" }}>
59
+ {message}
60
+ </Txt>
61
+ </View>
62
+ <View
63
+ className="flex-row mt-8"
64
+ style={{ borderTopWidth: 1, borderTopColor: colors.base300 }}
60
65
  >
61
- <Txt variant="mid">Ok</Txt>
62
- </TouchableOpacity>
66
+ <TouchableOpacity
67
+ className="w-[50%] justify-center items-center h-20"
68
+ style={{ borderRightWidth: 1, borderRightColor: colors.base300 }}
69
+ onPress={() => setShow(false)}
70
+ >
71
+ <Txt variant="sm">{cText}</Txt>
72
+ </TouchableOpacity>
73
+ <TouchableOpacity
74
+ className="w-[50%] justify-center items-center"
75
+ style={{ borderLeftWidth: 1, borderLeftColor: colors.base300 }}
76
+ onPress={ok}
77
+ >
78
+ <Txt variant="sm">{confText}</Txt>
79
+ </TouchableOpacity>
80
+ </View>
63
81
  </View>
64
82
  </View>
65
- </View>
83
+ </Portal>
66
84
  );
67
85
  }
@@ -0,0 +1,123 @@
1
+ import { useState } from "react";
2
+ import { TouchableOpacity, View, ActivityIndicator } from "react-native";
3
+ import { useColorScheme } from "nativewind";
4
+ import { Colors } from "./../../constants/Colors";
5
+ import { Txt } from "./Typography";
6
+ import { useAuthStore } from "./../../store/useAuthStore";
7
+ import { toast } from "./../../utils/toast";
8
+ import { Ionicons } from "@expo/vector-icons";
9
+ import { isExpoGo, hasNativeModule } from "../../utils/env";
10
+
11
+ interface GoogleAuthButtonProps {
12
+ onSuccess?: () => void;
13
+ label?: string;
14
+ isLogin?: boolean;
15
+ }
16
+
17
+ export default function GoogleAuthButton({
18
+ onSuccess,
19
+ label,
20
+ isLogin = true,
21
+ }: GoogleAuthButtonProps) {
22
+ const { colorScheme } = useColorScheme();
23
+ const colors = Colors[colorScheme ?? "light"];
24
+ const { googleLogin } = useAuthStore();
25
+ const [loading, setLoading] = useState(false);
26
+
27
+ const handlePress = async () => {
28
+ if (isExpoGo() || !hasNativeModule("RNGoogleSignin")) {
29
+ toast.info(
30
+ "Development Build Required",
31
+ "Google Sign-In requires a custom development build (expo-dev-client)."
32
+ );
33
+ return;
34
+ }
35
+
36
+ try {
37
+ setLoading(true);
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 });
55
+
56
+ // Force account selection every time
57
+ try {
58
+ await GoogleSignin.signOut();
59
+ } catch {
60
+ // Ignore if already signed out
61
+ }
62
+
63
+ const userInfo = await GoogleSignin.signIn();
64
+ const idToken = userInfo.data?.idToken;
65
+
66
+ if (!idToken) {
67
+ throw new Error("No ID token returned from Google");
68
+ }
69
+
70
+ const ok = await googleLogin(idToken);
71
+ if (ok) {
72
+ const currentUser = useAuthStore.getState().user;
73
+ toast.success("Success", `Welcome back, ${currentUser?.name || "User"}`);
74
+ onSuccess?.();
75
+ } else {
76
+ toast.error("Error", "Google authentication failed");
77
+ }
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
+
85
+ const err = error as { code?: string; message?: string };
86
+ if (err.code === statusCodes?.SIGN_IN_CANCELLED) {
87
+ // User cancelled the login flow
88
+ } else if (err.code === statusCodes?.IN_PROGRESS) {
89
+ // Operation is in progress already
90
+ } else if (err.code === statusCodes?.PLAY_SERVICES_NOT_AVAILABLE) {
91
+ toast.error("Error", "Google Play Services not available");
92
+ } else {
93
+ toast.error("Error", err.message || "An unexpected error occurred");
94
+ }
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ };
99
+
100
+ return (
101
+ <TouchableOpacity
102
+ activeOpacity={0.8}
103
+ onPress={handlePress}
104
+ disabled={loading}
105
+ className="w-full flex-row items-center justify-center py-4 rounded-xl mt-4 border"
106
+ style={{
107
+ backgroundColor: colors.base100,
108
+ borderColor: colors.base300,
109
+ }}
110
+ >
111
+ {loading ? (
112
+ <ActivityIndicator color={colors.primary} />
113
+ ) : (
114
+ <View className="flex-row items-center justify-center gap-2">
115
+ <Ionicons name="logo-google" size={20} color={colors.baseContent} />
116
+ <Txt className="font-semibold text-base text-base-content">
117
+ {label || (isLogin ? "Continue with Google" : "Sign up with Google")}
118
+ </Txt>
119
+ </View>
120
+ )}
121
+ </TouchableOpacity>
122
+ );
123
+ }