@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
@@ -17,13 +17,6 @@ export const initializeSocket = async () => {
17
17
  transports: ['websocket'],
18
18
  });
19
19
 
20
- socket.on('connect', () => {
21
- console.log('Connected to socket server');
22
- });
23
-
24
- socket.on('disconnect', () => {
25
- console.log('Disconnected from socket server');
26
- });
27
20
  };
28
21
 
29
22
  export const getSocket = () => socket;
@@ -11,6 +11,20 @@ import { useNotificationStore } from "./useNotificationStore";
11
11
  // called from a useEffect, the promise is already resolved or very close.
12
12
  const _prefetchedUser: Promise<User | null> = get("user");
13
13
 
14
+ interface ApiErrorResponse {
15
+ response?: {
16
+ data?: {
17
+ message?: string;
18
+ };
19
+ };
20
+ message?: string;
21
+ }
22
+
23
+ const getErrorMessage = (err: unknown): string => {
24
+ const apiErr = err as ApiErrorResponse;
25
+ return apiErr.response?.data?.message || apiErr.message || "An unexpected error occurred";
26
+ };
27
+
14
28
  export interface AuthState {
15
29
  user: User | null;
16
30
  loading: boolean;
@@ -31,6 +45,7 @@ export interface AuthState {
31
45
  role?: string,
32
46
  ) => Promise<boolean>;
33
47
  login: (email: string, password: string) => Promise<boolean>;
48
+ googleLogin: (idToken: string) => Promise<boolean>;
34
49
  demoLogin: () => Promise<void>;
35
50
  getMe: () => Promise<void>;
36
51
  updateProfile: (updates: Partial<User> | FormData) => Promise<boolean>;
@@ -111,9 +126,9 @@ export const useAuthStore = create<AuthState>((set) => ({
111
126
  try {
112
127
  await authService.sendOtp(email);
113
128
  return true;
114
- } catch (err: any) {
129
+ } catch (err: unknown) {
115
130
  set({
116
- error: err.response?.data?.message || err.message,
131
+ error: getErrorMessage(err),
117
132
  loading: false,
118
133
  });
119
134
  return false;
@@ -154,9 +169,9 @@ export const useAuthStore = create<AuthState>((set) => ({
154
169
  notifStore.registerForPushNotificationsAsync();
155
170
  notifStore.fetchNotifications();
156
171
  return true;
157
- } catch (err: any) {
172
+ } catch (err: unknown) {
158
173
  set({
159
- error: err.response?.data?.message || err.message,
174
+ error: getErrorMessage(err),
160
175
  loading: false,
161
176
  });
162
177
  return false;
@@ -185,9 +200,39 @@ export const useAuthStore = create<AuthState>((set) => ({
185
200
  notifStore.registerForPushNotificationsAsync();
186
201
  notifStore.fetchNotifications();
187
202
  return true;
188
- } catch (err: any) {
203
+ } catch (err: unknown) {
204
+ set({
205
+ error: getErrorMessage(err),
206
+ loading: false,
207
+ });
208
+ return false;
209
+ } finally {
210
+ set({ loading: false });
211
+ }
212
+ },
213
+
214
+ googleLogin: async (idToken: string) => {
215
+ set({ loading: true, error: null });
216
+ try {
217
+ const { data } = await authService.googleLogin(idToken);
218
+ if (data.data?.token) {
219
+ save("token", data.data.token);
220
+ }
221
+ const user = data.data?.user || data.data;
222
+ if (user) save("user", user);
223
+ set({
224
+ user,
225
+ loading: false,
226
+ });
227
+ initializeSocket();
228
+ const notifStore = useNotificationStore.getState();
229
+ notifStore.listenForSocketEvents();
230
+ notifStore.registerForPushNotificationsAsync();
231
+ notifStore.fetchNotifications();
232
+ return true;
233
+ } catch (err: unknown) {
189
234
  set({
190
- error: err.response?.data?.message || err.message,
235
+ error: getErrorMessage(err),
191
236
  loading: false,
192
237
  });
193
238
  return false;
@@ -231,9 +276,9 @@ export const useAuthStore = create<AuthState>((set) => ({
231
276
  const { data } = await authService.updateMe(updates);
232
277
  set({ user: data.data, loading: false });
233
278
  return true;
234
- } catch (err: any) {
279
+ } catch (err: unknown) {
235
280
  set({
236
- error: err.response?.data?.message || err.message,
281
+ error: getErrorMessage(err),
237
282
  loading: false,
238
283
  });
239
284
  return false;
@@ -264,8 +309,8 @@ export const useAuthStore = create<AuthState>((set) => ({
264
309
  try {
265
310
  await authService.genOtp(email);
266
311
  return true;
267
- } catch (err: any) {
268
- set({ error: err.response?.data?.message || err.message, loading: false });
312
+ } catch (err: unknown) {
313
+ set({ error: getErrorMessage(err), loading: false });
269
314
  return false;
270
315
  } finally {
271
316
  set({ loading: false });
@@ -278,8 +323,8 @@ export const useAuthStore = create<AuthState>((set) => ({
278
323
  const { data } = await authService.verifyOtp(email, otp);
279
324
  if (data.token) save("token", data.token);
280
325
  return true;
281
- } catch (err: any) {
282
- set({ error: err.response?.data?.message || err.message, loading: false });
326
+ } catch (err: unknown) {
327
+ set({ error: getErrorMessage(err), loading: false });
283
328
  return false;
284
329
  } finally {
285
330
  set({ loading: false });
@@ -291,8 +336,8 @@ export const useAuthStore = create<AuthState>((set) => ({
291
336
  try {
292
337
  await authService.resetPassword(newPassword);
293
338
  return true;
294
- } catch (err: any) {
295
- set({ error: err.response?.data?.message || err.message, loading: false });
339
+ } catch (err: unknown) {
340
+ set({ error: getErrorMessage(err), loading: false });
296
341
  return false;
297
342
  } finally {
298
343
  set({ loading: false });
@@ -304,8 +349,8 @@ export const useAuthStore = create<AuthState>((set) => ({
304
349
  try {
305
350
  await authService.changePassword(oldPassword, newPassword);
306
351
  return true;
307
- } catch (err: any) {
308
- set({ error: err.response?.data?.message || err.message, loading: false });
352
+ } catch (err: unknown) {
353
+ set({ error: getErrorMessage(err), loading: false });
309
354
  return false;
310
355
  } finally {
311
356
  set({ loading: false });
@@ -4,6 +4,7 @@ import Constants from "expo-constants";
4
4
  import { Platform } from "react-native";
5
5
  import { getSocket } from "../services/socket.service";
6
6
  import { toast } from "../utils/toast";
7
+ import { isExpoGo } from "@/utils/env";
7
8
 
8
9
  export interface AppNotification {
9
10
  id: string;
@@ -12,7 +13,7 @@ export interface AppNotification {
12
13
  type: string;
13
14
  url: string | null;
14
15
  isRead: boolean;
15
- metadata: any | null;
16
+ metadata: Record<string, unknown> | null;
16
17
  createdAt: string;
17
18
  }
18
19
 
@@ -47,9 +48,12 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
47
48
  unreadCount,
48
49
  loading: false,
49
50
  });
50
- } catch (error: any) {
51
+ } catch (error: unknown) {
52
+ const message =
53
+ (error as { response?: { data?: { message?: string } } })?.response
54
+ ?.data?.message || "Failed to fetch notifications";
51
55
  set({
52
- error: error.response?.data?.message || "Failed to fetch notifications",
56
+ error: message,
53
57
  loading: false,
54
58
  });
55
59
  }
@@ -58,45 +62,37 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
58
62
  markAsRead: async (id: string) => {
59
63
  try {
60
64
  // Optimistic update
61
- const { notifications, unreadCount } = get();
62
- const updated = notifications.map(n =>
63
- n.id === id ? { ...n, isRead: true } : n
64
- );
65
-
66
- set({
67
- notifications: updated,
68
- unreadCount: Math.max(0, unreadCount - 1)
69
- });
65
+ set((state) => ({
66
+ notifications: state.notifications.map((n) =>
67
+ n.id === id ? { ...n, isRead: true } : n
68
+ ),
69
+ unreadCount: Math.max(0, state.unreadCount - 1),
70
+ }));
70
71
 
71
72
  await notificationService.markAsRead(id);
72
- } catch{
73
- // Revert if failed
73
+ } catch {
74
+ // Revert if API fails
74
75
  get().fetchNotifications();
75
76
  }
76
77
  },
77
78
 
78
79
  markAllAsRead: async () => {
79
80
  try {
80
- const { notifications } = get();
81
- const updated = notifications.map(n => ({ ...n, isRead: true }));
82
-
83
- set({
84
- notifications: updated,
85
- unreadCount: 0
86
- });
81
+ set((state) => ({
82
+ notifications: state.notifications.map((n) => ({ ...n, isRead: true })),
83
+ unreadCount: 0,
84
+ }));
87
85
 
88
86
  await notificationService.markAllAsRead();
89
- } catch{
87
+ } catch {
90
88
  get().fetchNotifications();
91
89
  }
92
90
  },
93
91
 
94
92
  registerForPushNotificationsAsync: async () => {
95
- let token;
93
+ let token: string | undefined;
96
94
 
97
- const isExpoGo = Constants.appOwnership === 'expo';
98
- if (isExpoGo && Platform.OS === 'android') {
99
- console.log('Skipping push notification registration: not supported in Expo Go on Android (SDK 53+).');
95
+ if (isExpoGo() && Platform.OS === "android") {
100
96
  return;
101
97
  }
102
98
 
@@ -106,12 +102,12 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
106
102
  // eslint-disable-next-line @typescript-eslint/no-require-imports
107
103
  const Device = require("expo-device");
108
104
 
109
- if (Platform.OS === 'android') {
110
- await Notifications.setNotificationChannelAsync('default', {
111
- name: 'default',
105
+ if (Platform.OS === "android") {
106
+ await Notifications.setNotificationChannelAsync("default", {
107
+ name: "default",
112
108
  importance: Notifications.AndroidImportance.MAX,
113
109
  vibrationPattern: [0, 250, 250, 250],
114
- lightColor: '#FF231F7C',
110
+ lightColor: "#FF231F7C",
115
111
  });
116
112
  }
117
113
 
@@ -119,13 +115,12 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
119
115
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
120
116
  let finalStatus = existingStatus;
121
117
 
122
- if (existingStatus !== 'granted') {
118
+ if (existingStatus !== "granted") {
123
119
  const { status } = await Notifications.requestPermissionsAsync();
124
120
  finalStatus = status;
125
121
  }
126
122
 
127
- if (finalStatus !== 'granted') {
128
- console.log('Failed to get push token for push notification!');
123
+ if (finalStatus !== "granted") {
129
124
  return;
130
125
  }
131
126
 
@@ -136,17 +131,16 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
136
131
 
137
132
  token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
138
133
 
139
- // Send token to backend
140
- try {
141
- await notificationService.registerPushToken(token);
142
- } catch (e) {
143
- console.error("Failed to save push token to server", e);
134
+ if (token) {
135
+ try {
136
+ await notificationService.registerPushToken(token);
137
+ } catch {
138
+ // Token registration failed silently
139
+ }
144
140
  }
145
- } else {
146
- console.log('Must use physical device for Push Notifications');
147
141
  }
148
- } catch (e) {
149
- console.warn("Failed to setup push notifications dynamically:", e);
142
+ } catch {
143
+ // Notification setup failed silently
150
144
  }
151
145
 
152
146
  return token;
@@ -1,24 +1,26 @@
1
+ import type { ImageSourcePropType } from "react-native";
2
+
1
3
  declare module "*.png" {
2
- const value: any;
4
+ const value: ImageSourcePropType;
3
5
  export default value;
4
6
  }
5
7
 
6
8
  declare module "*.jpg" {
7
- const value: any;
9
+ const value: ImageSourcePropType;
8
10
  export default value;
9
11
  }
10
12
 
11
13
  declare module "*.jpeg" {
12
- const value: any;
14
+ const value: ImageSourcePropType;
13
15
  export default value;
14
16
  }
15
17
 
16
18
  declare module "*.gif" {
17
- const value: any;
19
+ const value: ImageSourcePropType;
18
20
  export default value;
19
21
  }
20
22
 
21
23
  declare module "*.svg" {
22
- const value: any;
24
+ const value: ImageSourcePropType;
23
25
  export default value;
24
26
  }
@@ -0,0 +1,47 @@
1
+ import Constants, { ExecutionEnvironment } from "expo-constants";
2
+ import { TurboModuleRegistry, NativeModules } from "react-native";
3
+
4
+ /**
5
+ * Checks whether a specific native module or TurboModule is linked in the native binary.
6
+ * Uses non-throwing lookups on both TurboModuleRegistry and NativeModules.
7
+ */
8
+ export const hasNativeModule = (name: string): boolean => {
9
+ try {
10
+ if (TurboModuleRegistry?.get && TurboModuleRegistry.get(name) != null) {
11
+ return true;
12
+ }
13
+ if (NativeModules && NativeModules[name] != null) {
14
+ return true;
15
+ }
16
+ } catch {
17
+ return false;
18
+ }
19
+ return false;
20
+ };
21
+
22
+ /**
23
+ * Returns true if the app is currently running inside Expo Go.
24
+ * Robust against early bundle evaluation where Constants.appOwnership may be null/undefined.
25
+ */
26
+ export const isExpoGo = (): boolean => {
27
+ try {
28
+ // Legacy appOwnership check (SDK < 50 or when populated)
29
+ if (Constants.appOwnership === "expo") {
30
+ return true;
31
+ }
32
+ // Modern execution environment check (SDK 50+)
33
+ if (Constants.executionEnvironment === ExecutionEnvironment.StoreClient) {
34
+ return true;
35
+ }
36
+ // Fallback: Check if custom third-party native modules absent in Expo Go are missing
37
+ const hasRNPermissions = hasNativeModule("RNPermissions");
38
+ const hasRNGoogleSignin = hasNativeModule("RNGoogleSignin");
39
+
40
+ if (!hasRNPermissions && !hasRNGoogleSignin) {
41
+ return true;
42
+ }
43
+ } catch {
44
+ return true;
45
+ }
46
+ return false;
47
+ };
@@ -0,0 +1,206 @@
1
+ import { Platform } from "react-native";
2
+ import { toast } from "./toast";
3
+ import * as FileSystem from "expo-file-system";
4
+ import * as Sharing from "expo-sharing";
5
+
6
+ /**
7
+ * Downloads a string or base64 data to a file.
8
+ * - On Web: triggers browser download via an anchor element and Blob.
9
+ * - On Android: saves to user-selected folder via StorageAccessFramework.
10
+ * - On iOS: writes to app document directory and opens native iOS Share/Save sheet.
11
+ */
12
+ export const downloadFileString = async (
13
+ content: string,
14
+ filename: string,
15
+ mimeType: string,
16
+ isBase64: boolean = false
17
+ ): Promise<boolean> => {
18
+ if (Platform.OS === "web") {
19
+ try {
20
+ let blob: Blob;
21
+ if (isBase64) {
22
+ const byteCharacters = atob(content);
23
+ const byteArrays: Uint8Array[] = [];
24
+ for (let offset = 0; offset < byteCharacters.length; offset += 512) {
25
+ const slice = byteCharacters.slice(offset, offset + 512);
26
+ const byteNumbers = new Array(slice.length);
27
+ for (let i = 0; i < slice.length; i++) {
28
+ byteNumbers[i] = slice.charCodeAt(i);
29
+ }
30
+ const byteArray = new Uint8Array(byteNumbers);
31
+ byteArrays.push(byteArray);
32
+ }
33
+ blob = new Blob(byteArrays as any, { type: mimeType });
34
+ } else {
35
+ blob = new Blob([content], { type: mimeType });
36
+ }
37
+ const link = document.createElement("a");
38
+ const url = URL.createObjectURL(blob);
39
+ link.setAttribute("href", url);
40
+ link.setAttribute("download", filename);
41
+ link.style.visibility = "hidden";
42
+ document.body.appendChild(link);
43
+ link.click();
44
+ document.body.removeChild(link);
45
+ URL.revokeObjectURL(url);
46
+ return true;
47
+ } catch (error: unknown) {
48
+ const err = error as Error;
49
+ toast.error("Error", err.message || `Failed to download ${filename}`);
50
+ return false;
51
+ }
52
+ }
53
+
54
+ try {
55
+ const encoding = isBase64
56
+ ? FileSystem.EncodingType.Base64
57
+ : FileSystem.EncodingType.UTF8;
58
+
59
+ if (Platform.OS === "android") {
60
+ const permissions =
61
+ await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
62
+ if (!permissions.granted) {
63
+ toast.info("Cancelled", "Directory permission was not granted.");
64
+ return false;
65
+ }
66
+
67
+ const createdFileUri =
68
+ await FileSystem.StorageAccessFramework.createFileAsync(
69
+ permissions.directoryUri,
70
+ filename,
71
+ mimeType
72
+ );
73
+ await FileSystem.writeAsStringAsync(createdFileUri, content, {
74
+ encoding,
75
+ });
76
+ toast.success("Success", `${filename} saved successfully!`);
77
+ return true;
78
+ }
79
+
80
+ // iOS and other native platforms
81
+ const baseDir =
82
+ FileSystem.documentDirectory ?? FileSystem.cacheDirectory ?? "";
83
+ const fileUri = `${baseDir}${filename}`;
84
+ await FileSystem.writeAsStringAsync(fileUri, content, { encoding });
85
+
86
+ const isSharingAvailable = await Sharing.isAvailableAsync();
87
+ if (isSharingAvailable) {
88
+ await Sharing.shareAsync(fileUri, {
89
+ mimeType,
90
+ dialogTitle: `Save ${filename}`,
91
+ UTI: mimeType,
92
+ });
93
+ toast.success("Success", "File ready to save or share.");
94
+ return true;
95
+ } else {
96
+ toast.error("Error", "Sharing is not available on this device.");
97
+ return false;
98
+ }
99
+ } catch (error: unknown) {
100
+ const err = error as Error;
101
+ toast.error("Error", err.message || `Failed to download ${filename}`);
102
+ return false;
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Downloads a CSV string as a .csv file.
108
+ */
109
+ export const downloadCsvString = async (
110
+ csvString: string,
111
+ filename: string = "export.csv"
112
+ ): Promise<boolean> => {
113
+ return downloadFileString(csvString, filename, "text/csv;charset=utf-8;");
114
+ };
115
+
116
+ /**
117
+ * Downloads JSON data as a formatted .json file.
118
+ */
119
+ export const downloadJson = async (
120
+ data: unknown,
121
+ filename: string = "export.json"
122
+ ): Promise<boolean> => {
123
+ const jsonString = JSON.stringify(data, null, 2);
124
+ return downloadFileString(jsonString, filename, "application/json");
125
+ };
126
+
127
+ /**
128
+ * Downloads a file from a remote URL.
129
+ * - On Web: triggers browser download.
130
+ * - On Mobile: downloads to local filesystem, then prompts to save on Android
131
+ * via StorageAccessFramework or opens iOS Share sheet.
132
+ */
133
+ export const downloadFileFromUrl = async (
134
+ url: string,
135
+ filename: string,
136
+ mimeType: string = "application/octet-stream"
137
+ ): Promise<boolean> => {
138
+ if (Platform.OS === "web") {
139
+ try {
140
+ const link = document.createElement("a");
141
+ link.href = url;
142
+ link.download = filename;
143
+ link.target = "_blank";
144
+ link.style.visibility = "hidden";
145
+ document.body.appendChild(link);
146
+ link.click();
147
+ document.body.removeChild(link);
148
+ return true;
149
+ } catch (error: unknown) {
150
+ const err = error as Error;
151
+ toast.error("Error", err.message || `Failed to download ${filename}`);
152
+ return false;
153
+ }
154
+ }
155
+
156
+ try {
157
+ const baseDir =
158
+ FileSystem.cacheDirectory ?? FileSystem.documentDirectory ?? "";
159
+ const localUri = `${baseDir}${filename}`;
160
+ const result = await FileSystem.downloadAsync(url, localUri);
161
+
162
+ if (Platform.OS === "android") {
163
+ const permissions =
164
+ await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
165
+ if (!permissions.granted) {
166
+ toast.info("Cancelled", "Directory permission was not granted.");
167
+ return false;
168
+ }
169
+
170
+ const fileBase64 = await FileSystem.readAsStringAsync(result.uri, {
171
+ encoding: FileSystem.EncodingType.Base64,
172
+ });
173
+
174
+ const createdFileUri =
175
+ await FileSystem.StorageAccessFramework.createFileAsync(
176
+ permissions.directoryUri,
177
+ filename,
178
+ mimeType
179
+ );
180
+
181
+ await FileSystem.writeAsStringAsync(createdFileUri, fileBase64, {
182
+ encoding: FileSystem.EncodingType.Base64,
183
+ });
184
+
185
+ toast.success("Success", `${filename} saved successfully!`);
186
+ return true;
187
+ }
188
+
189
+ const isSharingAvailable = await Sharing.isAvailableAsync();
190
+ if (isSharingAvailable) {
191
+ await Sharing.shareAsync(result.uri, {
192
+ mimeType,
193
+ dialogTitle: `Save ${filename}`,
194
+ });
195
+ toast.success("Success", "File ready to save or share.");
196
+ return true;
197
+ } else {
198
+ toast.error("Error", "Sharing is not available on this device.");
199
+ return false;
200
+ }
201
+ } catch (error: unknown) {
202
+ const err = error as Error;
203
+ toast.error("Error", err.message || `Failed to download ${filename}`);
204
+ return false;
205
+ }
206
+ };
@@ -1,7 +1,7 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage';
2
2
 
3
3
  // Save data
4
- export const save = async (key: string, value: any) => {
4
+ export const save = async (key: string, value: unknown) => {
5
5
  if (!key || !value) return;
6
6
  try {
7
7
  const jsonValue = JSON.stringify(value);
@@ -7,6 +7,12 @@
7
7
  "dev": "vite",
8
8
  "build": "tsc && vite build",
9
9
  "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
+ "lint:fix": "eslint . --ext ts,tsx --fix",
11
+ "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
12
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"",
13
+ "circular-check": "dpdm --circular --no-warning --no-tree --exit-code circular:1 \"src/**/*.{ts,tsx}\"",
14
+ "ts-prune": "ts-prune",
15
+ "ts-purns": "ts-prune",
10
16
  "preview": "vite preview"
11
17
  },
12
18
  "dependencies": {
@@ -37,10 +43,13 @@
37
43
  "@typescript-eslint/eslint-plugin": "^6.10.0",
38
44
  "@typescript-eslint/parser": "^6.10.0",
39
45
  "@vitejs/plugin-react": "^4.2.0",
46
+ "dpdm": "^3.14.0",
40
47
  "eslint": "^8.53.0",
41
48
  "eslint-plugin-react": "^7.33.2",
42
49
  "eslint-plugin-react-hooks": "^4.6.0",
43
50
  "eslint-plugin-react-refresh": "^0.4.4",
51
+ "prettier": "^3.4.2",
52
+ "ts-prune": "^0.10.3",
44
53
  "typescript": "^5.2.2",
45
54
  "vite": "^5.0.0"
46
55
  }
@@ -22,16 +22,8 @@
22
22
  "package": "com.anonymous.mobile",
23
23
  "permissions": [
24
24
  "android.permission.CAMERA",
25
- "android.permission.RECORD_AUDIO",
26
25
  "android.permission.ACCESS_FINE_LOCATION",
27
- "android.permission.ACCESS_COARSE_LOCATION",
28
- "android.permission.READ_CONTACTS",
29
- "android.permission.READ_CALENDAR",
30
- "android.permission.READ_EXTERNAL_STORAGE",
31
- "android.permission.WRITE_EXTERNAL_STORAGE",
32
- "android.permission.BLUETOOTH_CONNECT",
33
- "android.permission.BLUETOOTH_SCAN",
34
- "android.permission.CALL_PHONE"
26
+ "android.permission.ACCESS_COARSE_LOCATION"
35
27
  ]
36
28
  },
37
29
  "web": {
@@ -58,14 +50,9 @@
58
50
  {
59
51
  "iosPermissions": [
60
52
  "Camera",
61
- "Microphone",
62
- "LocationAlways",
63
53
  "LocationWhenInUse",
64
54
  "Notifications",
65
- "PhotoLibrary",
66
- "Contacts",
67
- "Calendars",
68
- "BluetoothPeripheral"
55
+ "PhotoLibrary"
69
56
  ]
70
57
  }
71
58
  ]