Browse Source

Merge branch 'notifications-test' of Viktoriia/nomadmania-app into dev

Viktoriia 10 months ago
parent
commit
5cd7ea51b1

+ 1 - 1
App.tsx

@@ -24,7 +24,7 @@ Sentry.init({
   dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
   dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
   integrations: [new Sentry.ReactNativeTracing({ routingInstrumentation })],
   integrations: [new Sentry.ReactNativeTracing({ routingInstrumentation })],
   debug: false,
   debug: false,
-  ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded'],
+  ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded']
 });
 });
 
 
 const App = () => {
 const App = () => {

+ 53 - 40
Route.tsx

@@ -84,6 +84,7 @@ import { userApi } from '@api/user';
 import axios from 'axios';
 import axios from 'axios';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { useNotification } from 'src/contexts/NotificationContext';
 import PreviewScreen from 'src/screens/InAppScreens/ProfileScreen/ShareScreen';
 import PreviewScreen from 'src/screens/InAppScreens/ProfileScreen/ShareScreen';
+import { PushNotificationProvider } from 'src/contexts/PushNotificationContext';
 
 
 enableScreens();
 enableScreens();
 
 
@@ -152,10 +153,10 @@ const Route = () => {
 
 
   useEffect(() => {
   useEffect(() => {
     const prepareApp = async () => {
     const prepareApp = async () => {
-      // checkTokenAndUpdate();
       await checkNmToken();
       await checkNmToken();
       await findFastestServer();
       await findFastestServer();
       await openDatabases();
       await openDatabases();
+      await checkTokenAndUpdate();
       setDbLoaded(true);
       setDbLoaded(true);
     };
     };
 
 
@@ -185,6 +186,10 @@ const Route = () => {
 
 
   const checkTokenAndUpdate = async () => {
   const checkTokenAndUpdate = async () => {
     const storedToken = storage.get('deviceToken', StoreType.STRING);
     const storedToken = storage.get('deviceToken', StoreType.STRING);
+    const { status } = await Notifications.getPermissionsAsync();
+    if (status !== 'granted') {
+      return;
+    }
     const currentToken = await Notifications.getDevicePushTokenAsync();
     const currentToken = await Notifications.getDevicePushTokenAsync();
 
 
     if (storedToken && currentToken?.data !== storedToken) {
     if (storedToken && currentToken?.data !== storedToken) {
@@ -406,45 +411,53 @@ const Route = () => {
   );
   );
 
 
   return (
   return (
-    <ScreenStack.Navigator
-      screenOptions={{ headerShown: false, cardStyle: { backgroundColor: 'white' } }}
-      initialRouteName={token ? NAVIGATION_PAGES.IN_APP : NAVIGATION_PAGES.WELCOME}
-    >
-      <ScreenStack.Screen name={NAVIGATION_PAGES.WELCOME} component={WelcomeScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.LOGIN} component={LoginScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER} component={JoinUsScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER_ACCOUNT_DATA} component={EditAccount} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.RESET_PASSWORD} component={ResetPasswordScreen} />
-      <ScreenStack.Screen
-        name={NAVIGATION_PAGES.RESET_PASSWORD_DEEP}
-        component={ResetPasswordDeepScreen}
-      />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.INFO} component={InfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.JOIN_INFO} component={JoinInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.DISCOVER_INFO} component={DiscoverInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.PLAN_INFO} component={PlanInfoScreen} />
-      <ScreenStack.Screen
-        name={NAVIGATION_PAGES.FIRST_STEPS_INFO}
-        component={FirstStepsInfoScreen}
-      />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.COUNTRIES_INFO} component={CountriesInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS_INFO} component={TripsInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS_INFO} component={FixersInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.EARTH_INFO} component={EarthInfoScreen} />
-      <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
-        {() => (
-          <MapDrawer.Navigator drawerContent={(props) => <MenuDrawer {...props} />}>
-            <MapDrawer.Screen
-              name="DrawerApp"
-              component={BottomTabNavigator}
-              options={{ headerShown: false }}
-            />
-          </MapDrawer.Navigator>
-        )}
-      </ScreenStack.Screen>
-    </ScreenStack.Navigator>
+    <PushNotificationProvider>
+      <ScreenStack.Navigator
+        screenOptions={{ headerShown: false, cardStyle: { backgroundColor: 'white' } }}
+        initialRouteName={token ? NAVIGATION_PAGES.IN_APP : NAVIGATION_PAGES.WELCOME}
+      >
+        <ScreenStack.Screen name={NAVIGATION_PAGES.WELCOME} component={WelcomeScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.LOGIN} component={LoginScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER} component={JoinUsScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER_ACCOUNT_DATA} component={EditAccount} />
+        <ScreenStack.Screen
+          name={NAVIGATION_PAGES.RESET_PASSWORD}
+          component={ResetPasswordScreen}
+        />
+        <ScreenStack.Screen
+          name={NAVIGATION_PAGES.RESET_PASSWORD_DEEP}
+          component={ResetPasswordDeepScreen}
+        />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.INFO} component={InfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.JOIN_INFO} component={JoinInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.DISCOVER_INFO} component={DiscoverInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.PLAN_INFO} component={PlanInfoScreen} />
+        <ScreenStack.Screen
+          name={NAVIGATION_PAGES.FIRST_STEPS_INFO}
+          component={FirstStepsInfoScreen}
+        />
+        <ScreenStack.Screen
+          name={NAVIGATION_PAGES.COUNTRIES_INFO}
+          component={CountriesInfoScreen}
+        />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS_INFO} component={TripsInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS_INFO} component={FixersInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.EARTH_INFO} component={EarthInfoScreen} />
+        <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
+          {() => (
+            <MapDrawer.Navigator drawerContent={(props) => <MenuDrawer {...props} />}>
+              <MapDrawer.Screen
+                name="DrawerApp"
+                component={BottomTabNavigator}
+                options={{ headerShown: false }}
+              />
+            </MapDrawer.Navigator>
+          )}
+        </ScreenStack.Screen>
+      </ScreenStack.Navigator>
+    </PushNotificationProvider>
   );
   );
 };
 };
 
 

+ 19 - 7
app.config.ts

@@ -1,12 +1,14 @@
 import 'dotenv/config';
 import 'dotenv/config';
-import { env } from 'process';
 import path from 'path';
 import path from 'path';
 import dotenv from 'dotenv';
 import dotenv from 'dotenv';
 
 
 import type { ConfigContext, ExpoConfig } from 'expo/config';
 import type { ConfigContext, ExpoConfig } from 'expo/config';
 
 
+const env = process.env;
+
 const API_HOST = env.ENV === 'production' ? env.PRODUCTION_API_HOST : env.DEVELOPMENT_API_HOST;
 const API_HOST = env.ENV === 'production' ? env.PRODUCTION_API_HOST : env.DEVELOPMENT_API_HOST;
 const MAP_HOST = env.ENV === 'production' ? env.PRODUCTION_MAP_HOST : env.DEVELOPMENT_MAP_HOST;
 const MAP_HOST = env.ENV === 'production' ? env.PRODUCTION_MAP_HOST : env.DEVELOPMENT_MAP_HOST;
+
 const GOOGLE_MAP_PLACES_APIKEY = env.GOOGLE_MAP_PLACES_APIKEY;
 const GOOGLE_MAP_PLACES_APIKEY = env.GOOGLE_MAP_PLACES_APIKEY;
 
 
 dotenv.config({
 dotenv.config({
@@ -67,6 +69,14 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         'Enable NomadMania.com to access your photo library to upload your profile picture. Any violence, excess of nudity, stolen picture, or scam is forbidden',
         'Enable NomadMania.com to access your photo library to upload your profile picture. Any violence, excess of nudity, stolen picture, or scam is forbidden',
       NSPushNotificationsDescription:
       NSPushNotificationsDescription:
         'This will allow NomadMania.com to send you notifications. Also you can disable it in app settings'
         'This will allow NomadMania.com to send you notifications. Also you can disable it in app settings'
+    },
+    privacyManifests: {
+      NSPrivacyAccessedAPITypes: [
+        {
+          NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryUserDefaults',
+          NSPrivacyAccessedAPITypeReasons: ['CA92.1']
+        }
+      ]
     }
     }
   },
   },
   android: {
   android: {
@@ -74,8 +84,9 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     config: {
     config: {
       googleMaps: {
       googleMaps: {
         apiKey: env.ANDROID_GOOGLE_MAP_APIKEY
         apiKey: env.ANDROID_GOOGLE_MAP_APIKEY
-      },
+      }
     },
     },
+    googleServicesFile: './google-services.json',
     permissions: [
     permissions: [
       // 'ACCESS_BACKGROUND_LOCATION',
       // 'ACCESS_BACKGROUND_LOCATION',
       'ACCESS_FINE_LOCATION',
       'ACCESS_FINE_LOCATION',
@@ -98,22 +109,23 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       }
       }
     ],
     ],
     [
     [
-      "expo-build-properties",
+      'expo-build-properties',
       {
       {
         android: {
         android: {
           minSdkVersion: 24,
           minSdkVersion: 24,
-          targetSdkVersion: 34,
+          targetSdkVersion: 34
           // kotlinVersion: '1.7.1'
           // kotlinVersion: '1.7.1'
         }
         }
       }
       }
     ],
     ],
     [
     [
-      "@sentry/react-native/expo",
+      '@sentry/react-native/expo',
       {
       {
         organization: env.SENTRY_ORG,
         organization: env.SENTRY_ORG,
         project: env.SENTRY_PROJECT,
         project: env.SENTRY_PROJECT,
-        url: "https://sentry.io/"
+        url: 'https://sentry.io/'
       }
       }
-    ]
+    ],
+    ['expo-asset', 'expo-font']
   ]
   ]
 });
 });

+ 30 - 30
package.json

@@ -12,63 +12,63 @@
     "postinstall": "patch-package"
     "postinstall": "patch-package"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@react-native-community/datetimepicker": "7.2.0",
-    "@react-native-community/netinfo": "9.3.10",
+    "@react-native-community/datetimepicker": "8.0.1",
+    "@react-native-community/netinfo": "11.3.1",
     "@react-navigation/bottom-tabs": "^6.5.11",
     "@react-navigation/bottom-tabs": "^6.5.11",
     "@react-navigation/drawer": "^6.6.15",
     "@react-navigation/drawer": "^6.6.15",
     "@react-navigation/material-top-tabs": "^6.6.5",
     "@react-navigation/material-top-tabs": "^6.6.5",
     "@react-navigation/native": "^6.1.9",
     "@react-navigation/native": "^6.1.9",
     "@react-navigation/native-stack": "^6.9.17",
     "@react-navigation/native-stack": "^6.9.17",
     "@react-navigation/stack": "^6.3.20",
     "@react-navigation/stack": "^6.3.20",
-    "@sentry/react-native": "^5.29.0",
-    "@shopify/flash-list": "1.4.3",
+    "@sentry/react-native": "~5.22.0",
+    "@shopify/flash-list": "1.6.4",
     "@tanstack/react-query": "latest",
     "@tanstack/react-query": "latest",
     "@turf/turf": "^6.5.0",
     "@turf/turf": "^6.5.0",
     "axios": "^1.6.1",
     "axios": "^1.6.1",
     "better-react-native-image-viewing": "^0.2.7",
     "better-react-native-image-viewing": "^0.2.7",
     "dotenv": "^16.3.1",
     "dotenv": "^16.3.1",
-    "expo": "~49.0.15",
-    "expo-asset": "8.10.1",
-    "expo-build-properties": "~0.8.3",
-    "expo-checkbox": "~2.4.0",
-    "expo-constants": "14.4.2",
-    "expo-dev-client": "~2.4.12",
-    "expo-file-system": "15.4.5",
-    "expo-font": "11.4.0",
-    "expo-image": "~1.3.5",
-    "expo-image-picker": "~14.3.2",
-    "expo-location": "~16.1.0",
-    "expo-notifications": "~0.20.1",
-    "expo-splash-screen": "~0.20.5",
-    "expo-sqlite": "~11.3.3",
-    "expo-updates": "~0.18.19",
+    "expo": "^51.0.9",
+    "expo-asset": "~10.0.10",
+    "expo-build-properties": "~0.12.5",
+    "expo-checkbox": "~3.0.0",
+    "expo-constants": "~16.0.2",
+    "expo-dev-client": "~4.0.26",
+    "expo-file-system": "~17.0.1",
+    "expo-font": "~12.0.10",
+    "expo-image": "~1.12.15",
+    "expo-image-picker": "~15.0.7",
+    "expo-location": "~17.0.1",
+    "expo-notifications": "~0.28.16",
+    "expo-splash-screen": "~0.27.5",
+    "expo-sqlite": "~14.0.6",
+    "expo-updates": "~0.25.24",
     "formik": "^2.4.5",
     "formik": "^2.4.5",
     "moment": "^2.29.4",
     "moment": "^2.29.4",
     "patch-package": "^8.0.0",
     "patch-package": "^8.0.0",
     "promise": "^8.3.0",
     "promise": "^8.3.0",
     "react": "18.2.0",
     "react": "18.2.0",
-    "react-native": "0.72.10",
+    "react-native": "0.74.5",
     "react-native-animated-pagination-dot": "^0.4.0",
     "react-native-animated-pagination-dot": "^0.4.0",
     "react-native-calendars": "^1.1304.1",
     "react-native-calendars": "^1.1304.1",
     "react-native-device-detection": "^0.2.1",
     "react-native-device-detection": "^0.2.1",
-    "react-native-gesture-handler": "~2.12.0",
+    "react-native-gesture-handler": "~2.16.1",
     "react-native-google-places-autocomplete": "^2.5.6",
     "react-native-google-places-autocomplete": "^2.5.6",
     "react-native-image-viewing": "^0.2.2",
     "react-native-image-viewing": "^0.2.2",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "react-native-map-clustering": "^3.4.2",
     "react-native-map-clustering": "^3.4.2",
-    "react-native-maps": "1.7.1",
+    "react-native-maps": "1.14.0",
     "react-native-mmkv": "^2.11.0",
     "react-native-mmkv": "^2.11.0",
     "react-native-modal": "^13.0.1",
     "react-native-modal": "^13.0.1",
-    "react-native-pager-view": "6.2.0",
+    "react-native-pager-view": "6.3.0",
     "react-native-paper": "^5.12.3",
     "react-native-paper": "^5.12.3",
     "react-native-progress": "^5.0.1",
     "react-native-progress": "^5.0.1",
-    "react-native-reanimated": "~3.3.0",
+    "react-native-reanimated": "~3.10.1",
     "react-native-reanimated-carousel": "^3.5.1",
     "react-native-reanimated-carousel": "^3.5.1",
-    "react-native-safe-area-context": "4.6.3",
-    "react-native-screens": "~3.22.0",
+    "react-native-safe-area-context": "4.10.5",
+    "react-native-screens": "3.31.1",
     "react-native-searchable-dropdown-kj": "^1.9.1",
     "react-native-searchable-dropdown-kj": "^1.9.1",
     "react-native-share": "^10.2.1",
     "react-native-share": "^10.2.1",
-    "react-native-svg": "13.9.0",
+    "react-native-svg": "15.2.0",
     "react-native-tab-view": "^3.5.2",
     "react-native-tab-view": "^3.5.2",
     "react-native-view-shot": "^3.7.0",
     "react-native-view-shot": "^3.7.0",
     "react-native-walkthrough-tooltip": "^1.6.0",
     "react-native-walkthrough-tooltip": "^1.6.0",
@@ -76,11 +76,11 @@
     "zustand": "^4.4.7"
     "zustand": "^4.4.7"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@babel/core": "^7.20.0",
+    "@babel/core": "^7.25.2",
     "@types/react": "~18.2.14",
     "@types/react": "~18.2.14",
     "prettier": "^3.1.0",
     "prettier": "^3.1.0",
-    "react-native-svg-transformer": "^1.1.0",
-    "typescript": "^5.1.3"
+    "react-native-svg-transformer": "^1.5.0",
+    "typescript": "~5.3.3"
   },
   },
   "private": true
   "private": true
 }
 }

+ 99 - 1
src/components/MenuDrawer/index.tsx

@@ -1,6 +1,7 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
-import { View, Image, Linking, Text } from 'react-native';
+import { View, Image, Linking, Text, Switch, Platform } from 'react-native';
 import { CommonActions, useNavigation } from '@react-navigation/native';
 import { CommonActions, useNavigation } from '@react-navigation/native';
+import * as Notifications from 'expo-notifications';
 
 
 import { WarningModal } from '../WarningModal';
 import { WarningModal } from '../WarningModal';
 import { MenuButton } from '../MenuButton';
 import { MenuButton } from '../MenuButton';
@@ -18,6 +19,8 @@ import InfoIcon from 'assets/icons/info-solid.svg';
 
 
 import { APP_VERSION, FASTEST_MAP_HOST } from 'src/constants';
 import { APP_VERSION, FASTEST_MAP_HOST } from 'src/constants';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { usePostSaveNotificationTokenMutation } from '@api/user';
+import { usePushNotification } from 'src/contexts/PushNotificationContext';
 
 
 export const MenuDrawer = (props: any) => {
 export const MenuDrawer = (props: any) => {
   const { mutate: deleteUser } = useDeleteUserMutation();
   const { mutate: deleteUser } = useDeleteUserMutation();
@@ -30,6 +33,9 @@ export const MenuDrawer = (props: any) => {
     action: () => {}
     action: () => {}
   });
   });
   const { updateNotificationStatus } = useNotification();
   const { updateNotificationStatus } = useNotification();
+  const { mutateAsync: saveNotificationToken } = usePostSaveNotificationTokenMutation();
+  const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
+  const { isSubscribed, toggleSubscription } = usePushNotification();
 
 
   const openModal = (type: string, message: string, action: any) => {
   const openModal = (type: string, message: string, action: any) => {
     setModalInfo({
     setModalInfo({
@@ -63,6 +69,72 @@ export const MenuDrawer = (props: any) => {
     deleteUser({ token }, { onSuccess: handleLogout });
     deleteUser({ token }, { onSuccess: handleLogout });
   };
   };
 
 
+  const handleSubscribe = async () => {
+    const deviceData = await registerForPushNotificationsAsync();
+
+    if (deviceData?.notificationToken) {
+      toggleSubscription();
+      await saveNotificationToken({
+        token,
+        platform: deviceData.platform,
+        n_token: deviceData.notificationToken
+      });
+    }
+  };
+
+  const toggleSwitch = async () => {
+    if (isSubscribed) {
+      toggleSubscription();
+    } else {
+      const { status } = await Notifications.getPermissionsAsync();
+      if (status !== 'granted') {
+        setModalInfo({
+          visible: true,
+          type: 'success',
+          message:
+            'To use this feature we need your permission to access your notifications. If you press OK your system will ask you to confirm permission to receive notifications from NomadMania.',
+          action: () => setShouldOpenWarningModal(true)
+        });
+      } else {
+        handleSubscribe();
+      }
+    }
+  };
+
+  async function registerForPushNotificationsAsync() {
+    const { status: existingStatus } = await Notifications.getPermissionsAsync();
+    let finalStatus = existingStatus;
+    if (existingStatus !== 'granted') {
+      const { status } = await Notifications.requestPermissionsAsync();
+      finalStatus = status;
+    }
+    if (finalStatus !== 'granted') {
+      setModalInfo({
+        visible: true,
+        type: 'success',
+        message:
+          'NomadMania app needs notification permissions to function properly. Open settings?',
+        action: () =>
+          Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
+      });
+      return null;
+    }
+    const deviceData = await Notifications.getDevicePushTokenAsync();
+    console.log('deviceData', deviceData);
+
+    if (Platform.OS === 'android') {
+      Notifications.setNotificationChannelAsync('default', {
+        name: 'default',
+        importance: Notifications.AndroidImportance.MAX,
+        vibrationPattern: [0, 250, 250, 250],
+        lightColor: '#FF231F7C'
+      });
+    }
+    storage.set('deviceToken', deviceData.data);
+
+    return { notificationToken: deviceData.data ?? '', platform: deviceData.type ?? '' };
+  }
+
   return (
   return (
     <>
     <>
       <View style={styles.container}>
       <View style={styles.container}>
@@ -88,6 +160,26 @@ export const MenuDrawer = (props: any) => {
             red={false}
             red={false}
             buttonFn={() => Linking.openURL('https://nomadmania.com/terms/')}
             buttonFn={() => Linking.openURL('https://nomadmania.com/terms/')}
           />
           />
+          <View
+            style={{
+              display: 'flex',
+              flexDirection: 'row',
+              justifyContent: 'space-between',
+              marginTop: 20,
+              alignItems: 'center'
+            }}
+          >
+            <Text style={{ color: Colors.DARK_BLUE, fontSize: 16, fontWeight: 'bold' }}>
+              Notifications
+            </Text>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleSwitch}
+              value={isSubscribed}
+              style={{ transform: 'scale(0.8)' }}
+            />
+          </View>
         </View>
         </View>
 
 
         <View style={styles.bottomMenu}>
         <View style={styles.bottomMenu}>
@@ -135,6 +227,12 @@ export const MenuDrawer = (props: any) => {
           modalInfo.action();
           modalInfo.action();
           closeModal();
           closeModal();
         }}
         }}
+        onModalHide={() => {
+          if (shouldOpenWarningModal) {
+            setShouldOpenWarningModal(false);
+            handleSubscribe();
+          }
+        }}
       />
       />
     </>
     </>
   );
   );

+ 138 - 0
src/contexts/PushNotificationContext.tsx

@@ -0,0 +1,138 @@
+import React, { useEffect, useState, useContext, createContext } from 'react';
+import * as Notifications from 'expo-notifications';
+import { storage, StoreType } from 'src/storage';
+import { Linking, Platform } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+const PushNotificationContext = createContext(null);
+
+export const usePushNotification = () => useContext(PushNotificationContext);
+
+export const PushNotificationProvider = ({ children }) => {
+  const [isSubscribed, setIsSubscribed] = useState(
+    (storage.get('subscribed', StoreType.BOOLEAN) as boolean) ?? false
+  );
+  const navigation = useNavigation();
+
+  const lastNotificationResponse = Notifications.useLastNotificationResponse();
+
+  useEffect(() => {
+    if (lastNotificationResponse && Platform.OS === 'android') {
+      console.log(
+        'lastNotificationResponse',
+        lastNotificationResponse.notification.request.content.data
+      );
+      const data = lastNotificationResponse.notification.request.content.data;
+
+      if (data?.screen && data?.parentScreen) {
+        if (data?.params) {
+          navigation.navigate(
+            ...([
+              data.parentScreen,
+              {
+                screen: data.screen,
+                params: data.params
+              }
+            ] as never)
+          );
+        } else {
+          navigation.navigate(
+            ...([
+              data.parentScreen,
+              {
+                screen: data.screen
+              }
+            ] as never)
+          );
+        }
+      }
+      if (data?.url) {
+        Linking.openURL(data.url);
+      }
+    }
+  }, [lastNotificationResponse]);
+
+  useEffect(() => {
+    if (isSubscribed) {
+      const notificationListener = Notifications.addNotificationReceivedListener((notification) => {
+        console.log('Notification received', notification.request);
+      });
+
+      const responseListener = Notifications.addNotificationResponseReceivedListener((response) => {
+        console.log('Notification response received', response.notification.request);
+
+        let screenName;
+        let url;
+        let parentScreen;
+        let params;
+        if (Platform.OS === 'ios') {
+          console.log('data ios', response.notification.request.trigger?.payload);
+          parentScreen = response.notification.request.trigger?.payload?.parentScreen;
+          screenName = response.notification.request.trigger?.payload?.screen;
+          params = response.notification.request.trigger?.payload?.params;
+
+          url = response.notification.request.trigger?.payload?.url;
+        }
+
+        if (screenName && parentScreen) {
+          if (params) {
+            navigation.navigate(
+              ...([
+                parentScreen,
+                {
+                  screen: screenName,
+                  params: params
+                }
+              ] as never)
+            );
+          } else {
+            navigation.navigate(
+              ...([
+                parentScreen,
+                {
+                  screen: screenName
+                }
+              ] as never)
+            );
+          }
+        }
+        if (url) {
+          Linking.openURL(url);
+        }
+      });
+
+      return () => {
+        notificationListener.remove();
+        responseListener.remove();
+      };
+    }
+  }, [isSubscribed]);
+
+  const subscribeToNotifications = async () => {
+    storage.set('subscribed', true);
+    setIsSubscribed(true);
+  };
+
+  const unsubscribeFromNotifications = async () => {
+    await removeNotificationTokenFromServer();
+    storage.remove('deviceToken');
+    storage.set('subscribed', false);
+    setIsSubscribed(false);
+  };
+
+  const toggleSubscription = async () => {
+    if (isSubscribed) {
+      await unsubscribeFromNotifications();
+    } else {
+      await subscribeToNotifications();
+    }
+  };
+
+  return (
+    <PushNotificationContext.Provider value={{ isSubscribed, toggleSubscription }}>
+      {children}
+    </PushNotificationContext.Provider>
+  );
+};
+
+async function removeNotificationTokenFromServer() {}

+ 1 - 1
src/database/index.ts

@@ -1,4 +1,4 @@
-import * as SQLite from 'expo-sqlite';
+import * as SQLite from 'expo-sqlite/legacy';
 import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
 import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
 import { StoreType, storage } from 'src/storage';
 import { StoreType, storage } from 'src/storage';
 import { fetchLimitedRanking, fetchLpi, fetchInHistory, fetchInMemoriam } from '@api/ranking';
 import { fetchLimitedRanking, fetchLpi, fetchInHistory, fetchInMemoriam } from '@api/ranking';

+ 1 - 1
src/db/index.ts

@@ -1,4 +1,4 @@
-import * as SQLite from 'expo-sqlite';
+import * as SQLite from 'expo-sqlite/legacy';
 import * as FileSystem from 'expo-file-system';
 import * as FileSystem from 'expo-file-system';
 import { Asset } from 'expo-asset';
 import { Asset } from 'expo-asset';
 import { API_HOST } from 'src/constants';
 import { API_HOST } from 'src/constants';

+ 1 - 0
src/modules/api/user/queries/index.ts

@@ -6,4 +6,5 @@ export * from './use-post-get-profile-regions';
 export * from './use-post-get-profile-data';
 export * from './use-post-get-profile-data';
 export * from './use-post-get-profile-updates';
 export * from './use-post-get-profile-updates';
 export * from './use-post-get-map-years';
 export * from './use-post-get-map-years';
+export * from './use-post-save-notification-token';
 export * from './use-post-get-update';
 export * from './use-post-get-update';

+ 30 - 0
src/modules/api/user/queries/use-post-save-notification-token.tsx

@@ -0,0 +1,30 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { userQueryKeys } from '../user-query-keys';
+import { userApi } from '../user-api';
+import { ResponseType } from '@api/response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostSaveNotificationTokenMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    {
+      token: string;
+      platform: string;
+      n_token: string;
+    },
+    ResponseType
+  >({
+    mutationKey: userQueryKeys.setNotificationToken(),
+    mutationFn: async (variables) => {
+      const response = await userApi.setNotificationToken(
+        variables.token,
+        variables.platform,
+        variables.n_token
+      );
+      return response.data;
+    }
+  });
+};

+ 6 - 0
src/modules/api/user/user-api.tsx

@@ -377,6 +377,12 @@ export const userApi = {
       token,
       token,
       profile_id
       profile_id
     }),
     }),
+  setNotificationToken: (token: string, platform: string, n_token: string) =>
+    request.postForm<ResponseType>(API.SET_NOTIFICATION_TOKEN, {
+      token,
+      platform,
+      n_token
+    }),
   getMapYears: (token: string, profile_id: number) =>
   getMapYears: (token: string, profile_id: number) =>
     request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { token, profile_id }),
     request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { token, profile_id }),
   getUpdate: <T extends string>(token: string, profile_id: number, type: T) =>
   getUpdate: <T extends string>(token: string, profile_id: number, type: T) =>

+ 1 - 0
src/modules/api/user/user-query-keys.tsx

@@ -7,5 +7,6 @@ export const userQueryKeys = {
   getProfileInfoData: (userId: number) => ['getProfileInfoData', userId] as const,
   getProfileInfoData: (userId: number) => ['getProfileInfoData', userId] as const,
   getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const,
   getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const,
   getMapYears: (userId: number) => ['getMapYears', userId] as const,
   getMapYears: (userId: number) => ['getMapYears', userId] as const,
+  setNotificationToken: () => ['setNotificationToken'] as const,
   getUpdate: (userId: number, type: string) => ['getUpdate', userId, type] as const
   getUpdate: (userId: number, type: string) => ['getUpdate', userId, type] as const
 };
 };

+ 1 - 1
src/modules/map/regionData.ts

@@ -1,4 +1,4 @@
-import { SQLiteDatabase } from 'expo-sqlite';
+import { SQLiteDatabase } from 'expo-sqlite/legacy';
 
 
 export const getData = async (
 export const getData = async (
   db: SQLiteDatabase | null, 
   db: SQLiteDatabase | null,