@imksh/create 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/commands/create.js +2 -0
  4. package/src/index.js +1 -1
  5. package/templates/layers/next/javascript/package.json +8 -2
  6. package/templates/layers/next/typescript/.eslintrc.json +12 -0
  7. package/templates/layers/next/typescript/package.json +14 -3
  8. package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -1
  9. package/templates/layers/next/typescript/src/components/common/LottieIcon.tsx +2 -2
  10. package/templates/layers/next/typescript/src/components/common/Table.tsx +4 -4
  11. package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +2 -1
  12. package/templates/layers/next/typescript/src/components/ui/Header.tsx +2 -2
  13. package/templates/layers/next/typescript/src/hooks/usePwaInstall.ts +10 -4
  14. package/templates/layers/next/typescript/src/services/authService.ts +1 -1
  15. package/templates/layers/next/typescript/src/store/useAuthStore.ts +24 -9
  16. package/templates/layers/node/javascript/.env.example +3 -1
  17. package/templates/layers/node/javascript/package.json +12 -3
  18. package/templates/layers/node/javascript/src/app.js +2 -1
  19. package/templates/layers/node/javascript/src/controllers/auth.controller.js +106 -168
  20. package/templates/layers/node/javascript/src/controllers/push.controller.js +51 -58
  21. package/templates/layers/node/javascript/src/controllers/upload.controller.js +12 -19
  22. package/templates/layers/node/javascript/src/middleware/auth.middleware.js +5 -11
  23. package/templates/layers/node/javascript/src/middleware/errorHandler.js +6 -4
  24. package/templates/layers/node/javascript/src/routes/auth.route.js +2 -0
  25. package/templates/layers/node/javascript/src/services/auth.service.js +159 -164
  26. package/templates/layers/node/javascript/src/utils/ApiError.js +20 -0
  27. package/templates/layers/node/javascript/src/utils/SuccessResponse.js +15 -0
  28. package/templates/layers/node/javascript/src/utils/apiResponse.js +4 -0
  29. package/templates/layers/node/javascript/src/utils/asyncHandler.js +5 -0
  30. package/templates/layers/node/typescript/.env.example +3 -1
  31. package/templates/layers/node/typescript/.eslintrc.cjs +28 -0
  32. package/templates/layers/node/typescript/package.json +16 -2
  33. package/templates/layers/node/typescript/src/app.ts +3 -2
  34. package/templates/layers/node/typescript/src/config/db.ts +3 -2
  35. package/templates/layers/node/typescript/src/config/socket.ts +5 -4
  36. package/templates/layers/node/typescript/src/controllers/auth.controller.ts +104 -132
  37. package/templates/layers/node/typescript/src/controllers/push.controller.ts +52 -59
  38. package/templates/layers/node/typescript/src/controllers/upload.controller.ts +12 -19
  39. package/templates/layers/node/typescript/src/middleware/auth.middleware.ts +13 -7
  40. package/templates/layers/node/typescript/src/middleware/errorHandler.ts +18 -5
  41. package/templates/layers/node/typescript/src/middleware/rateLimiter.ts +2 -2
  42. package/templates/layers/node/typescript/src/routes/auth.route.ts +2 -0
  43. package/templates/layers/node/typescript/src/services/auth.service.ts +86 -43
  44. package/templates/layers/node/typescript/src/types/express.d.ts +5 -3
  45. package/templates/layers/node/typescript/src/utils/ApiError.ts +25 -0
  46. package/templates/layers/node/typescript/src/utils/SuccessResponse.ts +17 -0
  47. package/templates/layers/node/typescript/src/utils/apiResponse.ts +4 -0
  48. package/templates/layers/node/typescript/src/utils/asyncHandler.ts +13 -0
  49. package/templates/layers/node/typescript/src/utils/cloudinaryUpload.ts +18 -7
  50. package/templates/layers/node/typescript/src/utils/expoNotification.ts +1 -1
  51. package/templates/layers/node/typescript/src/utils/genOtpToken.ts +7 -1
  52. package/templates/layers/node/typescript/src/utils/jwt.ts +8 -2
  53. package/templates/layers/node/typescript/src/utils/logger.ts +2 -1
  54. package/templates/layers/node/typescript/src/utils/pushHelper.ts +13 -9
  55. package/templates/layers/node-prisma/javascript/.env.example +3 -1
  56. package/templates/layers/node-prisma/javascript/package.json +11 -2
  57. package/templates/layers/node-prisma/javascript/src/app.js +2 -1
  58. package/templates/layers/node-prisma/javascript/src/controllers/auth.controller.js +90 -135
  59. package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +64 -76
  60. package/templates/layers/node-prisma/javascript/src/controllers/upload.controller.js +12 -19
  61. package/templates/layers/node-prisma/javascript/src/middleware/auth.middleware.js +5 -4
  62. package/templates/layers/node-prisma/javascript/src/middleware/errorHandler.js +6 -4
  63. package/templates/layers/node-prisma/javascript/src/routes/auth.route.js +2 -0
  64. package/templates/layers/node-prisma/javascript/src/services/auth.service.js +145 -122
  65. package/templates/layers/node-prisma/javascript/src/utils/ApiError.js +20 -0
  66. package/templates/layers/node-prisma/javascript/src/utils/SuccessResponse.js +15 -0
  67. package/templates/layers/node-prisma/javascript/src/utils/apiResponse.js +4 -0
  68. package/templates/layers/node-prisma/javascript/src/utils/asyncHandler.js +5 -0
  69. package/templates/layers/node-prisma/typescript/.env.example +3 -1
  70. package/templates/layers/node-prisma/typescript/.eslintrc.cjs +28 -0
  71. package/templates/layers/node-prisma/typescript/package.json +19 -5
  72. package/templates/layers/node-prisma/typescript/src/app.ts +3 -2
  73. package/templates/layers/node-prisma/typescript/src/config/prisma.ts +3 -2
  74. package/templates/layers/node-prisma/typescript/src/config/socket.ts +5 -4
  75. package/templates/layers/node-prisma/typescript/src/controllers/auth.controller.ts +89 -105
  76. package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -83
  77. package/templates/layers/node-prisma/typescript/src/controllers/upload.controller.ts +12 -19
  78. package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +9 -13
  79. package/templates/layers/node-prisma/typescript/src/middleware/errorHandler.ts +18 -5
  80. package/templates/layers/node-prisma/typescript/src/middleware/rateLimiter.ts +2 -2
  81. package/templates/layers/node-prisma/typescript/src/routes/auth.route.ts +2 -0
  82. package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +83 -31
  83. package/templates/layers/node-prisma/typescript/src/types/express.d.ts +5 -3
  84. package/templates/layers/node-prisma/typescript/src/utils/ApiError.ts +25 -0
  85. package/templates/layers/node-prisma/typescript/src/utils/SuccessResponse.ts +17 -0
  86. package/templates/layers/node-prisma/typescript/src/utils/apiResponse.ts +4 -0
  87. package/templates/layers/node-prisma/typescript/src/utils/asyncHandler.ts +13 -0
  88. package/templates/layers/node-prisma/typescript/src/utils/cloudinaryUpload.ts +18 -7
  89. package/templates/layers/node-prisma/typescript/src/utils/genOtpToken.ts +7 -1
  90. package/templates/layers/node-prisma/typescript/src/utils/jwt.ts +8 -2
  91. package/templates/layers/node-prisma/typescript/src/utils/logger.ts +2 -1
  92. package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +10 -6
  93. package/templates/layers/prisma/javascript/package.json +8 -0
  94. package/templates/layers/prisma/typescript/package.json +14 -1
  95. package/templates/layers/prisma/typescript/src/config/db.ts +3 -2
  96. package/templates/layers/react/javascript/package.json +6 -0
  97. package/templates/layers/react/typescript/.eslintrc.cjs +20 -0
  98. package/templates/layers/react/typescript/package.json +11 -2
  99. package/templates/layers/react/typescript/src/components/common/Table.tsx +4 -4
  100. package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +1 -1
  101. package/templates/layers/react/typescript/src/main.tsx +11 -11
  102. package/templates/layers/react/typescript/src/services/authService.ts +1 -1
  103. package/templates/layers/react/typescript/src/store/useAuthStore.ts +24 -9
  104. package/templates/layers/react-native/base/app.json +9 -22
  105. package/templates/layers/react-native/base/eslint.config.js +5 -1
  106. package/templates/layers/react-native/base/package.json +40 -33
  107. package/templates/layers/react-native/base/src/app/(auth)/edit-profile.tsx +7 -7
  108. package/templates/layers/react-native/base/src/app/(auth)/login.tsx +31 -7
  109. package/templates/layers/react-native/base/src/app/(tabs)/_layout.tsx +3 -3
  110. package/templates/layers/react-native/base/src/app/(tabs)/index.tsx +6 -5
  111. package/templates/layers/react-native/base/src/app/_layout.tsx +4 -8
  112. package/templates/layers/react-native/base/src/app/screens/doc-viewer.tsx +378 -0
  113. package/templates/layers/react-native/base/src/components/common/ConfirmationToast.tsx +58 -40
  114. package/templates/layers/react-native/base/src/components/common/GoogleAuthButton.tsx +123 -0
  115. package/templates/layers/react-native/base/src/components/common/LocationLoader.tsx +64 -9
  116. package/templates/layers/react-native/base/src/components/common/Typography.tsx +54 -19
  117. package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +3 -3
  118. package/templates/layers/react-native/base/src/components/ui/ClassicTabBar.tsx +15 -12
  119. package/templates/layers/react-native/base/src/components/ui/CustomTabBar.tsx +90 -91
  120. package/templates/layers/react-native/base/src/components/ui/Drawer.tsx +344 -0
  121. package/templates/layers/react-native/base/src/components/ui/Sidebar.tsx +4 -4
  122. package/templates/layers/react-native/base/src/components/ui/buttons/Button.tsx +29 -6
  123. package/templates/layers/react-native/base/src/components/ui/data-display/Accordion.tsx +2 -2
  124. package/templates/layers/react-native/base/src/components/ui/data-display/Badge.tsx +1 -2
  125. package/templates/layers/react-native/base/src/components/ui/data-display/Image.tsx +3 -3
  126. package/templates/layers/react-native/base/src/components/ui/data-display/SwipeableRow.tsx +28 -7
  127. package/templates/layers/react-native/base/src/components/ui/feedback/GestureWrapper.tsx +11 -4
  128. package/templates/layers/react-native/base/src/components/ui/inputs/DatePicker.tsx +3 -3
  129. package/templates/layers/react-native/base/src/components/ui/inputs/ImagePickerModal.tsx +45 -19
  130. package/templates/layers/react-native/base/src/components/ui/inputs/Input.tsx +28 -9
  131. package/templates/layers/react-native/base/src/components/ui/inputs/SearchablePicker.tsx +199 -0
  132. package/templates/layers/react-native/base/src/components/ui/inputs/Switch.tsx +7 -7
  133. package/templates/layers/react-native/base/src/components/ui/inputs/WebLocationPicker.tsx +146 -72
  134. package/templates/layers/react-native/base/src/config/api.ts +5 -5
  135. package/templates/layers/react-native/base/src/services/auth.service.ts +3 -1
  136. package/templates/layers/react-native/base/src/services/permission.service.ts +168 -44
  137. package/templates/layers/react-native/base/src/services/socket.service.ts +0 -7
  138. package/templates/layers/react-native/base/src/store/useAuthStore.ts +61 -16
  139. package/templates/layers/react-native/base/src/store/useNotificationStore.ts +36 -42
  140. package/templates/layers/react-native/base/src/types/images.d.ts +7 -5
  141. package/templates/layers/react-native/base/src/utils/env.ts +47 -0
  142. package/templates/layers/react-native/base/src/utils/fileDownload.ts +206 -0
  143. package/templates/layers/react-native/base/src/utils/storage.ts +1 -1
  144. package/templates/presets/production-fullstack/overrides/client/package.json +9 -0
  145. package/templates/presets/production-fullstack/overrides/mobile/app.json +2 -15
  146. package/templates/presets/production-fullstack/overrides/mobile/package.json +39 -29
  147. package/templates/presets/production-fullstack/overrides/mobile/src/app/_layout.tsx +3 -3
  148. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/Drawer.tsx +344 -0
  149. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/DatePicker.tsx +3 -3
  150. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/inputs/ImagePickerModal.tsx +3 -3
  151. package/templates/presets/production-fullstack/overrides/mobile/src/services/permission.service.ts +168 -44
  152. package/templates/presets/production-fullstack/overrides/mobile/src/store/useNotificationStore.ts +16 -0
  153. package/templates/presets/production-fullstack/overrides/mobile/src/utils/env.ts +47 -0
  154. package/templates/presets/production-fullstack/overrides/server/package.json +11 -2
  155. package/templates/layers/react-native/base/src/app/doc-viewer.tsx +0 -193
  156. package/templates/layers/react-native/base/src/components/ui/BottomModal.tsx +0 -176
  157. package/templates/presets/production-fullstack/overrides/mobile/src/components/ui/BottomModal.tsx +0 -159
  158. /package/templates/layers/react-native/base/assets/images/logo/{logoWithBg.png → icon.png} +0 -0
@@ -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({ accuracy: Location.Accuracy.Balanced });
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
- onLocationSelected(details);
124
- setModalVisible(false);
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.success("Location not found");
187
+ toast.error("Location not found");
146
188
  }
147
- } catch (error) {
148
- console.warn("Search failed", error);
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}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
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
- `, [mapCenter.latitude, mapCenter.longitude]);
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 on Map"
227
- : "Pick from Map"}
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 name="chevron-forward" size={20} color={theme.secondary} />
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
- <View
255
- className="h-16 flex-row items-center justify-between px-4 border-b border-base-300 pt-4 bg-base-100"
256
- style={{ zIndex: 10 }}
257
- >
258
- <TouchableOpacity onPress={() => setModalVisible(false)}>
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
- <View className="px-4 py-3 bg-base-100 flex-row items-center border-b border-base-200 gap-4">
268
- <View className="flex-1" style={{ marginBottom: -16 }}>
269
- <Input
270
- placeholder="Search city, area, etc."
271
- value={searchQuery}
272
- onChangeText={setSearchQuery}
273
- onSubmitEditing={handleSearch}
274
- returnKeyType="search"
275
- containerStyle={{ marginBottom: 0 }}
276
- leftIcon="search"
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
- <View className="flex-1 relative">
291
- <WebView
292
- ref={webViewRef}
293
- source={{ html: htmlContent }}
294
- onMessage={handleMessage}
295
- style={{ flex: 1 }}
296
- javaScriptEnabled={true}
297
- scrollEnabled={false}
298
- bounces={false}
299
- startInLoadingState={true}
300
- renderLoading={() => (
301
- <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f0f0f0' }}>
302
- <ActivityIndicator size="large" color={theme.primary} />
303
- </View>
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
- <View className="p-4 bg-base-100 border-t border-base-300 pb-8">
309
- <Button
310
- label="Confirm Location"
311
- onPress={handleConfirmLocation}
312
- isLoading={loading}
313
- variant="primary"
314
- />
315
- </View>
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 = any, R = AxiosResponse<ApiResponse<T>>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
8
- delete<T = any, R = AxiosResponse<ApiResponse<T>>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
9
- post<T = any, R = AxiosResponse<ApiResponse<T>>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
10
- put<T = any, R = AxiosResponse<ApiResponse<T>>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
11
- patch<T = any, R = AxiosResponse<ApiResponse<T>>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
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: any) => api.put("/auth/me", 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
- check,
4
- request,
5
- PERMISSIONS,
6
- RESULTS,
7
- Permission,
8
- PermissionStatus,
9
- openSettings,
10
- } from "react-native-permissions";
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
- // eslint-disable-next-line @typescript-eslint/no-require-imports
100
- const { checkNotifications } = require("react-native-permissions");
101
- const { status } = await checkNotifications();
102
- return status;
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
- // eslint-disable-next-line @typescript-eslint/no-require-imports
124
- const { requestNotifications } = require("react-native-permissions");
125
- const { status } = await requestNotifications(["alert", "sound", "badge"]);
126
- return status;
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
- * Helper to open the OS Settings screen for the app if permission is blocked
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
- { text: "Open Settings", onPress: () => openSettings() },
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
- Notifications.setNotificationChannelAsync("default", {
191
- name: "default",
192
- importance: Notifications.AndroidImportance.MAX,
193
- vibrationPattern: [0, 250, 250, 250],
194
- lightColor: "#FF231F7C",
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.error("Failed to get push token", e);
332
+ console.warn("Failed to get push token", e);
209
333
  return null;
210
334
  }
211
335
  }