@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
|
@@ -10,6 +10,7 @@ import Input from "./Input";
|
|
|
10
10
|
import { toast } from "@/utils/toast";
|
|
11
11
|
import { WebView, WebViewMessageEvent } from "react-native-webview";
|
|
12
12
|
import { LocationLoader } from "../../common/LocationLoader";
|
|
13
|
+
import ScreenHeader from "@/components/common/ScreenHeader";
|
|
13
14
|
|
|
14
15
|
export interface LocationDetails {
|
|
15
16
|
addressLine1: string;
|
|
@@ -40,11 +41,12 @@ export default function WebLocationPicker({
|
|
|
40
41
|
const [loading, setLoading] = useState(false);
|
|
41
42
|
const [searchQuery, setSearchQuery] = useState("");
|
|
42
43
|
const [searching, setSearching] = useState(false);
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
const [loadingLocation, setLoadingLocation] = useState(false);
|
|
45
46
|
const [animationDone, setAnimationDone] = useState(false);
|
|
46
47
|
const [positionFetched, setPositionFetched] = useState(false);
|
|
47
|
-
|
|
48
|
+
const [showWebView, setShowWebView] = useState(true);
|
|
49
|
+
|
|
48
50
|
const [mapCenter, setMapCenter] = useState({
|
|
49
51
|
latitude: initialCoordinate?.latitude || 28.6139,
|
|
50
52
|
longitude: initialCoordinate?.longitude || 77.209,
|
|
@@ -56,7 +58,6 @@ export default function WebLocationPicker({
|
|
|
56
58
|
|
|
57
59
|
useEffect(() => {
|
|
58
60
|
if (initialCoordinate?.latitude && initialCoordinate?.longitude) {
|
|
59
|
-
|
|
60
61
|
setMapCenter(initialCoordinate);
|
|
61
62
|
setCurrentCenter(initialCoordinate);
|
|
62
63
|
}
|
|
@@ -69,6 +70,7 @@ export default function WebLocationPicker({
|
|
|
69
70
|
}, [animationDone, positionFetched]);
|
|
70
71
|
|
|
71
72
|
const requestLocationAndOpenMap = async () => {
|
|
73
|
+
setShowWebView(true);
|
|
72
74
|
setModalVisible(true);
|
|
73
75
|
setLoadingLocation(true);
|
|
74
76
|
setAnimationDone(false);
|
|
@@ -84,7 +86,9 @@ export default function WebLocationPicker({
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
if (!initialCoordinate?.latitude) {
|
|
87
|
-
let location = await Location.getCurrentPositionAsync({
|
|
89
|
+
let location = await Location.getCurrentPositionAsync({
|
|
90
|
+
accuracy: Location.Accuracy.Balanced,
|
|
91
|
+
});
|
|
88
92
|
setMapCenter({
|
|
89
93
|
latitude: location.coords.latitude,
|
|
90
94
|
longitude: location.coords.longitude,
|
|
@@ -120,8 +124,15 @@ export default function WebLocationPicker({
|
|
|
120
124
|
latitude: currentCenter.latitude,
|
|
121
125
|
longitude: currentCenter.longitude,
|
|
122
126
|
};
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
|
|
128
|
+
// Hide WebView first to prevent Android crash when Modal closes
|
|
129
|
+
setShowWebView(false);
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
onLocationSelected(details);
|
|
132
|
+
setModalVisible(false);
|
|
133
|
+
setLoading(false);
|
|
134
|
+
}, 150);
|
|
135
|
+
return;
|
|
125
136
|
}
|
|
126
137
|
} catch (error) {
|
|
127
138
|
console.warn("Geocoding failed", error);
|
|
@@ -129,6 +140,37 @@ export default function WebLocationPicker({
|
|
|
129
140
|
setLoading(false);
|
|
130
141
|
};
|
|
131
142
|
|
|
143
|
+
const [fetchingCurrentLocation, setFetchingCurrentLocation] = useState(false);
|
|
144
|
+
|
|
145
|
+
const handleCurrentLocation = async () => {
|
|
146
|
+
setFetchingCurrentLocation(true);
|
|
147
|
+
try {
|
|
148
|
+
let { status } = await Location.requestForegroundPermissionsAsync();
|
|
149
|
+
if (status !== "granted") {
|
|
150
|
+
toast.error("Permission to access location was denied");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let location = await Location.getCurrentPositionAsync({
|
|
155
|
+
accuracy: Location.Accuracy.Balanced,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const lat = location.coords.latitude;
|
|
159
|
+
const lng = location.coords.longitude;
|
|
160
|
+
|
|
161
|
+
setCurrentCenter({ latitude: lat, longitude: lng });
|
|
162
|
+
webViewRef.current?.injectJavaScript(`
|
|
163
|
+
map.setView([${lat}, ${lng}], 15);
|
|
164
|
+
true;
|
|
165
|
+
`);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn("Failed to get current location", error);
|
|
168
|
+
toast.error("Failed to get current location");
|
|
169
|
+
} finally {
|
|
170
|
+
setFetchingCurrentLocation(false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
132
174
|
const handleSearch = async () => {
|
|
133
175
|
if (!searchQuery.trim()) return;
|
|
134
176
|
setSearching(true);
|
|
@@ -142,11 +184,10 @@ export default function WebLocationPicker({
|
|
|
142
184
|
true;
|
|
143
185
|
`);
|
|
144
186
|
} else {
|
|
145
|
-
toast.
|
|
187
|
+
toast.error("Location not found");
|
|
146
188
|
}
|
|
147
|
-
} catch
|
|
148
|
-
|
|
149
|
-
toast.error("Failed to search location");
|
|
189
|
+
} catch {
|
|
190
|
+
toast.error("Failed to search");
|
|
150
191
|
}
|
|
151
192
|
setSearching(false);
|
|
152
193
|
};
|
|
@@ -163,7 +204,8 @@ export default function WebLocationPicker({
|
|
|
163
204
|
} catch {}
|
|
164
205
|
};
|
|
165
206
|
|
|
166
|
-
const htmlContent = useMemo(
|
|
207
|
+
const htmlContent = useMemo(
|
|
208
|
+
() => `
|
|
167
209
|
<!DOCTYPE html>
|
|
168
210
|
<html>
|
|
169
211
|
<head>
|
|
@@ -188,7 +230,7 @@ export default function WebLocationPicker({
|
|
|
188
230
|
<div class="center-marker">📍</div>
|
|
189
231
|
<script>
|
|
190
232
|
var map = L.map('map', { zoomControl: false, attributionControl: false }).setView([${mapCenter.latitude}, ${mapCenter.longitude}], 15);
|
|
191
|
-
L.tileLayer('https://{s}.
|
|
233
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
192
234
|
maxZoom: 19
|
|
193
235
|
}).addTo(map);
|
|
194
236
|
|
|
@@ -203,7 +245,9 @@ export default function WebLocationPicker({
|
|
|
203
245
|
</script>
|
|
204
246
|
</body>
|
|
205
247
|
</html>
|
|
206
|
-
`,
|
|
248
|
+
`,
|
|
249
|
+
[mapCenter.latitude, mapCenter.longitude],
|
|
250
|
+
);
|
|
207
251
|
|
|
208
252
|
return (
|
|
209
253
|
<>
|
|
@@ -223,14 +267,18 @@ export default function WebLocationPicker({
|
|
|
223
267
|
/>
|
|
224
268
|
<Txt color={theme.baseContent} weight="semibold">
|
|
225
269
|
{initialCoordinate?.latitude
|
|
226
|
-
? "Update Location
|
|
227
|
-
: "Pick from
|
|
270
|
+
? "Update Location"
|
|
271
|
+
: "Pick from map"}
|
|
228
272
|
</Txt>
|
|
229
273
|
</View>
|
|
230
274
|
{loading && !modalVisible ? (
|
|
231
275
|
<ActivityIndicator color={theme.primary} />
|
|
232
276
|
) : (
|
|
233
|
-
<Ionicons
|
|
277
|
+
<Ionicons
|
|
278
|
+
name="chevron-forward"
|
|
279
|
+
size={20}
|
|
280
|
+
color={theme.secondary}
|
|
281
|
+
/>
|
|
234
282
|
)}
|
|
235
283
|
</TouchableOpacity>
|
|
236
284
|
)}
|
|
@@ -251,68 +299,94 @@ export default function WebLocationPicker({
|
|
|
251
299
|
</View>
|
|
252
300
|
) : (
|
|
253
301
|
<View className="flex-1 bg-base-100">
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
<Ionicons name="close" size={28} color={theme.baseContent} />
|
|
260
|
-
</TouchableOpacity>
|
|
261
|
-
<Txt weight="bold" style={{ fontSize: 18 }}>
|
|
262
|
-
Select Location
|
|
263
|
-
</Txt>
|
|
264
|
-
<View style={{ width: 28 }} />
|
|
265
|
-
</View>
|
|
302
|
+
<ScreenHeader
|
|
303
|
+
icon="close"
|
|
304
|
+
onBack={() => setModalVisible(false)}
|
|
305
|
+
title="Select Location"
|
|
306
|
+
/>
|
|
266
307
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
308
|
+
<View className="px-4 py-3 bg-base-100 flex-row items-center border-b border-base-200 gap-4">
|
|
309
|
+
<View className="flex-1" style={{ marginBottom: -8 }}>
|
|
310
|
+
<Input
|
|
311
|
+
placeholder="Search Location"
|
|
312
|
+
value={searchQuery}
|
|
313
|
+
onChangeText={setSearchQuery}
|
|
314
|
+
onSubmitEditing={handleSearch}
|
|
315
|
+
returnKeyType="search"
|
|
316
|
+
containerStyle={{ marginBottom: 0 }}
|
|
317
|
+
leftIcon="search"
|
|
318
|
+
/>
|
|
319
|
+
</View>
|
|
320
|
+
<Button
|
|
321
|
+
onPress={handleSearch}
|
|
322
|
+
isLoading={searching}
|
|
323
|
+
label="Go"
|
|
324
|
+
variant="primary"
|
|
325
|
+
soft
|
|
326
|
+
size="md"
|
|
327
|
+
className=""
|
|
277
328
|
/>
|
|
278
329
|
</View>
|
|
279
|
-
<Button
|
|
280
|
-
onPress={handleSearch}
|
|
281
|
-
isLoading={searching}
|
|
282
|
-
label="Go"
|
|
283
|
-
variant="primary"
|
|
284
|
-
soft
|
|
285
|
-
size="md"
|
|
286
|
-
className="my-auto"
|
|
287
|
-
/>
|
|
288
|
-
</View>
|
|
289
330
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
<View className="flex-1 relative">
|
|
332
|
+
{showWebView && (
|
|
333
|
+
<WebView
|
|
334
|
+
ref={webViewRef}
|
|
335
|
+
source={{ html: htmlContent }}
|
|
336
|
+
onMessage={handleMessage}
|
|
337
|
+
style={{ flex: 1 }}
|
|
338
|
+
javaScriptEnabled={true}
|
|
339
|
+
scrollEnabled={false}
|
|
340
|
+
bounces={false}
|
|
341
|
+
startInLoadingState={true}
|
|
342
|
+
renderLoading={() => (
|
|
343
|
+
<View
|
|
344
|
+
style={{
|
|
345
|
+
position: "absolute",
|
|
346
|
+
top: 0,
|
|
347
|
+
left: 0,
|
|
348
|
+
right: 0,
|
|
349
|
+
bottom: 0,
|
|
350
|
+
justifyContent: "center",
|
|
351
|
+
alignItems: "center",
|
|
352
|
+
backgroundColor: "#f0f0f0",
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
<ActivityIndicator size="large" color={theme.primary} />
|
|
356
|
+
</View>
|
|
357
|
+
)}
|
|
358
|
+
/>
|
|
304
359
|
)}
|
|
305
|
-
/>
|
|
306
|
-
</View>
|
|
307
360
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
361
|
+
<TouchableOpacity
|
|
362
|
+
onPress={handleCurrentLocation}
|
|
363
|
+
disabled={fetchingCurrentLocation}
|
|
364
|
+
className="absolute bottom-6 right-4 w-12 h-12 rounded-full items-center justify-center shadow-lg border border-base-200"
|
|
365
|
+
style={{
|
|
366
|
+
backgroundColor: theme.base100,
|
|
367
|
+
elevation: 5,
|
|
368
|
+
shadowColor: "#000",
|
|
369
|
+
shadowOffset: { width: 0, height: 2 },
|
|
370
|
+
shadowOpacity: 0.25,
|
|
371
|
+
shadowRadius: 3.84,
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
{fetchingCurrentLocation ? (
|
|
375
|
+
<ActivityIndicator size="small" color={theme.primary} />
|
|
376
|
+
) : (
|
|
377
|
+
<Ionicons name="locate" size={24} color={theme.primary} />
|
|
378
|
+
)}
|
|
379
|
+
</TouchableOpacity>
|
|
380
|
+
</View>
|
|
381
|
+
|
|
382
|
+
<View className="p-4 bg-base-100 border-t border-base-300 pb-8">
|
|
383
|
+
<Button
|
|
384
|
+
label="Confirm Location"
|
|
385
|
+
onPress={handleConfirmLocation}
|
|
386
|
+
isLoading={loading}
|
|
387
|
+
variant="primary"
|
|
388
|
+
/>
|
|
389
|
+
</View>
|
|
316
390
|
</View>
|
|
317
391
|
)}
|
|
318
392
|
</Modal>
|
|
@@ -4,11 +4,11 @@ import { ApiResponse } from "../types/common.type";
|
|
|
4
4
|
|
|
5
5
|
declare module 'axios' {
|
|
6
6
|
export interface AxiosInstance {
|
|
7
|
-
get<T =
|
|
8
|
-
delete<T =
|
|
9
|
-
post<T =
|
|
10
|
-
put<T =
|
|
11
|
-
patch<T =
|
|
7
|
+
get<T = unknown, R = AxiosResponse<ApiResponse<T>>, D = unknown>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
|
|
8
|
+
delete<T = unknown, R = AxiosResponse<ApiResponse<T>>, D = unknown>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
|
|
9
|
+
post<T = unknown, R = AxiosResponse<ApiResponse<T>>, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
|
|
10
|
+
put<T = unknown, R = AxiosResponse<ApiResponse<T>>, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
|
|
11
|
+
patch<T = unknown, R = AxiosResponse<ApiResponse<T>>, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -14,9 +14,11 @@ export const authService = {
|
|
|
14
14
|
login: (email: string, password: string) =>
|
|
15
15
|
api.post("/auth/login", { email, password }),
|
|
16
16
|
|
|
17
|
+
googleLogin: (idToken: string) => api.post("/auth/google", { idToken }),
|
|
18
|
+
|
|
17
19
|
getMe: () => api.get("/auth/me"),
|
|
18
20
|
|
|
19
|
-
updateMe: (data:
|
|
21
|
+
updateMe: (data: FormData | Record<string, unknown>) => api.put("/auth/me", data),
|
|
20
22
|
|
|
21
23
|
logout: () => api.post("/auth/logout"),
|
|
22
24
|
|
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
import { Platform, Alert } from "react-native";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
1
|
+
import { Platform, Alert, Linking } from "react-native";
|
|
2
|
+
import { isExpoGo, hasNativeModule } from "@/utils/env";
|
|
3
|
+
|
|
4
|
+
export const RESULTS = {
|
|
5
|
+
UNAVAILABLE: "unavailable",
|
|
6
|
+
DENIED: "denied",
|
|
7
|
+
BLOCKED: "blocked",
|
|
8
|
+
GRANTED: "granted",
|
|
9
|
+
LIMITED: "limited",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type PermissionStatus = (typeof RESULTS)[keyof typeof RESULTS];
|
|
13
|
+
export type Permission = string;
|
|
14
|
+
|
|
15
|
+
export const openSettings = async (): Promise<void> => {
|
|
16
|
+
try {
|
|
17
|
+
await Linking.openSettings();
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.warn("Failed to open device settings", e);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getRNPModule = () => {
|
|
24
|
+
if (isExpoGo() || !hasNativeModule("RNPermissions")) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
return require("react-native-permissions");
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
11
34
|
|
|
12
35
|
export type AppPermissionType =
|
|
13
36
|
| "camera"
|
|
@@ -27,6 +50,12 @@ class PermissionService {
|
|
|
27
50
|
* Helper to determine the correct native Permission object based on platform
|
|
28
51
|
*/
|
|
29
52
|
private getNativePermission(type: AppPermissionType): Permission | null {
|
|
53
|
+
const rnp = getRNPModule();
|
|
54
|
+
if (!rnp?.PERMISSIONS) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const { PERMISSIONS } = rnp;
|
|
58
|
+
|
|
30
59
|
if (Platform.OS === "ios") {
|
|
31
60
|
switch (type) {
|
|
32
61
|
case "camera":
|
|
@@ -34,10 +63,7 @@ class PermissionService {
|
|
|
34
63
|
case "audio":
|
|
35
64
|
return PERMISSIONS.IOS.MICROPHONE;
|
|
36
65
|
case "image":
|
|
37
|
-
return PERMISSIONS.IOS.PHOTO_LIBRARY;
|
|
38
66
|
case "file":
|
|
39
|
-
// iOS doesn't have a broad "file system" permission for the app sandbox
|
|
40
|
-
// Usually handled via DocumentPicker or PhotoLibrary depending on use case.
|
|
41
67
|
return PERMISSIONS.IOS.PHOTO_LIBRARY;
|
|
42
68
|
case "location":
|
|
43
69
|
return PERMISSIONS.IOS.LOCATION_WHEN_IN_USE;
|
|
@@ -50,11 +76,7 @@ class PermissionService {
|
|
|
50
76
|
case "nearbydevice":
|
|
51
77
|
return PERMISSIONS.IOS.BLUETOOTH;
|
|
52
78
|
case "notification":
|
|
53
|
-
// handled uniquely by react-native-permissions using requestNotifications
|
|
54
|
-
return null;
|
|
55
79
|
case "phone":
|
|
56
|
-
// iOS doesn't require explicit permission to use Linking.openURL('tel:...')
|
|
57
|
-
return null;
|
|
58
80
|
default:
|
|
59
81
|
return null;
|
|
60
82
|
}
|
|
@@ -77,13 +99,10 @@ class PermissionService {
|
|
|
77
99
|
case "calendar":
|
|
78
100
|
return PERMISSIONS.ANDROID.READ_CALENDAR;
|
|
79
101
|
case "nearbydevice":
|
|
80
|
-
// Required for Android 12+
|
|
81
102
|
return PERMISSIONS.ANDROID.BLUETOOTH_CONNECT;
|
|
82
103
|
case "phone":
|
|
83
104
|
return PERMISSIONS.ANDROID.CALL_PHONE;
|
|
84
105
|
case "notification":
|
|
85
|
-
// handled uniquely by checkNotifications/requestNotifications
|
|
86
|
-
return null;
|
|
87
106
|
default:
|
|
88
107
|
return null;
|
|
89
108
|
}
|
|
@@ -91,24 +110,101 @@ class PermissionService {
|
|
|
91
110
|
return null;
|
|
92
111
|
}
|
|
93
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Fallback for checking permissions in Expo Go using Expo APIs
|
|
115
|
+
*/
|
|
116
|
+
private async checkExpoPermission(type: AppPermissionType): Promise<PermissionStatus> {
|
|
117
|
+
try {
|
|
118
|
+
if (type === "notification") {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
120
|
+
const Notifications = require("expo-notifications");
|
|
121
|
+
const { status } = await Notifications.getPermissionsAsync();
|
|
122
|
+
return status === "granted" ? RESULTS.GRANTED : RESULTS.DENIED;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (type === "location" || type === "locationAlways") {
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
127
|
+
const Location = require("expo-location");
|
|
128
|
+
const { status } = await Location.getForegroundPermissionsAsync();
|
|
129
|
+
return status === "granted" ? RESULTS.GRANTED : RESULTS.DENIED;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (type === "camera" || type === "image") {
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
134
|
+
const ImagePicker = require("expo-image-picker");
|
|
135
|
+
const { status } =
|
|
136
|
+
type === "camera"
|
|
137
|
+
? await ImagePicker.getCameraPermissionsAsync()
|
|
138
|
+
: await ImagePicker.getMediaLibraryPermissionsAsync();
|
|
139
|
+
return status === "granted" ? RESULTS.GRANTED : RESULTS.DENIED;
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
return RESULTS.GRANTED;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return RESULTS.GRANTED;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fallback for requesting permissions in Expo Go using Expo APIs
|
|
150
|
+
*/
|
|
151
|
+
private async requestExpoPermission(type: AppPermissionType): Promise<PermissionStatus> {
|
|
152
|
+
try {
|
|
153
|
+
if (type === "notification") {
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
155
|
+
const Notifications = require("expo-notifications");
|
|
156
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
157
|
+
return status === "granted" ? RESULTS.GRANTED : RESULTS.DENIED;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (type === "location" || type === "locationAlways") {
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
162
|
+
const Location = require("expo-location");
|
|
163
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
164
|
+
return status === "granted" ? RESULTS.GRANTED : RESULTS.DENIED;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (type === "camera" || type === "image") {
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
169
|
+
const ImagePicker = require("expo-image-picker");
|
|
170
|
+
const { status } =
|
|
171
|
+
type === "camera"
|
|
172
|
+
? await ImagePicker.requestCameraPermissionsAsync()
|
|
173
|
+
: await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
174
|
+
return status === "granted" ? RESULTS.GRANTED : RESULTS.DENIED;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
return RESULTS.GRANTED;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return RESULTS.GRANTED;
|
|
181
|
+
}
|
|
182
|
+
|
|
94
183
|
/**
|
|
95
184
|
* Check the current status of a permission without asking the user
|
|
96
185
|
*/
|
|
97
186
|
async checkPermission(type: AppPermissionType): Promise<PermissionStatus> {
|
|
187
|
+
const rnp = getRNPModule();
|
|
188
|
+
if (isExpoGo() || !rnp) {
|
|
189
|
+
return this.checkExpoPermission(type);
|
|
190
|
+
}
|
|
191
|
+
|
|
98
192
|
if (type === "notification") {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
193
|
+
try {
|
|
194
|
+
const { status } = await rnp.checkNotifications();
|
|
195
|
+
return status as PermissionStatus;
|
|
196
|
+
} catch {
|
|
197
|
+
return this.checkExpoPermission(type);
|
|
198
|
+
}
|
|
103
199
|
}
|
|
104
|
-
|
|
200
|
+
|
|
105
201
|
const nativePerm = this.getNativePermission(type);
|
|
106
202
|
if (!nativePerm) {
|
|
107
|
-
// If the permission doesn't exist on this platform (e.g. Phone on iOS), we consider it granted
|
|
108
203
|
return RESULTS.GRANTED;
|
|
109
204
|
}
|
|
205
|
+
|
|
110
206
|
try {
|
|
111
|
-
return await check(nativePerm);
|
|
207
|
+
return (await rnp.check(nativePerm as any)) as PermissionStatus;
|
|
112
208
|
} catch (error) {
|
|
113
209
|
console.error(`Error checking permission ${type}:`, error);
|
|
114
210
|
return RESULTS.UNAVAILABLE;
|
|
@@ -119,19 +215,27 @@ class PermissionService {
|
|
|
119
215
|
* Request a permission from the user
|
|
120
216
|
*/
|
|
121
217
|
async requestPermission(type: AppPermissionType): Promise<PermissionStatus> {
|
|
218
|
+
const rnp = getRNPModule();
|
|
219
|
+
if (isExpoGo() || !rnp) {
|
|
220
|
+
return this.requestExpoPermission(type);
|
|
221
|
+
}
|
|
222
|
+
|
|
122
223
|
if (type === "notification") {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
224
|
+
try {
|
|
225
|
+
const { status } = await rnp.requestNotifications(["alert", "sound", "badge"]);
|
|
226
|
+
return status as PermissionStatus;
|
|
227
|
+
} catch {
|
|
228
|
+
return this.requestExpoPermission(type);
|
|
229
|
+
}
|
|
127
230
|
}
|
|
128
231
|
|
|
129
232
|
const nativePerm = this.getNativePermission(type);
|
|
130
233
|
if (!nativePerm) {
|
|
131
234
|
return RESULTS.GRANTED;
|
|
132
235
|
}
|
|
236
|
+
|
|
133
237
|
try {
|
|
134
|
-
return await request(nativePerm);
|
|
238
|
+
return (await rnp.request(nativePerm as any)) as PermissionStatus;
|
|
135
239
|
} catch (error) {
|
|
136
240
|
console.error(`Error requesting permission ${type}:`, error);
|
|
137
241
|
return RESULTS.UNAVAILABLE;
|
|
@@ -152,7 +256,14 @@ class PermissionService {
|
|
|
152
256
|
}
|
|
153
257
|
|
|
154
258
|
/**
|
|
155
|
-
*
|
|
259
|
+
* Opens the OS Settings screen for the app
|
|
260
|
+
*/
|
|
261
|
+
async openSettings(): Promise<void> {
|
|
262
|
+
return openSettings();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Helper to show an alert and open the OS Settings screen for the app if permission is blocked
|
|
156
267
|
*/
|
|
157
268
|
async openSettingsForApp(
|
|
158
269
|
title = "Permission Required",
|
|
@@ -160,52 +271,65 @@ class PermissionService {
|
|
|
160
271
|
) {
|
|
161
272
|
Alert.alert(title, message, [
|
|
162
273
|
{ text: "Cancel", style: "cancel" },
|
|
163
|
-
{
|
|
274
|
+
{
|
|
275
|
+
text: "Open Settings",
|
|
276
|
+
onPress: () => {
|
|
277
|
+
openSettings();
|
|
278
|
+
},
|
|
279
|
+
},
|
|
164
280
|
]);
|
|
165
281
|
}
|
|
282
|
+
|
|
166
283
|
/**
|
|
167
284
|
* Utility to register and fetch the Expo Push Token for this device.
|
|
168
285
|
* This uses the unified PermissionService to request the notification permission.
|
|
169
286
|
* NOTE: This will only return a token on physical devices.
|
|
170
287
|
*/
|
|
171
288
|
async registerForPushNotificationsAsync(): Promise<string | null> {
|
|
289
|
+
// In Expo Go on Android, FCM push notifications are not supported
|
|
290
|
+
if (isExpoGo() && Platform.OS === "android") {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
172
294
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
173
295
|
const Device = require("expo-device");
|
|
174
296
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
175
297
|
const Notifications = require("expo-notifications");
|
|
176
298
|
|
|
177
299
|
if (!Device.isDevice) {
|
|
178
|
-
console.log("Must use physical device for Push Notifications");
|
|
179
300
|
return null;
|
|
180
301
|
}
|
|
181
302
|
|
|
182
303
|
const hasPermission = await this.requirePermission("notification");
|
|
183
304
|
if (!hasPermission) {
|
|
184
|
-
console.log("User denied push notification permissions");
|
|
185
305
|
return null;
|
|
186
306
|
}
|
|
187
307
|
|
|
188
308
|
// Set channel for Android
|
|
189
309
|
if (Platform.OS === "android") {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
310
|
+
try {
|
|
311
|
+
await Notifications.setNotificationChannelAsync("default", {
|
|
312
|
+
name: "default",
|
|
313
|
+
importance: Notifications.AndroidImportance.MAX,
|
|
314
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
315
|
+
lightColor: "#FF231F7C",
|
|
316
|
+
});
|
|
317
|
+
} catch {
|
|
318
|
+
// Channel setup may fail in Expo Go
|
|
319
|
+
}
|
|
196
320
|
}
|
|
197
321
|
|
|
198
322
|
try {
|
|
199
323
|
const projectId =
|
|
200
324
|
require("../../app.json")?.expo?.extra?.eas?.projectId;
|
|
201
|
-
|
|
325
|
+
|
|
202
326
|
const tokenData = await Notifications.getExpoPushTokenAsync({
|
|
203
327
|
projectId,
|
|
204
328
|
});
|
|
205
329
|
|
|
206
330
|
return tokenData.data;
|
|
207
331
|
} catch (e) {
|
|
208
|
-
console.
|
|
332
|
+
console.warn("Failed to get push token", e);
|
|
209
333
|
return null;
|
|
210
334
|
}
|
|
211
335
|
}
|