@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 +1 -1
- package/templates/layers/react-native/base/package.json +32 -35
- package/templates/layers/react-native/base/src/app/_layout.tsx +1 -5
- package/templates/layers/react-native/base/src/app/screens/doc-viewer.tsx +378 -0
- package/templates/layers/react-native/base/src/components/common/ConfirmationToast.tsx +2 -5
- package/templates/layers/react-native/base/src/components/common/GoogleAuthButton.tsx +33 -54
- package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +3 -3
- package/templates/layers/react-native/base/src/components/ui/CustomTabBar.tsx +14 -9
- package/templates/layers/react-native/base/src/services/permission.service.ts +168 -42
- package/templates/layers/react-native/base/src/store/useNotificationStore.ts +2 -2
- package/templates/layers/react-native/base/src/types/common.type.ts +17 -0
- package/templates/layers/react-native/base/src/utils/env.ts +47 -0
- package/templates/layers/react-native/base/src/utils/fileDownload.ts +2 -2
- package/templates/presets/production-fullstack/overrides/mobile/package.json +32 -32
- package/templates/presets/production-fullstack/overrides/mobile/src/services/permission.service.ts +168 -44
- package/templates/presets/production-fullstack/overrides/mobile/src/store/useNotificationStore.ts +16 -0
- package/templates/presets/production-fullstack/overrides/mobile/src/utils/env.ts +47 -0
- package/templates/layers/react-native/base/src/app/doc-viewer.tsx +0 -193
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "Mobile",
|
|
3
3
|
"main": "expo-router/entry",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@expo/ui": "~57.0.
|
|
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.
|
|
17
|
-
"expo-blur": "~57.0.
|
|
17
|
+
"expo": "~57.0.27",
|
|
18
|
+
"expo-blur": "~57.0.3",
|
|
18
19
|
"expo-constants": "~57.0.6",
|
|
19
|
-
"expo-device": "~57.0.
|
|
20
|
-
"expo-file-system": "~
|
|
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.
|
|
24
|
-
"expo-image": "~57.0.
|
|
25
|
-
"expo-image-picker": "
|
|
26
|
-
"expo-linear-gradient": "
|
|
27
|
-
"expo-linking": "~57.0.
|
|
28
|
-
"expo-local-authentication": "~
|
|
29
|
-
"expo-location": "~
|
|
30
|
-
"expo-notifications": "~57.0.
|
|
31
|
-
"
|
|
32
|
-
"expo-
|
|
33
|
-
"expo-splash-screen": "~57.0.
|
|
34
|
-
"expo-sqlite": "
|
|
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.
|
|
37
|
-
"expo-system-ui": "~57.0.
|
|
38
|
-
"expo-web-browser": "~57.0.
|
|
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.
|
|
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": "^
|
|
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": "
|
|
56
|
-
"react-native-safe-area-context": "
|
|
57
|
-
"react-native-screens": "4.
|
|
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": "
|
|
61
|
-
"react-native-worklets": "0.10.
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
29
|
-
const
|
|
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 {
|
|
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 "
|
|
4
|
+
import { Colors } from "./../../constants/Colors";
|
|
6
5
|
import { Txt } from "./Typography";
|
|
7
|
-
import { useAuthStore } from "
|
|
8
|
-
import { toast } from "
|
|
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
|
-
"
|
|
67
|
-
"Google Sign-In
|
|
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
|
-
|
|
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
|
-
//
|
|
87
|
+
// User cancelled the login flow
|
|
107
88
|
} else if (err.code === statusCodes?.IN_PROGRESS) {
|
|
108
|
-
//
|
|
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 "
|
|
9
|
-
import Button from "
|
|
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(
|
|
63
|
+
const success = await authenticateAsync(`Unlock ${appConfig.app.name}`);
|
|
64
64
|
|
|
65
65
|
if (success) {
|
|
66
66
|
setIsAppLocked(false);
|