浏览代码

update to 1.2.5

Ian Fung 1 年之前
父节点
当前提交
3e9c6669a6

+ 6 - 4
app.json

@@ -2,7 +2,7 @@
     "expo": {
         "name": "Crazycharge",
         "slug": "template",
-        "version": "1.2.4",
+        "version": "1.2.5",
         "orientation": "portrait",
         "icon": "./assets/CC_Logo.png",
         "userInterfaceStyle": "light",
@@ -19,12 +19,14 @@
         },
         "android": {
             "adaptiveIcon": {
-                "foregroundImage": "./assets/CC_Logo.png",
-                "backgroundColor": "#ffffff"
+                "foregroundImage": "./assets/cc_android_logo.png",
+                "backgroundColor": "#000000"
             },
+            "icon": "./assets/CC_Logo.png",
+            "googleServicesFile": "./android/app/google-services.json",
             "permissions": ["android.permission.CAMERA", "android.permission.RECORD_AUDIO"],
             "package": "hk.com.crazycharge",
-            "versionCode": 19
+            "versionCode": 21
         },
         "web": {
             "favicon": "./assets/favicon.png"

+ 57 - 24
app/(auth)/(tabs)/(home)/scanQrPage.tsx

@@ -117,7 +117,9 @@ const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
 
 //reminder: scan qr code page, ic call should be false
 const ScanQrPage = () => {
-    const { userID, currentPrice } = useUserInfoStore();
+    const { userID, currentPrice, setCurrentPrice } = useUserInfoStore();
+    const [currentPriceFetchedWhenScanQr, setCurrentPriceFetchedWhenScanQr] = useState(0);
+
     const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
     const [permission, requestPermission] = useCameraPermissions();
     const [scanned, setScanned] = useState(false);
@@ -229,6 +231,21 @@ const ScanQrPage = () => {
         getWalletBalance();
     }, []);
 
+    const fetchCurrentPrice = async () => {
+        try {
+            const response = await chargeStationService.getCurrentPrice();
+            if (response) {
+                setCurrentPriceFetchedWhenScanQr(response);
+                setCurrentPrice(response);
+                return response;
+            }
+        } catch (error) {
+            console.error('Error fetching current price:', error);
+            Alert.alert('錯誤', '無法獲取當前價格,請稍後再試');
+            return null;
+        }
+    };
+
     const planMap = {
         // 3: { duration: 10, kWh: 3, displayDuration: 5, fee: 3 * currentPrice },
         25: { duration: 40, kWh: 20, displayDuration: 25, fee: 20 * currentPrice },
@@ -255,6 +272,12 @@ const ScanQrPage = () => {
             // console.log(`type: ${type}   data: ${data}  typeofData ${typeof data}`);
 
             try {
+                const price = await fetchCurrentPrice();
+                console.log('fetchedCurrentPrice in scanQrPage', price);
+                if (!price) {
+                    return; // Exit if price fetch failed
+                }
+
                 const response = await chargeStationService.getTodayReservation();
                 if (response) {
                     const now = new Date();
@@ -264,12 +287,12 @@ const ScanQrPage = () => {
 
                     // Check availability for each duration```````````````````````````````````````````````````````````````````
                     const availability = {
-                        // 3: checkAvailability(onlyThisConnector, now, 10) && walletBalance >= 3 * currentPrice,
-                        25: checkAvailability(onlyThisConnector, now, 40) && walletBalance >= 20 * currentPrice,
-                        30: checkAvailability(onlyThisConnector, now, 45) && walletBalance >= 25 * currentPrice,
-                        40: checkAvailability(onlyThisConnector, now, 55) && walletBalance >= 30 * currentPrice,
-                        45: checkAvailability(onlyThisConnector, now, 60) && walletBalance >= 40 * currentPrice,
-                        full: checkAvailability(onlyThisConnector, now, 120) && walletBalance >= 80 * currentPrice
+                        // 3: checkAvailability(onlyThisConnector, now, 10) && walletBalance >= 3 * price,
+                        25: checkAvailability(onlyThisConnector, now, 40) && walletBalance >= 20 * price,
+                        30: checkAvailability(onlyThisConnector, now, 45) && walletBalance >= 25 * price,
+                        40: checkAvailability(onlyThisConnector, now, 55) && walletBalance >= 30 * price,
+                        45: checkAvailability(onlyThisConnector, now, 60) && walletBalance >= 40 * price,
+                        full: checkAvailability(onlyThisConnector, now, 120) && walletBalance >= 80 * price
                     };
 
                     setAvailableSlots(availability);
@@ -287,7 +310,7 @@ const ScanQrPage = () => {
             }, 2000);
             return;
         }
-
+        // -----------------------------------------------------------------------------------------------------
         const { origin, size } = bounds;
 
         // Calculate the size of the square transparent area
@@ -308,6 +331,11 @@ const ScanQrPage = () => {
             // console.log(` type: ${type}   data: ${data}  typeofData ${typeof data}`);
 
             try {
+                const price = await fetchCurrentPrice();
+                console.log('fetchedCurrentPrice in scanQrPage', price);
+                if (!price) {
+                    return; // Exit if price fetch failed
+                }
                 const response = await chargeStationService.getTodayReservation();
                 if (response) {
                     const now = new Date();
@@ -318,12 +346,12 @@ const ScanQrPage = () => {
 
                     // Check availability for each duration
                     const availability = {
-                        // 3: checkAvailability(onlyThisConnector, now, 10) && walletBalance >= 3 * currentPrice,
-                        25: checkAvailability(onlyThisConnector, now, 40) && walletBalance >= 20 * currentPrice,
-                        30: checkAvailability(onlyThisConnector, now, 45) && walletBalance >= 25 * currentPrice,
-                        40: checkAvailability(onlyThisConnector, now, 55) && walletBalance >= 30 * currentPrice,
-                        45: checkAvailability(onlyThisConnector, now, 60) && walletBalance >= 40 * currentPrice,
-                        full: checkAvailability(onlyThisConnector, now, 120) && walletBalance >= 80 * currentPrice
+                        // 3: checkAvailability(onlyThisConnector, now, 10) && walletBalance >= 3 * price,
+                        25: checkAvailability(onlyThisConnector, now, 40) && walletBalance >= 20 * price,
+                        30: checkAvailability(onlyThisConnector, now, 45) && walletBalance >= 25 * price,
+                        40: checkAvailability(onlyThisConnector, now, 55) && walletBalance >= 30 * price,
+                        45: checkAvailability(onlyThisConnector, now, 60) && walletBalance >= 40 * price,
+                        full: checkAvailability(onlyThisConnector, now, 120) && walletBalance >= 80 * price
                     };
                     // console.log('availability', availability);
 
@@ -390,18 +418,29 @@ const ScanQrPage = () => {
             let fee;
             let totalPower;
 
+            //i create a planMap2 because i want to move the planMap inside this component but i dont wanna move the outside one because i dont wanna make any potential disruptive changes
+            const planMap2 = {
+                // 3: { duration: 10, kWh: 3, displayDuration: 5, fee: 3 * currentPriceFetchedWhenScanQr },
+                25: { duration: 40, kWh: 20, displayDuration: 25, fee: 20 * currentPriceFetchedWhenScanQr },
+                30: { duration: 45, kWh: 25, displayDuration: 30, fee: 25 * currentPriceFetchedWhenScanQr },
+                40: { duration: 55, kWh: 30, displayDuration: 40, fee: 30 * currentPriceFetchedWhenScanQr },
+                45: { duration: 60, kWh: 40, displayDuration: 45, fee: 40 * currentPriceFetchedWhenScanQr },
+                full: { duration: 120, displayDuration: '充滿停機', fee: 80 * currentPriceFetchedWhenScanQr }
+            };
+
             if (selectedDuration === 'full') {
                 endTime = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2 hours for "充滿停機"
-                fee = planMap.full.fee;
+                fee = planMap2.full.fee;
                 totalPower = 80; // Set to 130 for "充滿停機"
             } else {
                 const durationInMinutes = parseInt(selectedDuration);
                 endTime = new Date(now.getTime() + durationInMinutes * 60 * 1000);
                 // console.log('endTime', endTime);
-                fee = planMap[selectedDuration].fee;
-                totalPower = planMap[selectedDuration].kWh;
+                fee = planMap2[selectedDuration].fee;
+                totalPower = planMap2[selectedDuration].kWh;
             }
             setTotalFee(fee);
+            console.log('fee in scanQrPage-- this is the total_fee i send to backend', fee);
             const dataForSubmission = {
                 stationID: '2405311022116801000',
                 connector: scannedResult,
@@ -734,9 +773,7 @@ const ScanQrPage = () => {
                 return;
             }
 
-            ////////
             const wallet = await walletService.getWalletBalance();
-            // console.log('wallet in startCharging in scanQrPage', wallet);
 
             if (wallet < dataForSubmission.total_fee) {
                 oneTimeCharging(dataForSubmission.total_fee);
@@ -760,9 +797,8 @@ const ScanQrPage = () => {
             );
             if (response.status === 200 || response.status === 201) {
                 setSelectedDuration(null);
-                // console.log('Charging started from startCharging', response);
+
                 setIsConfirmLoading(false);
-                // Set a flag in AsyncStorage to indicate charging has started
 
                 await AsyncStorage.setItem('chargingStarted', 'true');
 
@@ -784,9 +820,6 @@ const ScanQrPage = () => {
                         }
                     }
                 ]);
-
-                // Start the navigation attempt loop
-                // startNavigationAttempts();
             } else if (response.status === 400) {
                 console.log('400 error in paymentSummaryPageComponent');
 

+ 4 - 18
app/(auth)/(tabs)/_layout.tsx

@@ -1,11 +1,7 @@
 import React, { useEffect, useState } from 'react';
 import { Tabs, useSegments } from 'expo-router';
 import { Dimensions, Platform } from 'react-native';
-import {
-    TabAccountIcon,
-    TabChargingIcon,
-    TabHomeIcon
-} from '../../../component/global/SVG';
+import { TabAccountIcon, TabChargingIcon, TabHomeIcon } from '../../../component/global/SVG';
 
 export default function TabLayout() {
     const { height } = Dimensions.get('window');
@@ -50,9 +46,7 @@ export default function TabLayout() {
                 options={{
                     title: '主頁',
                     // tabBarHideOnKeyboard: true,
-                    tabBarIcon: ({ focused }) => (
-                        <TabHomeIcon color={focused ? '#02677D' : '#BBBBBB'} />
-                    )
+                    tabBarIcon: ({ focused }) => <TabHomeIcon color={focused ? '#02677D' : '#BBBBBB'} />
                 }}
             />
             <Tabs.Screen
@@ -60,11 +54,7 @@ export default function TabLayout() {
                 options={{
                     title: '充電',
                     tabBarHideOnKeyboard: true,
-                    tabBarIcon: ({ focused }) => (
-                        <TabChargingIcon
-                            color={focused ? '#02677D' : '#BBBBBB'}
-                        />
-                    )
+                    tabBarIcon: ({ focused }) => <TabChargingIcon color={focused ? '#02677D' : '#BBBBBB'} />
                 }}
             />
             <Tabs.Screen
@@ -72,11 +62,7 @@ export default function TabLayout() {
                 options={{
                     title: '帳戶',
                     tabBarHideOnKeyboard: true,
-                    tabBarIcon: ({ focused, color }) => (
-                        <TabAccountIcon
-                            color={focused ? '#02677D' : '#BBBBBB'}
-                        />
-                    )
+                    tabBarIcon: ({ focused, color }) => <TabAccountIcon color={focused ? '#02677D' : '#BBBBBB'} />
                 }}
             />
         </Tabs>

+ 6 - 2
app/_layout.tsx

@@ -7,20 +7,24 @@ import { useEffect, useState } from 'react';
 import { ActivityIndicator, View } from 'react-native';
 import { checkVersion } from '../component/checkVersion';
 import { authenticationService } from '../service/authService';
+import { usePushNotifications } from './hooks/usePushNotifications';
 
 export default function RootLayout() {
     const [isLoading, setIsLoading] = useState(true);
     const { user } = useAuth();
+    const { expoPushToken, notification } = usePushNotifications();
+
+    const data = JSON.stringify(notification, undefined, 2);
 
     useEffect(() => {
         const fetchVersion = async () => {
             const response = await authenticationService.getVersion();
-            console.log('response', response);
+
             checkVersion(response);
         };
-
         fetchVersion();
     }, []);
+
     return (
         <GestureHandlerRootView style={{ flex: 1 }}>
             <AuthProvider>

+ 88 - 0
app/hooks/usePushNotifications.ts

@@ -0,0 +1,88 @@
+import { useEffect, useState, useRef } from 'react';
+import * as Notifications from 'expo-notifications';
+import * as Device from 'expo-device';
+import Constants from 'expo-constants';
+import { Platform } from 'react-native';
+
+export interface PushNotificationState {
+    notification?: Notifications.Notification;
+    expoPushToken?: Notifications.ExpoPushToken;
+}
+
+export const usePushNotifications = (): PushNotificationState => {
+    const [expoPushToken, setExpoPushToken] = useState<Notifications.ExpoPushToken | undefined>();
+    const [notification, setNotification] = useState<Notifications.Notification | undefined>();
+
+    const notificationListener = useRef<Notifications.Subscription>();
+    const responseListener = useRef<Notifications.Subscription>();
+
+    useEffect(() => {
+        Notifications.setNotificationHandler({
+            handleNotification: async () => ({
+                shouldPlaySound: true,
+                shouldShowAlert: true,
+                shouldSetBadge: true,
+                icon: './assets/images/cc.png',
+                vibrate: true,
+                shouldShowWhenInForeground: true
+            })
+        });
+
+        registerForPushNotificationAsync().then((token) => {
+            setExpoPushToken(token);
+        });
+
+        notificationListener.current = Notifications.addNotificationReceivedListener((notification) => {
+            setNotification(notification);
+        });
+
+        responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
+            console.log(response);
+        });
+
+        return () => {
+            Notifications.removeNotificationSubscription(notificationListener.current!);
+            Notifications.removeNotificationSubscription(responseListener.current!);
+        };
+    }, []);
+
+    return {
+        expoPushToken,
+        notification
+    };
+};
+
+async function registerForPushNotificationAsync() {
+    let token;
+
+    // Check if the device is a physical device, because this only works for physical devices not simulators
+    if (Device.isDevice) {
+        const { status: existingStatus } = await Notifications.getPermissionsAsync();
+        let finalStatus = existingStatus;
+        if (existingStatus !== 'granted') {
+            const { status } = await Notifications.requestPermissionsAsync();
+            finalStatus = status;
+        }
+        if (finalStatus !== 'granted') {
+            alert('Failed to get push token for push notification!');
+            return;
+        }
+        //if we have permission, then get the token
+        token = await Notifications.getExpoPushTokenAsync({
+            projectId: Constants.expoConfig?.extra?.eas?.projectId
+        });
+
+        if (Platform.OS === 'android') {
+            Notifications.setNotificationChannelAsync('default', {
+                name: 'default',
+                importance: Notifications.AndroidImportance.MAX,
+                vibrationPattern: [0, 250, 250, 250],
+                lightColor: '#FF231F7C'
+            });
+        }
+        console.log('token', token);
+        return token;
+    } else {
+        console.log('Must use physical device for Push Notifications');
+    }
+}

二进制
assets/cc_android_logo.png


+ 16 - 3
component/accountPages/paymentRecordPageComponent.tsx

@@ -21,7 +21,8 @@ const TransactionRow: React.FC<TransactionRecordItem> = ({ date, description, am
         <Text className="flex-[0.25] text-sm text-right">
             {actual_total_power !== '-' ? Number(actual_total_power).toFixed(1) : actual_total_power}
         </Text>
-        <Text className="flex-[0.25] text-sm text-right">${Number.isInteger(amount) ? amount : amount.toFixed(1)}</Text>
+        {/* <Text className="flex-[0.25] text-sm text-right">${Number.isInteger(amount) ? amount : amount.toFixed(1)}</Text> */}
+        <Text className="flex-[0.25] text-sm text-right">${amount}</Text>
     </View>
 );
 const PaymentRecordPageComponent = () => {
@@ -90,6 +91,12 @@ const PaymentRecordPageComponent = () => {
                 const response = await walletService.getTransactionRecord();
                 const formattedData: TransactionRecordItem[] = response
                     .sort((a: any, b: any) => new Date(b.createdAt) - new Date(a.createdAt))
+                    .filter(
+                        (item: any) =>
+                            item.type !== 'wallet' ||
+                            item.goods_name === 'Penalty' ||
+                            (item.goods_name === 'Walk In' && Number(item.actual_total_power) >= 1) // Keep Walk In items only if power >= 1
+                    )
                     .map((item: any) => {
                         let description;
                         if (item.type === 'wallet') {
@@ -109,13 +116,19 @@ const PaymentRecordPageComponent = () => {
                                 default:
                                     description = '充電';
                             }
-                        } else {
+                        } else if (item.type === 'qfpay') {
                             description = '錢包增值';
                         }
                         return {
                             date: convertToHKTime(item.createdAt).hkDate,
                             description: description,
-                            amount: item.amount,
+                            // amount: item.amount,
+                            amount:
+                                item.type === 'qfpay'
+                                    ? item.amount
+                                    : item.current_price && item.actual_total_power
+                                    ? (item.current_price * item.actual_total_power).toFixed(1)
+                                    : '-',
                             actual_total_power:
                                 item.actual_total_power !== undefined &&
                                 item.actual_total_power !== null &&

+ 15 - 14
component/chargingPage/chargingPageComponent.tsx

@@ -21,7 +21,7 @@ const ChargingPageComponent = ({ data }) => {
     const voltageA = onGoingChargingData?.data?.VoltageA;
     const currentA = onGoingChargingData?.data?.CurrentA;
     const { user } = useContext(AuthContext);
-    console.log('voltageA and currentA', voltageA, currentA);
+    // console.log('voltageA and currentA', voltageA, currentA);
     useEffect(() => {
         const fetchOngoingChargingData = async () => {
             setIsLoading(true);
@@ -29,8 +29,9 @@ const ChargingPageComponent = ({ data }) => {
                 const response = await chargeStationService.fetchOngoingChargingData(reservationData.format_order_id);
                 if (response) {
                     setOnGoingChargingData(response);
-                    console.log('i am ongoingchargingdata', response);
-                    console.log('onGoingData current and voltage', response.CurrentA, response.VoltageA);
+                    // console.log('i am ongoingchargingdata', response);
+
+                    // console.log('onGoingData current and voltage', response.CurrentA, response.VoltageA);
                 } else {
                     console.log('error fetching data');
                 }
@@ -72,15 +73,15 @@ const ChargingPageComponent = ({ data }) => {
     useEffect(() => {
         const now = new Date();
         const endTime = reservationData?.end_time ? new Date(reservationData.end_time) : null;
-        console.log('now in chargingPageComponent', now);
-        console.log('endTime in chargingPageComponent', endTime);
-        console.log('Checking stop conditions:', { now, endTime, Soc: reservationData?.Soc });
+        // console.log('now in chargingPageComponent', now);
+        // console.log('endTime in chargingPageComponent', endTime);
+        // console.log('Checking stop conditions:', { now, endTime, Soc: reservationData?.Soc });
 
         // Access the snapshot properties
         const snapshotData = reservationData?.snapshot ? JSON.parse(reservationData.snapshot) : null;
-        console.log('snapshotData in onGoingChargingData', snapshotData);
-        console.log('snapshot.is_ic_call', snapshotData?.is_ic_call);
-        console.log('snapshot.type', snapshotData?.type);
+        // console.log('snapshotData in onGoingChargingData', snapshotData);
+        // console.log('snapshot.is_ic_call', snapshotData?.is_ic_call);
+        // console.log('snapshot.type', snapshotData?.type);
 
         if (reservationData && snapshotData) {
             const isWalkingOrIcCall = snapshotData.type === 'walking' || snapshotData.is_ic_call === true;
@@ -118,15 +119,15 @@ const ChargingPageComponent = ({ data }) => {
 
     function timeSinceBooking(timeString) {
         if (timeString) {
-            console.log('timeString in timeSinceBooking', timeString);
+            // console.log('timeString in timeSinceBooking', timeString);
             const startTime = new Date(timeString);
             const now = new Date();
-            console.log('now in timeSinceBooking', now);
-            console.log('startTime in timeSinceBooking', startTime);
-            console.log('now - startTime', now - startTime);
+            // console.log('now in timeSinceBooking', now);
+            // console.log('startTime in timeSinceBooking', startTime);
+            // console.log('now - startTime', now - startTime);
             const diffInMilliseconds = now - startTime;
             const diffInMinutes = Math.floor(diffInMilliseconds / (1000 * 60));
-            console.log('diffInMinutes in timeSinceBooking', diffInMinutes);
+            // console.log('diffInMinutes in timeSinceBooking', diffInMinutes);
             if (diffInMinutes < 1) {
                 return '< 1 minute';
             } else {

+ 15 - 4
component/chargingPage/penaltyPaymentPageComponent.tsx

@@ -24,7 +24,7 @@ const PenaltyPaymentPageComponent = () => {
         const bookingDate = new Date(isoDateString);
 
         // Adjust to local time (+8 hours)
-        bookingDate.setHours(bookingDate.getHours() + 8);
+        bookingDate.setHours(bookingDate.getHours());
 
         // Format date as "MM-DD"
         const date = `${(bookingDate.getMonth() + 1).toString().padStart(2, '0')}-${bookingDate
@@ -41,9 +41,20 @@ const PenaltyPaymentPageComponent = () => {
         return { date, time };
     };
 
+    const calculateUserEndTime = (actualEndTimeStr: string, penaltyFee: string): string => {
+        const actualEndTime = new Date(actualEndTimeStr);
+        const penaltyMinutes = Math.floor(parseFloat(penaltyFee) / 3); // $3 per minute
+        const userEndTime = new Date(actualEndTime.getTime() + penaltyMinutes * 60000); // add minutes
+
+        return userEndTime.toISOString();
+    };
+
     const { date, time } = convertBookingDateTime(params.book_time);
     const { date: end_date, time: end_time } = convertBookingDateTime(params.end_time as string);
     const { date: actual_end_date, time: actual_end_time } = convertBookingDateTime(params.actual_end_time as string);
+    const { time: user_end_time } = convertBookingDateTime(
+        calculateUserEndTime(params.actual_end_time as string, params.penalty_fee as string)
+    );
 
     const payload = {
         userId: userID,
@@ -79,10 +90,10 @@ const PenaltyPaymentPageComponent = () => {
                         <View className="flex-1 flex-row items-center pb-3">
                             <View className="flex-1 flex-column">
                                 <Text style={styles.grayColor} className="text-base">
-                                    充電到期時間
+                                    實際充電到期時間
                                 </Text>
                                 <Text style={styles.greenColor} className="text-4xl text-center  pt-2">
-                                    {end_time}
+                                    {actual_end_time}
                                 </Text>
                             </View>
                             <View className="flex-1 flex-column">
@@ -90,7 +101,7 @@ const PenaltyPaymentPageComponent = () => {
                                     實際充電結束時間
                                 </Text>
                                 <Text style={styles.greenColor} className="text-4xl text-center pt-2">
-                                    {actual_end_time}
+                                    {user_end_time}
                                 </Text>
                             </View>
                         </View>

+ 1 - 1
component/searchPage/searchPageComponent.tsx

@@ -135,7 +135,7 @@ const SearchPageComponent: React.FC<SearchPageComponentProps> = () => {
                             <NormalButton
                                 title={<Text style={{ color: '#061E25' }}>瀏覽地圖查看現有充電站</Text>}
                                 // onPress={() => console.log('附近的充電站')}
-                                onPress={() => router.push('/searchResultPage')}
+                                onPress={() => router.push('/(auth)/(tabs)/(home)/(booking)/searchResultPage')}
                                 buttonPressedStyle={{
                                     backgroundColor: '#CFDEE4'
                                 }}

+ 149 - 0
package-lock.json

@@ -23,11 +23,13 @@
         "expo-camera": "~15.0.13",
         "expo-checkbox": "~3.0.0",
         "expo-constants": "~16.0.2",
+        "expo-device": "~6.0.2",
         "expo-env": "^1.1.1",
         "expo-file-system": "~17.0.1",
         "expo-image-picker": "~15.0.7",
         "expo-linking": "~6.3.1",
         "expo-location": "~17.0.1",
+        "expo-notifications": "~0.28.19",
         "expo-router": "~3.5.14",
         "expo-secure-store": "~13.0.1",
         "expo-status-bar": "~1.12.1",
@@ -3376,6 +3378,11 @@
         "@hapi/hoek": "^9.0.0"
       }
     },
+    "node_modules/@ide/backoff": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
+      "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g=="
+    },
     "node_modules/@isaacs/ttlcache": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
@@ -6715,6 +6722,18 @@
       "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
       "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
     },
+    "node_modules/assert": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+      "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "is-nan": "^1.3.2",
+        "object-is": "^1.1.5",
+        "object.assign": "^4.1.4",
+        "util": "^0.12.5"
+      }
+    },
     "node_modules/ast-types": {
       "version": "0.15.2",
       "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
@@ -6897,6 +6916,11 @@
         "react-refresh": "^0.14.2"
       }
     },
+    "node_modules/badgin": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
+      "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw=="
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -8546,6 +8570,14 @@
         "expo": "bin/cli"
       }
     },
+    "node_modules/expo-application": {
+      "version": "5.9.1",
+      "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-5.9.1.tgz",
+      "integrity": "sha512-uAfLBNZNahnDZLRU41ZFmNSKtetHUT9Ua557/q189ua0AWV7pQjoVAx49E4953feuvqc9swtU3ScZ/hN1XO/FQ==",
+      "peerDependencies": {
+        "expo": "*"
+      }
+    },
     "node_modules/expo-asset": {
       "version": "10.0.6",
       "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-10.0.6.tgz",
@@ -8588,6 +8620,42 @@
         "expo": "*"
       }
     },
+    "node_modules/expo-device": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-6.0.2.tgz",
+      "integrity": "sha512-sCt91CuTmAuMXX4SlFOn4lIos2UIr8vb0jDstDDZXys6kErcj0uynC7bQAMreU5uRUTKMAl4MAMpKt9ufCXPBw==",
+      "dependencies": {
+        "ua-parser-js": "^0.7.33"
+      },
+      "peerDependencies": {
+        "expo": "*"
+      }
+    },
+    "node_modules/expo-device/node_modules/ua-parser-js": {
+      "version": "0.7.39",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
+      "integrity": "sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/ua-parser-js"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/faisalman"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/faisalman"
+        }
+      ],
+      "bin": {
+        "ua-parser-js": "script/cli.js"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/expo-env": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/expo-env/-/expo-env-1.1.1.tgz",
@@ -8782,6 +8850,57 @@
         "invariant": "^2.2.4"
       }
     },
+    "node_modules/expo-notifications": {
+      "version": "0.28.19",
+      "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.19.tgz",
+      "integrity": "sha512-rKKTnVQQ9XNQyTNwKmI9OlchhVu0XOZfRpImMqPFCJg6IwECM1izdas2SLCbE/GApg2Tw3U5R2fd26OnCtUU/w==",
+      "dependencies": {
+        "@expo/image-utils": "^0.5.0",
+        "@ide/backoff": "^1.0.0",
+        "abort-controller": "^3.0.0",
+        "assert": "^2.0.0",
+        "badgin": "^1.1.5",
+        "expo-application": "~5.9.0",
+        "expo-constants": "~16.0.0",
+        "fs-extra": "^9.1.0"
+      },
+      "peerDependencies": {
+        "expo": "*"
+      }
+    },
+    "node_modules/expo-notifications/node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/expo-notifications/node_modules/jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/expo-notifications/node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
     "node_modules/expo-router": {
       "version": "3.5.14",
       "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-3.5.14.tgz",
@@ -9925,6 +10044,21 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-nan": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+      "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+      "dependencies": {
+        "call-bind": "^1.0.0",
+        "define-properties": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-negative-zero": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -12194,6 +12328,21 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/object-is": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",

+ 5 - 3
package.json

@@ -4,8 +4,8 @@
   "main": "expo-router/entry",
   "scripts": {
     "start": "expo start -c",
-    "android": "expo start --android",
-    "ios": "expo start --ios",
+    "android": "expo run:android",
+    "ios": "expo run:ios",
     "web": "expo start --web"
   },
   "dependencies": {
@@ -51,7 +51,9 @@
     "react-native-tab-view": "^3.5.2",
     "react-native-webview": "^13.11.1",
     "react-query": "^3.39.3",
-    "zustand": "^4.5.2"
+    "zustand": "^4.5.2",
+    "expo-notifications": "~0.28.19",
+    "expo-device": "~6.0.2"
   },
   "devDependencies": {
     "@babel/core": "^7.20.0",