@imksh/create 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/create.js +2 -1
- package/src/index.js +1 -1
- package/src/utils/askTemplate.js +1 -0
- package/templates/mern-react-native/mobile/_gitignore +43 -0
- package/templates/mern-react-native/mobile/package.json +2 -1
- package/templates/mern-react-native/mobile/src/app/(auth)/edit-profile.tsx +11 -15
- package/templates/mern-react-native/mobile/src/app/(tabs)/dashboard.tsx +2 -2
- package/templates/mern-react-native/mobile/src/app/(tabs)/search.tsx +2 -2
- package/templates/mern-react-native/mobile/src/config/sidebar.config.ts +0 -60
- package/templates/mern-react-native/mobile/src/config/tabNavigation.ts +6 -36
- package/templates/mern-react-native/mobile/src/services/notification.service.ts +22 -0
- package/templates/mern-react-native/mobile/src/store/useNotificationStore.ts +158 -33
- package/templates/next/next.config.mjs +1 -0
- package/templates/next/package.json +10 -9
- package/templates/next/postcss.config.js +5 -0
- package/templates/next/src/app/layout.js +1 -0
- package/templates/next/src/app/page.js +1 -1
- package/templates/next/src/components/layout/ProtectedRoute.jsx +3 -3
- package/templates/next/src/index.css +1 -0
- package/templates/next/src/store/useUiStore.js +13 -7
- package/templates/next-ts/.env.example +0 -0
- package/templates/next-ts/AGENTS.md +37 -0
- package/templates/next-ts/CLAUDE.md +1 -0
- package/templates/next-ts/next-env.d.ts +3 -1
- package/templates/next-ts/next.config.mjs +1 -0
- package/templates/next-ts/package.json +13 -12
- package/templates/next-ts/postcss.config.js +5 -0
- package/templates/next-ts/public/logo.png +0 -0
- package/templates/next-ts/src/app/error.tsx +41 -0
- package/templates/next-ts/src/app/layout.tsx +2 -1
- package/templates/next-ts/src/app/not-found.tsx +5 -0
- package/templates/next-ts/src/app/page.tsx +1 -1
- package/templates/next-ts/src/components/layout/ProtectedRoute.tsx +3 -3
- package/templates/next-ts/src/components/ui/Button.tsx +63 -0
- package/templates/next-ts/src/components/ui/Input.tsx +79 -0
- package/templates/next-ts/src/index.css +1 -0
- package/templates/next-ts/src/store/useUiStore.ts +13 -7
- package/templates/next-ts/tsconfig.json +19 -5
- package/templates/production-fullstack/client/_gitignore +24 -0
- package/templates/production-fullstack/client/index.html +14 -0
- package/templates/production-fullstack/client/package.json +47 -0
- package/templates/production-fullstack/client/public/_redirects +1 -0
- package/templates/production-fullstack/client/public/favicon.ico +0 -0
- package/templates/production-fullstack/client/public/favicon.svg +1 -0
- package/templates/production-fullstack/client/public/logo.png +0 -0
- package/templates/production-fullstack/client/public/manifest.json +24 -0
- package/templates/production-fullstack/client/public/sw.js +56 -0
- package/templates/production-fullstack/client/src/App.tsx +68 -0
- package/templates/production-fullstack/client/src/assets/animations/loading.json +1 -0
- package/templates/production-fullstack/client/src/assets/data/navigation.ts +29 -0
- package/templates/production-fullstack/client/src/assets/images/logo.png +0 -0
- package/templates/production-fullstack/client/src/components/layout/ProtectedRoute.tsx +23 -0
- package/templates/production-fullstack/client/src/components/layout/PublicLayout.tsx +13 -0
- package/templates/production-fullstack/client/src/components/layout/SidebarLayout.tsx +37 -0
- package/templates/production-fullstack/client/src/components/shared/Breadcrumbs.tsx +27 -0
- package/templates/production-fullstack/client/src/components/shared/LottieIcon.tsx +36 -0
- package/templates/production-fullstack/client/src/components/shared/NotFound.tsx +78 -0
- package/templates/production-fullstack/client/src/components/shared/PwaInstallPrompt.tsx +72 -0
- package/templates/production-fullstack/client/src/components/shared/SEO.tsx +67 -0
- package/templates/production-fullstack/client/src/components/shared/Scroll.tsx +25 -0
- package/templates/production-fullstack/client/src/components/shared/Unauthorized.tsx +76 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/CardSkeleton.tsx +23 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/DashboardSkeleton.tsx +19 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/ListSkeleton.tsx +24 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/PageSkeleton.tsx +26 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/ProfileSkeleton.tsx +23 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/Skeleton.tsx +18 -0
- package/templates/production-fullstack/client/src/components/shared/skeletons/common/TableSkeleton.tsx +33 -0
- package/templates/production-fullstack/client/src/components/ui/ConfirmModal.tsx +152 -0
- package/templates/production-fullstack/client/src/components/ui/Header.tsx +230 -0
- package/templates/production-fullstack/client/src/components/ui/Loading.tsx +19 -0
- package/templates/production-fullstack/client/src/components/ui/Modal.tsx +106 -0
- package/templates/production-fullstack/client/src/components/ui/SidebarHeader.tsx +34 -0
- package/templates/production-fullstack/client/src/components/ui/Table.tsx +118 -0
- package/templates/production-fullstack/client/src/components/ui/ToggleSwitch.tsx +38 -0
- package/templates/production-fullstack/client/src/components/ui/sidebar.tsx +152 -0
- package/templates/production-fullstack/client/src/config/index.ts +44 -0
- package/templates/production-fullstack/client/src/constants/navigation.ts +14 -0
- package/templates/production-fullstack/client/src/features/auth/api/authService.ts +19 -0
- package/templates/production-fullstack/client/src/features/auth/pages/Login.tsx +233 -0
- package/templates/production-fullstack/client/src/features/auth/store/useAuthStore.ts +143 -0
- package/templates/production-fullstack/client/src/features/core/pages/Home.tsx +150 -0
- package/templates/production-fullstack/client/src/features/core/pages/Profile.tsx +9 -0
- package/templates/production-fullstack/client/src/hooks/useLenis.ts +28 -0
- package/templates/production-fullstack/client/src/hooks/usePwaInstall.ts +45 -0
- package/templates/production-fullstack/client/src/hooks/useWindowSize.ts +25 -0
- package/templates/production-fullstack/client/src/index.css +158 -0
- package/templates/production-fullstack/client/src/lib/axios.ts +22 -0
- package/templates/production-fullstack/client/src/lib/dayjs.ts +12 -0
- package/templates/production-fullstack/client/src/lib/query-client.ts +11 -0
- package/templates/production-fullstack/client/src/lib/socket.ts +6 -0
- package/templates/production-fullstack/client/src/main.tsx +28 -0
- package/templates/production-fullstack/client/src/store/useUiStore.ts +74 -0
- package/templates/production-fullstack/client/src/utils/formatDate.ts +19 -0
- package/templates/production-fullstack/client/src/utils/getInitials.ts +10 -0
- package/templates/production-fullstack/client/src/vite-env.d.ts +1 -0
- package/templates/production-fullstack/client/tsconfig.json +25 -0
- package/templates/production-fullstack/client/tsconfig.node.json +10 -0
- package/templates/production-fullstack/client/vite.config.ts +8 -0
- package/templates/production-fullstack/mobile/LICENSE +21 -0
- package/templates/production-fullstack/mobile/_gitignore +43 -0
- package/templates/production-fullstack/mobile/app.json +84 -0
- package/templates/production-fullstack/mobile/assets/animations/celebrate.json +1 -0
- package/templates/production-fullstack/mobile/assets/animations/infinity.json +1 -0
- package/templates/production-fullstack/mobile/assets/animations/loading.json +1 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_Bold.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_ExtraBold.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_Medium.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_Regular.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/fonts/Inter_SemiBold.ttf +0 -0
- package/templates/production-fullstack/mobile/assets/images/logo/logo.png +0 -0
- package/templates/production-fullstack/mobile/assets/images/logo/logo.svg +1 -0
- package/templates/production-fullstack/mobile/assets/images/logo/logoWithBg.png +0 -0
- package/templates/production-fullstack/mobile/babel.config.js +13 -0
- package/templates/production-fullstack/mobile/drizzle.config.ts +8 -0
- package/templates/production-fullstack/mobile/eas.json +24 -0
- package/templates/production-fullstack/mobile/eslint.config.js +10 -0
- package/templates/production-fullstack/mobile/expo-env.d.ts +3 -0
- package/templates/production-fullstack/mobile/metro.config.js +6 -0
- package/templates/production-fullstack/mobile/nativewind-env.d.ts +3 -0
- package/templates/production-fullstack/mobile/package.json +89 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/_layout.tsx +12 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/change-password.tsx +112 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/edit-profile.tsx +160 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/forgot-password.tsx +191 -0
- package/templates/production-fullstack/mobile/src/app/(auth)/login.tsx +253 -0
- package/templates/production-fullstack/mobile/src/app/_layout.tsx +102 -0
- package/templates/production-fullstack/mobile/src/app/global.css +82 -0
- package/templates/production-fullstack/mobile/src/components/common/AnimatedHeader.tsx +328 -0
- package/templates/production-fullstack/mobile/src/components/common/ConfirmationToast.tsx +67 -0
- package/templates/production-fullstack/mobile/src/components/common/Loading.tsx +26 -0
- package/templates/production-fullstack/mobile/src/components/common/LocationLoader.tsx +591 -0
- package/templates/production-fullstack/mobile/src/components/common/ScreenHeader.tsx +68 -0
- package/templates/production-fullstack/mobile/src/components/common/TabHeader.tsx +58 -0
- package/templates/production-fullstack/mobile/src/components/common/Typography.tsx +113 -0
- package/templates/production-fullstack/mobile/src/components/common/Welcome.tsx +55 -0
- package/templates/production-fullstack/mobile/src/components/ui/BottomModal.tsx +159 -0
- package/templates/production-fullstack/mobile/src/components/ui/ClassicTabBar.tsx +129 -0
- package/templates/production-fullstack/mobile/src/components/ui/CustomTabBar.tsx +238 -0
- package/templates/production-fullstack/mobile/src/components/ui/Sidebar.tsx +258 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/Button.tsx +180 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/ButtonGroup.tsx +32 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/FloatingActionButton.tsx +72 -0
- package/templates/production-fullstack/mobile/src/components/ui/buttons/IconButton.tsx +84 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Accordion.tsx +82 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Avatar.tsx +47 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Badge.tsx +104 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Card.tsx +36 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/Image.tsx +43 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/ImageViewer.tsx +33 -0
- package/templates/production-fullstack/mobile/src/components/ui/data-display/SwipeableRow.tsx +94 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/CircularProgress.tsx +38 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/GestureWrapper.tsx +114 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/ProgressBar.tsx +51 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/Skeleton.tsx +84 -0
- package/templates/production-fullstack/mobile/src/components/ui/feedback/Spinner.tsx +50 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Checkbox.tsx +54 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/DatePicker.tsx +186 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/ImagePickerModal.tsx +129 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Input.tsx +158 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/LocationPicker.tsx +211 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/OTPInput.tsx +83 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/PasswordInput.tsx +16 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Radio.tsx +54 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/SearchBar.tsx +53 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Slider.tsx +49 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/Switch.tsx +75 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/TextArea.tsx +15 -0
- package/templates/production-fullstack/mobile/src/components/ui/inputs/WebLocationPicker.tsx +321 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Container.tsx +23 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Divider.tsx +20 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Grid.tsx +62 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/KeyboardAvoidingViewWrapper.tsx +16 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/ScrollContainer.tsx +21 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Section.tsx +51 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Spacer.tsx +6 -0
- package/templates/production-fullstack/mobile/src/components/ui/layout/Stack.tsx +34 -0
- package/templates/production-fullstack/mobile/src/components/ui/navigation/Pagination.tsx +61 -0
- package/templates/production-fullstack/mobile/src/components/ui/navigation/ProgressStepper.tsx +66 -0
- package/templates/production-fullstack/mobile/src/components/ui/navigation/Tabs.tsx +173 -0
- package/templates/production-fullstack/mobile/src/config/sidebar.config.ts +72 -0
- package/templates/production-fullstack/mobile/src/config/tabNavigation.ts +64 -0
- package/templates/production-fullstack/mobile/src/constants/Colors.ts +71 -0
- package/templates/production-fullstack/mobile/src/db/index.ts +9 -0
- package/templates/production-fullstack/mobile/src/db/schema.ts +14 -0
- package/templates/production-fullstack/mobile/src/features/auth/api/auth.service.ts +35 -0
- package/templates/production-fullstack/mobile/src/features/auth/store/useAuthStore.ts +273 -0
- package/templates/production-fullstack/mobile/src/features/auth/types/auth.type.ts +14 -0
- package/templates/production-fullstack/mobile/src/hooks/useDebounce.ts +17 -0
- package/templates/production-fullstack/mobile/src/lib/axios.ts +27 -0
- package/templates/production-fullstack/mobile/src/lib/query-client.ts +11 -0
- package/templates/production-fullstack/mobile/src/lib/socket.ts +36 -0
- package/templates/production-fullstack/mobile/src/lib/storage.ts +55 -0
- package/templates/production-fullstack/mobile/src/services/permission.service.ts +214 -0
- package/templates/production-fullstack/mobile/src/store/localStore.ts +7 -0
- package/templates/production-fullstack/mobile/src/store/useNotificationStore.ts +53 -0
- package/templates/production-fullstack/mobile/src/types/common.type.ts +6 -0
- package/templates/production-fullstack/mobile/src/types/images.d.ts +24 -0
- package/templates/production-fullstack/mobile/src/utils/toast.ts +26 -0
- package/templates/production-fullstack/mobile/tailwind.config.js +71 -0
- package/templates/production-fullstack/mobile/tsconfig.json +23 -0
- package/templates/production-fullstack/server/.env.example +63 -0
- package/templates/production-fullstack/server/README.md +75 -0
- package/templates/production-fullstack/server/_gitignore +24 -0
- package/templates/production-fullstack/server/eslint.config.js +13 -0
- package/templates/production-fullstack/server/package.json +58 -0
- package/templates/production-fullstack/server/prisma/schema/core/attachment.prisma +14 -0
- package/templates/production-fullstack/server/prisma/schema/core/notification.prisma +18 -0
- package/templates/production-fullstack/server/prisma/schema/core/permission.prisma +26 -0
- package/templates/production-fullstack/server/prisma/schema/core/role.prisma +25 -0
- package/templates/production-fullstack/server/prisma/schema/core/user.prisma +56 -0
- package/templates/production-fullstack/server/prisma/schema/enums/common.prisma +13 -0
- package/templates/production-fullstack/server/prisma/schema/enums/notification.prisma +6 -0
- package/templates/production-fullstack/server/prisma/schema/enums/user.prisma +11 -0
- package/templates/production-fullstack/server/prisma/schema/schema.prisma +7 -0
- package/templates/production-fullstack/server/prisma.config.ts +14 -0
- package/templates/production-fullstack/server/src/app.ts +36 -0
- package/templates/production-fullstack/server/src/common/middlewares/auth.middleware.ts +45 -0
- package/templates/production-fullstack/server/src/common/middlewares/errorHandler.ts +16 -0
- package/templates/production-fullstack/server/src/common/middlewares/multer.ts +20 -0
- package/templates/production-fullstack/server/src/common/middlewares/rateLimiter.ts +53 -0
- package/templates/production-fullstack/server/src/common/services/email.service.ts +28 -0
- package/templates/production-fullstack/server/src/common/services/push.service.ts +65 -0
- package/templates/production-fullstack/server/src/common/services/storage.service.ts +9 -0
- package/templates/production-fullstack/server/src/common/types/express.d.ts +10 -0
- package/templates/production-fullstack/server/src/common/utils/cloudinaryUpload.ts +44 -0
- package/templates/production-fullstack/server/src/common/utils/emailOtp.ts +30 -0
- package/templates/production-fullstack/server/src/common/utils/genOtpToken.ts +22 -0
- package/templates/production-fullstack/server/src/common/utils/generateVapidKeys.ts +12 -0
- package/templates/production-fullstack/server/src/common/utils/jwt.ts +27 -0
- package/templates/production-fullstack/server/src/common/utils/pushHelper.ts +22 -0
- package/templates/production-fullstack/server/src/config/env.ts +2 -0
- package/templates/production-fullstack/server/src/config/logger.ts +58 -0
- package/templates/production-fullstack/server/src/config/prisma.ts +26 -0
- package/templates/production-fullstack/server/src/features/attachments/upload.controller.ts +22 -0
- package/templates/production-fullstack/server/src/features/attachments/upload.routes.ts +11 -0
- package/templates/production-fullstack/server/src/features/auth/auth.controller.ts +124 -0
- package/templates/production-fullstack/server/src/features/auth/auth.routes.ts +22 -0
- package/templates/production-fullstack/server/src/features/auth/auth.service.ts +155 -0
- package/templates/production-fullstack/server/src/features/notifications/push.controller.ts +55 -0
- package/templates/production-fullstack/server/src/features/notifications/push.routes.ts +11 -0
- package/templates/production-fullstack/server/src/index.ts +17 -0
- package/templates/production-fullstack/server/src/routes/index.ts +12 -0
- package/templates/production-fullstack/server/src/sockets/auth.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/connection.ts +31 -0
- package/templates/production-fullstack/server/src/sockets/emitters/assignment.emitter.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/emitters/attendance.emitter.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/emitters/exam.emitter.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/emitters/notification.emitter.ts +17 -0
- package/templates/production-fullstack/server/src/sockets/events.ts +27 -0
- package/templates/production-fullstack/server/src/sockets/handlers/attendance.handler.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/handlers/chat.handler.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/handlers/index.ts +15 -0
- package/templates/production-fullstack/server/src/sockets/handlers/lms.handler.ts +0 -0
- package/templates/production-fullstack/server/src/sockets/handlers/notification.handler.ts +39 -0
- package/templates/production-fullstack/server/src/sockets/index.ts +39 -0
- package/templates/production-fullstack/server/src/sockets/middleware.ts +54 -0
- package/templates/production-fullstack/server/src/sockets/rooms.ts +11 -0
- package/templates/production-fullstack/server/src/sockets/types.ts +32 -0
- package/templates/production-fullstack/server/tsconfig.json +14 -0
- package/templates/react/src/assets/animations/loading.json +1 -0
- package/templates/react-native/_gitignore +43 -0
- package/templates/react-native/package.json +2 -1
- package/templates/react-native/src/app/(auth)/edit-profile.tsx +11 -15
- package/templates/react-native/src/app/(tabs)/dashboard.tsx +2 -2
- package/templates/react-native/src/app/(tabs)/search.tsx +2 -2
- package/templates/react-native/src/config/sidebar.config.ts +0 -60
- package/templates/react-native/src/config/tabNavigation.ts +6 -36
- package/templates/react-native/src/services/notification.service.ts +22 -0
- package/templates/react-native/src/store/useNotificationStore.ts +158 -33
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { authService } from "../api/auth.service";
|
|
3
|
+
import { save, remove, get } from "../../../lib/storage";
|
|
4
|
+
import { User } from "../types/auth.type";
|
|
5
|
+
import { initializeSocket, disconnectSocket } from "../../../lib/socket";
|
|
6
|
+
import { useNotificationStore } from "../../../store/useNotificationStore";
|
|
7
|
+
|
|
8
|
+
// ── Module-level prefetch ──────────────────────────────────────────────────
|
|
9
|
+
// Kick off the AsyncStorage read the instant this module is imported
|
|
10
|
+
// (before any React component mounts). By the time loadCachedUser() is
|
|
11
|
+
// called from a useEffect, the promise is already resolved or very close.
|
|
12
|
+
const _prefetchedUser: Promise<User | null> = get("user");
|
|
13
|
+
|
|
14
|
+
export interface AuthState {
|
|
15
|
+
user: User | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
isCheckingAuth: boolean;
|
|
18
|
+
error: string | null;
|
|
19
|
+
sendOtp: (email: string) => Promise<boolean>;
|
|
20
|
+
signup: (
|
|
21
|
+
name: string,
|
|
22
|
+
email: string,
|
|
23
|
+
password: string,
|
|
24
|
+
otp: string,
|
|
25
|
+
role?: string,
|
|
26
|
+
) => Promise<boolean>;
|
|
27
|
+
login: (email: string, password: string) => Promise<boolean>;
|
|
28
|
+
demoLogin: () => Promise<void>;
|
|
29
|
+
getMe: () => Promise<void>;
|
|
30
|
+
updateProfile: (updates: Partial<User> | FormData) => Promise<boolean>;
|
|
31
|
+
logout: () => Promise<void>;
|
|
32
|
+
genOtp: (email: string) => Promise<boolean>;
|
|
33
|
+
verifyOtp: (email: string, otp: string) => Promise<boolean>;
|
|
34
|
+
resetPassword: (newPassword: string) => Promise<boolean>;
|
|
35
|
+
changePassword: (oldPassword: string, newPassword: string) => Promise<boolean>;
|
|
36
|
+
setUser: (user: User) => void;
|
|
37
|
+
clearError: () => void;
|
|
38
|
+
loadCachedUser: () => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
42
|
+
user: null,
|
|
43
|
+
loading: false,
|
|
44
|
+
isCheckingAuth: true,
|
|
45
|
+
error: null,
|
|
46
|
+
|
|
47
|
+
setUser: (user: User) => set({ user }),
|
|
48
|
+
|
|
49
|
+
clearError: () => set({ error: null }),
|
|
50
|
+
|
|
51
|
+
loadCachedUser: async () => {
|
|
52
|
+
// Await the already-in-flight prefetch — typically resolves in <1ms here
|
|
53
|
+
const cached = await _prefetchedUser;
|
|
54
|
+
if (cached) {
|
|
55
|
+
set({ user: cached, isCheckingAuth: false });
|
|
56
|
+
// Re-establish socket + push on app resume with a cached session
|
|
57
|
+
initializeSocket();
|
|
58
|
+
const notifStore = useNotificationStore.getState();
|
|
59
|
+
notifStore.listenForSocketEvents();
|
|
60
|
+
notifStore.registerForPushNotificationsAsync();
|
|
61
|
+
notifStore.fetchNotifications();
|
|
62
|
+
} else {
|
|
63
|
+
// No cache — mark auth check done so routing doesn't hang
|
|
64
|
+
set({ isCheckingAuth: false });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
sendOtp: async (email: string) => {
|
|
69
|
+
set({ loading: true, error: null });
|
|
70
|
+
try {
|
|
71
|
+
await authService.sendOtp(email);
|
|
72
|
+
return true;
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
set({
|
|
75
|
+
error: err.response?.data?.message || err.message,
|
|
76
|
+
loading: false,
|
|
77
|
+
});
|
|
78
|
+
return false;
|
|
79
|
+
} finally {
|
|
80
|
+
set({ loading: false });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
signup: async (
|
|
85
|
+
name: string,
|
|
86
|
+
email: string,
|
|
87
|
+
password: string,
|
|
88
|
+
otp: string,
|
|
89
|
+
role = "user",
|
|
90
|
+
) => {
|
|
91
|
+
set({ loading: true, error: null });
|
|
92
|
+
try {
|
|
93
|
+
const { data } = await authService.signup(
|
|
94
|
+
name,
|
|
95
|
+
email,
|
|
96
|
+
password,
|
|
97
|
+
otp,
|
|
98
|
+
role,
|
|
99
|
+
);
|
|
100
|
+
if (data.data?.token) {
|
|
101
|
+
save("token", data.data.token);
|
|
102
|
+
}
|
|
103
|
+
const user = data.data?.user;
|
|
104
|
+
if (user) save("user", user);
|
|
105
|
+
set({
|
|
106
|
+
user,
|
|
107
|
+
loading: false,
|
|
108
|
+
});
|
|
109
|
+
// Initialise socket + push after signup
|
|
110
|
+
initializeSocket();
|
|
111
|
+
const notifStore = useNotificationStore.getState();
|
|
112
|
+
notifStore.listenForSocketEvents();
|
|
113
|
+
notifStore.registerForPushNotificationsAsync();
|
|
114
|
+
notifStore.fetchNotifications();
|
|
115
|
+
return true;
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
set({
|
|
118
|
+
error: err.response?.data?.message || err.message,
|
|
119
|
+
loading: false,
|
|
120
|
+
});
|
|
121
|
+
return false;
|
|
122
|
+
} finally {
|
|
123
|
+
set({ loading: false });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
login: async (email: string, password: string) => {
|
|
128
|
+
set({ loading: true, error: null });
|
|
129
|
+
try {
|
|
130
|
+
const { data } = await authService.login(email, password);
|
|
131
|
+
if (data.data?.token) {
|
|
132
|
+
save("token", data.data.token);
|
|
133
|
+
}
|
|
134
|
+
const user = data.data?.user;
|
|
135
|
+
if (user) save("user", user);
|
|
136
|
+
set({
|
|
137
|
+
user,
|
|
138
|
+
loading: false,
|
|
139
|
+
});
|
|
140
|
+
// Initialise socket + push after login
|
|
141
|
+
initializeSocket();
|
|
142
|
+
const notifStore = useNotificationStore.getState();
|
|
143
|
+
notifStore.listenForSocketEvents();
|
|
144
|
+
notifStore.registerForPushNotificationsAsync();
|
|
145
|
+
notifStore.fetchNotifications();
|
|
146
|
+
return true;
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
set({
|
|
149
|
+
error: err.response?.data?.message || err.message,
|
|
150
|
+
loading: false,
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
} finally {
|
|
154
|
+
set({ loading: false });
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
demoLogin: async () => {
|
|
159
|
+
set({ loading: true, error: null });
|
|
160
|
+
// Simulate network delay
|
|
161
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
162
|
+
save("token", "demo-token-123");
|
|
163
|
+
set({
|
|
164
|
+
user: {
|
|
165
|
+
id: "demo_user_123",
|
|
166
|
+
name: "Demo User",
|
|
167
|
+
email: "demo@imksh.com",
|
|
168
|
+
role: "admin",
|
|
169
|
+
},
|
|
170
|
+
loading: false,
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
getMe: async () => {
|
|
175
|
+
try {
|
|
176
|
+
const { data } = await authService.getMe();
|
|
177
|
+
const user = data.data;
|
|
178
|
+
// Always update cache with fresh server data
|
|
179
|
+
if (user) save("user", user);
|
|
180
|
+
set({ user, isCheckingAuth: false });
|
|
181
|
+
// Network failed — cached user (already loaded) stays in place
|
|
182
|
+
} finally {
|
|
183
|
+
set({ isCheckingAuth: false });
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
updateProfile: async (updates: Partial<User> | FormData) => {
|
|
188
|
+
set({ loading: true, error: null });
|
|
189
|
+
try {
|
|
190
|
+
const { data } = await authService.updateMe(updates);
|
|
191
|
+
set({ user: data.data, loading: false });
|
|
192
|
+
return true;
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
set({
|
|
195
|
+
error: err.response?.data?.message || err.message,
|
|
196
|
+
loading: false,
|
|
197
|
+
});
|
|
198
|
+
return false;
|
|
199
|
+
} finally {
|
|
200
|
+
set({ loading: false });
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
logout: async () => {
|
|
205
|
+
try {
|
|
206
|
+
await authService.logout();
|
|
207
|
+
} catch {
|
|
208
|
+
// ignore errors — cookie may already be expired
|
|
209
|
+
} finally {
|
|
210
|
+
await remove("token");
|
|
211
|
+
await remove("user"); // clear cached user on logout
|
|
212
|
+
disconnectSocket();
|
|
213
|
+
set({ loading: false });
|
|
214
|
+
}
|
|
215
|
+
set({
|
|
216
|
+
user: null,
|
|
217
|
+
error: null,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
genOtp: async (email: string) => {
|
|
222
|
+
set({ loading: true, error: null });
|
|
223
|
+
try {
|
|
224
|
+
await authService.genOtp(email);
|
|
225
|
+
return true;
|
|
226
|
+
} catch (err: any) {
|
|
227
|
+
set({ error: err.response?.data?.message || err.message, loading: false });
|
|
228
|
+
return false;
|
|
229
|
+
} finally {
|
|
230
|
+
set({ loading: false });
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
verifyOtp: async (email: string, otp: string) => {
|
|
235
|
+
set({ loading: true, error: null });
|
|
236
|
+
try {
|
|
237
|
+
const { data } = await authService.verifyOtp(email, otp);
|
|
238
|
+
if (data.token) save("token", data.token);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (err: any) {
|
|
241
|
+
set({ error: err.response?.data?.message || err.message, loading: false });
|
|
242
|
+
return false;
|
|
243
|
+
} finally {
|
|
244
|
+
set({ loading: false });
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
resetPassword: async (newPassword: string) => {
|
|
249
|
+
set({ loading: true, error: null });
|
|
250
|
+
try {
|
|
251
|
+
await authService.resetPassword(newPassword);
|
|
252
|
+
return true;
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
set({ error: err.response?.data?.message || err.message, loading: false });
|
|
255
|
+
return false;
|
|
256
|
+
} finally {
|
|
257
|
+
set({ loading: false });
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
changePassword: async (oldPassword: string, newPassword: string) => {
|
|
262
|
+
set({ loading: true, error: null });
|
|
263
|
+
try {
|
|
264
|
+
await authService.changePassword(oldPassword, newPassword);
|
|
265
|
+
return true;
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
set({ error: err.response?.data?.message || err.message, loading: false });
|
|
268
|
+
return false;
|
|
269
|
+
} finally {
|
|
270
|
+
set({ loading: false });
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
id: string; // Updated from _id for Prisma consistency
|
|
3
|
+
_id?: string; // Fallback for mongo legacy if any
|
|
4
|
+
name: string;
|
|
5
|
+
email: string;
|
|
6
|
+
phone?: string;
|
|
7
|
+
role: string;
|
|
8
|
+
avatar?: {
|
|
9
|
+
url?: string;
|
|
10
|
+
publicId?: string;
|
|
11
|
+
};
|
|
12
|
+
createdAt?: string;
|
|
13
|
+
updatedAt?: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const handler = setTimeout(() => {
|
|
8
|
+
setDebouncedValue(value);
|
|
9
|
+
}, delay);
|
|
10
|
+
|
|
11
|
+
return () => {
|
|
12
|
+
clearTimeout(handler);
|
|
13
|
+
};
|
|
14
|
+
}, [value, delay]);
|
|
15
|
+
|
|
16
|
+
return debouncedValue;
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
|
|
2
|
+
import { get } from "./storage";
|
|
3
|
+
import { ApiResponse } from "../types/common.type";
|
|
4
|
+
|
|
5
|
+
declare module 'axios' {
|
|
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>;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line import/no-named-as-default-member
|
|
16
|
+
export const api = axios.create({
|
|
17
|
+
baseURL: process.env.EXPO_PUBLIC_API_URL,
|
|
18
|
+
withCredentials: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
api.interceptors.request.use(async (config) => {
|
|
22
|
+
const token = await get("token");
|
|
23
|
+
if (token) {
|
|
24
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
25
|
+
}
|
|
26
|
+
return config;
|
|
27
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
export const queryClient = new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
refetchOnWindowFocus: false, // React Native doesn't have traditional window focus
|
|
7
|
+
retry: 1,
|
|
8
|
+
staleTime: 5 * 60 * 1000,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { io, Socket } from 'socket.io-client';
|
|
2
|
+
import { get } from './storage';
|
|
3
|
+
import Constants from 'expo-constants';
|
|
4
|
+
|
|
5
|
+
let socket: Socket | null = null;
|
|
6
|
+
|
|
7
|
+
// Set your backend socket URL
|
|
8
|
+
const SOCKET_URL = Constants.expoConfig?.extra?.apiUrl || 'http://localhost:3000';
|
|
9
|
+
|
|
10
|
+
export const initializeSocket = async () => {
|
|
11
|
+
if (socket) return;
|
|
12
|
+
const token = await get('token');
|
|
13
|
+
if (!token) return;
|
|
14
|
+
|
|
15
|
+
socket = io(SOCKET_URL, {
|
|
16
|
+
auth: { token },
|
|
17
|
+
transports: ['websocket'],
|
|
18
|
+
});
|
|
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
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getSocket = () => socket;
|
|
30
|
+
|
|
31
|
+
export const disconnectSocket = () => {
|
|
32
|
+
if (socket) {
|
|
33
|
+
socket.disconnect();
|
|
34
|
+
socket = null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
|
|
3
|
+
// Save data
|
|
4
|
+
export const save = async (key: string, value: any) => {
|
|
5
|
+
if (!key || !value) return;
|
|
6
|
+
try {
|
|
7
|
+
const jsonValue = JSON.stringify(value);
|
|
8
|
+
await AsyncStorage.setItem(key, jsonValue);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.warn("AsyncStorage save error:", error);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Get data
|
|
15
|
+
export const get = async (key: string) => {
|
|
16
|
+
try {
|
|
17
|
+
if (!AsyncStorage) return null;
|
|
18
|
+
const value = await AsyncStorage.getItem(key);
|
|
19
|
+
return value ? JSON.parse(value) : null;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn('AsyncStorage get error:', error);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getData = async (key: string) => {
|
|
27
|
+
try {
|
|
28
|
+
if (!AsyncStorage) return null;
|
|
29
|
+
const value = await AsyncStorage.getItem(key);
|
|
30
|
+
return value ? JSON.parse(value) : null;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn('AsyncStorage get error:', error);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Remove specific key
|
|
38
|
+
export const remove = async (key: string) => {
|
|
39
|
+
try {
|
|
40
|
+
if (!AsyncStorage) return;
|
|
41
|
+
await AsyncStorage.removeItem(key);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn('AsyncStorage remove error:', error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Clear entire storage
|
|
48
|
+
export const clear = async () => {
|
|
49
|
+
try {
|
|
50
|
+
if (!AsyncStorage) return;
|
|
51
|
+
await AsyncStorage.clear();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('AsyncStorage clear error:', error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
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";
|
|
11
|
+
|
|
12
|
+
export type AppPermissionType =
|
|
13
|
+
| "camera"
|
|
14
|
+
| "audio" // Microphone
|
|
15
|
+
| "notification" // Notifications (Push)
|
|
16
|
+
| "image" // Photo Library / Read External Storage
|
|
17
|
+
| "file" // Write External Storage
|
|
18
|
+
| "location" // Location When In Use
|
|
19
|
+
| "locationAlways" // Location Always
|
|
20
|
+
| "contacts" // Contacts
|
|
21
|
+
| "phone" // Call Phone
|
|
22
|
+
| "nearbydevice" // Bluetooth
|
|
23
|
+
| "calendar"; // Calendar
|
|
24
|
+
|
|
25
|
+
class PermissionService {
|
|
26
|
+
/**
|
|
27
|
+
* Helper to determine the correct native Permission object based on platform
|
|
28
|
+
*/
|
|
29
|
+
private getNativePermission(type: AppPermissionType): Permission | null {
|
|
30
|
+
if (Platform.OS === "ios") {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case "camera":
|
|
33
|
+
return PERMISSIONS.IOS.CAMERA;
|
|
34
|
+
case "audio":
|
|
35
|
+
return PERMISSIONS.IOS.MICROPHONE;
|
|
36
|
+
case "image":
|
|
37
|
+
return PERMISSIONS.IOS.PHOTO_LIBRARY;
|
|
38
|
+
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
|
+
return PERMISSIONS.IOS.PHOTO_LIBRARY;
|
|
42
|
+
case "location":
|
|
43
|
+
return PERMISSIONS.IOS.LOCATION_WHEN_IN_USE;
|
|
44
|
+
case "locationAlways":
|
|
45
|
+
return PERMISSIONS.IOS.LOCATION_ALWAYS;
|
|
46
|
+
case "contacts":
|
|
47
|
+
return PERMISSIONS.IOS.CONTACTS;
|
|
48
|
+
case "calendar":
|
|
49
|
+
return PERMISSIONS.IOS.CALENDARS;
|
|
50
|
+
case "nearbydevice":
|
|
51
|
+
return PERMISSIONS.IOS.BLUETOOTH;
|
|
52
|
+
case "notification":
|
|
53
|
+
// handled uniquely by react-native-permissions using requestNotifications
|
|
54
|
+
return null;
|
|
55
|
+
case "phone":
|
|
56
|
+
// iOS doesn't require explicit permission to use Linking.openURL('tel:...')
|
|
57
|
+
return null;
|
|
58
|
+
default:
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
} else if (Platform.OS === "android") {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "camera":
|
|
64
|
+
return PERMISSIONS.ANDROID.CAMERA;
|
|
65
|
+
case "audio":
|
|
66
|
+
return PERMISSIONS.ANDROID.RECORD_AUDIO;
|
|
67
|
+
case "image":
|
|
68
|
+
return PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
|
69
|
+
case "file":
|
|
70
|
+
return PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE;
|
|
71
|
+
case "location":
|
|
72
|
+
return PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
|
|
73
|
+
case "locationAlways":
|
|
74
|
+
return PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION;
|
|
75
|
+
case "contacts":
|
|
76
|
+
return PERMISSIONS.ANDROID.READ_CONTACTS;
|
|
77
|
+
case "calendar":
|
|
78
|
+
return PERMISSIONS.ANDROID.READ_CALENDAR;
|
|
79
|
+
case "nearbydevice":
|
|
80
|
+
// Required for Android 12+
|
|
81
|
+
return PERMISSIONS.ANDROID.BLUETOOTH_CONNECT;
|
|
82
|
+
case "phone":
|
|
83
|
+
return PERMISSIONS.ANDROID.CALL_PHONE;
|
|
84
|
+
case "notification":
|
|
85
|
+
// handled uniquely by checkNotifications/requestNotifications
|
|
86
|
+
return null;
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check the current status of a permission without asking the user
|
|
96
|
+
*/
|
|
97
|
+
async checkPermission(type: AppPermissionType): Promise<PermissionStatus> {
|
|
98
|
+
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;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nativePerm = this.getNativePermission(type);
|
|
106
|
+
if (!nativePerm) {
|
|
107
|
+
// If the permission doesn't exist on this platform (e.g. Phone on iOS), we consider it granted
|
|
108
|
+
return RESULTS.GRANTED;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return await check(nativePerm);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Error checking permission ${type}:`, error);
|
|
114
|
+
return RESULTS.UNAVAILABLE;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Request a permission from the user
|
|
120
|
+
*/
|
|
121
|
+
async requestPermission(type: AppPermissionType): Promise<PermissionStatus> {
|
|
122
|
+
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;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nativePerm = this.getNativePermission(type);
|
|
130
|
+
if (!nativePerm) {
|
|
131
|
+
return RESULTS.GRANTED;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return await request(nativePerm);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`Error requesting permission ${type}:`, error);
|
|
137
|
+
return RESULTS.UNAVAILABLE;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ensures a permission is granted. If not, requests it.
|
|
143
|
+
*/
|
|
144
|
+
async requirePermission(type: AppPermissionType): Promise<boolean> {
|
|
145
|
+
let status = await this.checkPermission(type);
|
|
146
|
+
|
|
147
|
+
if (status === RESULTS.DENIED) {
|
|
148
|
+
status = await this.requestPermission(type);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return status === RESULTS.GRANTED || status === RESULTS.LIMITED;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Helper to open the OS Settings screen for the app if permission is blocked
|
|
156
|
+
*/
|
|
157
|
+
async openSettingsForApp(
|
|
158
|
+
title = "Permission Required",
|
|
159
|
+
message = "Please enable this permission in your device settings."
|
|
160
|
+
) {
|
|
161
|
+
Alert.alert(title, message, [
|
|
162
|
+
{ text: "Cancel", style: "cancel" },
|
|
163
|
+
{ text: "Open Settings", onPress: () => openSettings() },
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Utility to register and fetch the Expo Push Token for this device.
|
|
168
|
+
* This uses the unified PermissionService to request the notification permission.
|
|
169
|
+
* NOTE: This will only return a token on physical devices.
|
|
170
|
+
*/
|
|
171
|
+
async registerForPushNotificationsAsync(): Promise<string | null> {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
173
|
+
const Device = require("expo-device");
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
175
|
+
const Notifications = require("expo-notifications");
|
|
176
|
+
|
|
177
|
+
if (!Device.isDevice) {
|
|
178
|
+
console.log("Must use physical device for Push Notifications");
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const hasPermission = await this.requirePermission("notification");
|
|
183
|
+
if (!hasPermission) {
|
|
184
|
+
console.log("User denied push notification permissions");
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Set channel for Android
|
|
189
|
+
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
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const projectId =
|
|
200
|
+
require("../../app.json")?.expo?.extra?.eas?.projectId;
|
|
201
|
+
|
|
202
|
+
const tokenData = await Notifications.getExpoPushTokenAsync({
|
|
203
|
+
projectId,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return tokenData.data;
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.error("Failed to get push token", e);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const permissionService = new PermissionService();
|