@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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/commands/create.js +2 -0
- package/src/index.js +1 -1
- package/templates/layers/next/javascript/package.json +8 -2
- package/templates/layers/next/typescript/.eslintrc.json +12 -0
- package/templates/layers/next/typescript/package.json +14 -3
- package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -1
- package/templates/layers/next/typescript/src/components/common/LottieIcon.tsx +2 -2
- package/templates/layers/next/typescript/src/components/common/Table.tsx +4 -4
- package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +2 -1
- package/templates/layers/next/typescript/src/components/ui/Header.tsx +2 -2
- package/templates/layers/next/typescript/src/hooks/usePwaInstall.ts +10 -4
- package/templates/layers/next/typescript/src/services/authService.ts +1 -1
- package/templates/layers/next/typescript/src/store/useAuthStore.ts +24 -9
- package/templates/layers/node/javascript/.env.example +3 -1
- package/templates/layers/node/javascript/package.json +12 -3
- package/templates/layers/node/javascript/src/app.js +2 -1
- package/templates/layers/node/javascript/src/controllers/auth.controller.js +106 -168
- package/templates/layers/node/javascript/src/controllers/push.controller.js +51 -58
- package/templates/layers/node/javascript/src/controllers/upload.controller.js +12 -19
- package/templates/layers/node/javascript/src/middleware/auth.middleware.js +5 -11
- package/templates/layers/node/javascript/src/middleware/errorHandler.js +6 -4
- package/templates/layers/node/javascript/src/routes/auth.route.js +2 -0
- package/templates/layers/node/javascript/src/services/auth.service.js +159 -164
- package/templates/layers/node/javascript/src/utils/ApiError.js +20 -0
- package/templates/layers/node/javascript/src/utils/SuccessResponse.js +15 -0
- package/templates/layers/node/javascript/src/utils/apiResponse.js +4 -0
- package/templates/layers/node/javascript/src/utils/asyncHandler.js +5 -0
- package/templates/layers/node/typescript/.env.example +3 -1
- package/templates/layers/node/typescript/.eslintrc.cjs +28 -0
- package/templates/layers/node/typescript/package.json +16 -2
- package/templates/layers/node/typescript/src/app.ts +3 -2
- package/templates/layers/node/typescript/src/config/db.ts +3 -2
- package/templates/layers/node/typescript/src/config/socket.ts +5 -4
- package/templates/layers/node/typescript/src/controllers/auth.controller.ts +104 -132
- package/templates/layers/node/typescript/src/controllers/push.controller.ts +52 -59
- package/templates/layers/node/typescript/src/controllers/upload.controller.ts +12 -19
- package/templates/layers/node/typescript/src/middleware/auth.middleware.ts +13 -7
- package/templates/layers/node/typescript/src/middleware/errorHandler.ts +18 -5
- package/templates/layers/node/typescript/src/middleware/rateLimiter.ts +2 -2
- package/templates/layers/node/typescript/src/routes/auth.route.ts +2 -0
- package/templates/layers/node/typescript/src/services/auth.service.ts +86 -43
- package/templates/layers/node/typescript/src/types/express.d.ts +5 -3
- package/templates/layers/node/typescript/src/utils/ApiError.ts +25 -0
- package/templates/layers/node/typescript/src/utils/SuccessResponse.ts +17 -0
- package/templates/layers/node/typescript/src/utils/apiResponse.ts +4 -0
- package/templates/layers/node/typescript/src/utils/asyncHandler.ts +13 -0
- package/templates/layers/node/typescript/src/utils/cloudinaryUpload.ts +18 -7
- package/templates/layers/node/typescript/src/utils/expoNotification.ts +1 -1
- package/templates/layers/node/typescript/src/utils/genOtpToken.ts +7 -1
- package/templates/layers/node/typescript/src/utils/jwt.ts +8 -2
- package/templates/layers/node/typescript/src/utils/logger.ts +2 -1
- package/templates/layers/node/typescript/src/utils/pushHelper.ts +13 -9
- package/templates/layers/node-prisma/javascript/.env.example +3 -1
- package/templates/layers/node-prisma/javascript/package.json +11 -2
- package/templates/layers/node-prisma/javascript/src/app.js +2 -1
- package/templates/layers/node-prisma/javascript/src/controllers/auth.controller.js +90 -135
- package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +64 -76
- package/templates/layers/node-prisma/javascript/src/controllers/upload.controller.js +12 -19
- package/templates/layers/node-prisma/javascript/src/middleware/auth.middleware.js +5 -4
- package/templates/layers/node-prisma/javascript/src/middleware/errorHandler.js +6 -4
- package/templates/layers/node-prisma/javascript/src/routes/auth.route.js +2 -0
- package/templates/layers/node-prisma/javascript/src/services/auth.service.js +145 -122
- package/templates/layers/node-prisma/javascript/src/utils/ApiError.js +20 -0
- package/templates/layers/node-prisma/javascript/src/utils/SuccessResponse.js +15 -0
- package/templates/layers/node-prisma/javascript/src/utils/apiResponse.js +4 -0
- package/templates/layers/node-prisma/javascript/src/utils/asyncHandler.js +5 -0
- package/templates/layers/node-prisma/typescript/.env.example +3 -1
- package/templates/layers/node-prisma/typescript/.eslintrc.cjs +28 -0
- package/templates/layers/node-prisma/typescript/package.json +19 -5
- package/templates/layers/node-prisma/typescript/src/app.ts +3 -2
- package/templates/layers/node-prisma/typescript/src/config/prisma.ts +3 -2
- package/templates/layers/node-prisma/typescript/src/config/socket.ts +5 -4
- package/templates/layers/node-prisma/typescript/src/controllers/auth.controller.ts +89 -105
- package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -83
- package/templates/layers/node-prisma/typescript/src/controllers/upload.controller.ts +12 -19
- package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +9 -13
- package/templates/layers/node-prisma/typescript/src/middleware/errorHandler.ts +18 -5
- package/templates/layers/node-prisma/typescript/src/middleware/rateLimiter.ts +2 -2
- package/templates/layers/node-prisma/typescript/src/routes/auth.route.ts +2 -0
- package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +83 -31
- package/templates/layers/node-prisma/typescript/src/types/express.d.ts +5 -3
- package/templates/layers/node-prisma/typescript/src/utils/ApiError.ts +25 -0
- package/templates/layers/node-prisma/typescript/src/utils/SuccessResponse.ts +17 -0
- package/templates/layers/node-prisma/typescript/src/utils/apiResponse.ts +4 -0
- package/templates/layers/node-prisma/typescript/src/utils/asyncHandler.ts +13 -0
- package/templates/layers/node-prisma/typescript/src/utils/cloudinaryUpload.ts +18 -7
- package/templates/layers/node-prisma/typescript/src/utils/genOtpToken.ts +7 -1
- package/templates/layers/node-prisma/typescript/src/utils/jwt.ts +8 -2
- package/templates/layers/node-prisma/typescript/src/utils/logger.ts +2 -1
- package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +10 -6
- package/templates/layers/prisma/javascript/package.json +8 -0
- package/templates/layers/prisma/typescript/package.json +14 -1
- package/templates/layers/prisma/typescript/src/config/db.ts +3 -2
- package/templates/layers/react/javascript/package.json +6 -0
- package/templates/layers/react/typescript/.eslintrc.cjs +20 -0
- package/templates/layers/react/typescript/package.json +11 -2
- package/templates/layers/react/typescript/src/components/common/Table.tsx +4 -4
- package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +1 -1
- package/templates/layers/react/typescript/src/main.tsx +11 -11
- package/templates/layers/react/typescript/src/services/authService.ts +1 -1
- package/templates/layers/react/typescript/src/store/useAuthStore.ts +24 -9
- package/templates/layers/react-native/base/app.json +9 -22
- package/templates/layers/react-native/base/eslint.config.js +5 -1
- package/templates/layers/react-native/base/package.json +40 -33
- package/templates/layers/react-native/base/src/app/(auth)/edit-profile.tsx +7 -7
- package/templates/layers/react-native/base/src/app/(auth)/login.tsx +31 -7
- package/templates/layers/react-native/base/src/app/(tabs)/_layout.tsx +3 -3
- package/templates/layers/react-native/base/src/app/(tabs)/index.tsx +6 -5
- package/templates/layers/react-native/base/src/app/_layout.tsx +4 -8
- 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 +58 -40
- package/templates/layers/react-native/base/src/components/common/GoogleAuthButton.tsx +123 -0
- package/templates/layers/react-native/base/src/components/common/LocationLoader.tsx +64 -9
- package/templates/layers/react-native/base/src/components/common/Typography.tsx +54 -19
- package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +3 -3
- package/templates/layers/react-native/base/src/components/ui/ClassicTabBar.tsx +15 -12
- package/templates/layers/react-native/base/src/components/ui/CustomTabBar.tsx +90 -91
- package/templates/layers/react-native/base/src/components/ui/Drawer.tsx +344 -0
- package/templates/layers/react-native/base/src/components/ui/Sidebar.tsx +4 -4
- package/templates/layers/react-native/base/src/components/ui/buttons/Button.tsx +29 -6
- package/templates/layers/react-native/base/src/components/ui/data-display/Accordion.tsx +2 -2
- package/templates/layers/react-native/base/src/components/ui/data-display/Badge.tsx +1 -2
- package/templates/layers/react-native/base/src/components/ui/data-display/Image.tsx +3 -3
- package/templates/layers/react-native/base/src/components/ui/data-display/SwipeableRow.tsx +28 -7
- package/templates/layers/react-native/base/src/components/ui/feedback/GestureWrapper.tsx +11 -4
- package/templates/layers/react-native/base/src/components/ui/inputs/DatePicker.tsx +3 -3
- package/templates/layers/react-native/base/src/components/ui/inputs/ImagePickerModal.tsx +45 -19
- package/templates/layers/react-native/base/src/components/ui/inputs/Input.tsx +28 -9
- package/templates/layers/react-native/base/src/components/ui/inputs/SearchablePicker.tsx +199 -0
- package/templates/layers/react-native/base/src/components/ui/inputs/Switch.tsx +7 -7
- package/templates/layers/react-native/base/src/components/ui/inputs/WebLocationPicker.tsx +146 -72
- package/templates/layers/react-native/base/src/config/api.ts +5 -5
- package/templates/layers/react-native/base/src/services/auth.service.ts +3 -1
- package/templates/layers/react-native/base/src/services/permission.service.ts +168 -44
- package/templates/layers/react-native/base/src/services/socket.service.ts +0 -7
- package/templates/layers/react-native/base/src/store/useAuthStore.ts +61 -16
- package/templates/layers/react-native/base/src/store/useNotificationStore.ts +36 -42
- package/templates/layers/react-native/base/src/types/images.d.ts +7 -5
- package/templates/layers/react-native/base/src/utils/env.ts +47 -0
- package/templates/layers/react-native/base/src/utils/fileDownload.ts +206 -0
- package/templates/layers/react-native/base/src/utils/storage.ts +1 -1
- package/templates/presets/production-fullstack/overrides/client/package.json +9 -0
- package/templates/presets/production-fullstack/overrides/mobile/app.json +2 -15
- package/templates/presets/production-fullstack/overrides/mobile/package.json +39 -29
- package/templates/presets/production-fullstack/overrides/mobile/src/app/_layout.tsx +3 -3
- package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/Drawer.tsx +344 -0
- package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/DatePicker.tsx +3 -3
- package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/ImagePickerModal.tsx +3 -3
- 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/presets/production-fullstack/overrides/server/package.json +11 -2
- package/templates/layers/react-native/base/src/app/doc-viewer.tsx +0 -193
- package/templates/layers/react-native/base/src/components/ui/BottomModal.tsx +0 -176
- package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/BottomModal.tsx +0 -159
- /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
|
-
|
|
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
|
-
<
|
|
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="
|
|
47
|
-
style={{
|
|
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
|
-
<
|
|
50
|
-
|
|
51
|
-
style={{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
<
|
|
62
|
-
|
|
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
|
-
</
|
|
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
|
+
}
|