ソースを参照

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

Viktoriia 10 ヶ月 前
コミット
5cd7ea51b1

+ 1 - 1
App.tsx

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

+ 53 - 40
Route.tsx

@@ -84,6 +84,7 @@ import { userApi } from '@api/user';
 import axios from 'axios';
 import { useNotification } from 'src/contexts/NotificationContext';
 import PreviewScreen from 'src/screens/InAppScreens/ProfileScreen/ShareScreen';
+import { PushNotificationProvider } from 'src/contexts/PushNotificationContext';
 
 enableScreens();
 
@@ -152,10 +153,10 @@ const Route = () => {
 
   useEffect(() => {
     const prepareApp = async () => {
-      // checkTokenAndUpdate();
       await checkNmToken();
       await findFastestServer();
       await openDatabases();
+      await checkTokenAndUpdate();
       setDbLoaded(true);
     };
 
@@ -185,6 +186,10 @@ const Route = () => {
 
   const checkTokenAndUpdate = async () => {
     const storedToken = storage.get('deviceToken', StoreType.STRING);
+    const { status } = await Notifications.getPermissionsAsync();
+    if (status !== 'granted') {
+      return;
+    }
     const currentToken = await Notifications.getDevicePushTokenAsync();
 
     if (storedToken && currentToken?.data !== storedToken) {
@@ -406,45 +411,53 @@ const Route = () => {
   );
 
   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 { env } from 'process';
 import path from 'path';
 import dotenv from 'dotenv';
 
 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 MAP_HOST = env.ENV === 'production' ? env.PRODUCTION_MAP_HOST : env.DEVELOPMENT_MAP_HOST;
+
 const GOOGLE_MAP_PLACES_APIKEY = env.GOOGLE_MAP_PLACES_APIKEY;
 
 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',
       NSPushNotificationsDescription:
         '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: {
@@ -74,8 +84,9 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     config: {
       googleMaps: {
         apiKey: env.ANDROID_GOOGLE_MAP_APIKEY
-      },
+      }
     },
+    googleServicesFile: './google-services.json',
     permissions: [
       // 'ACCESS_BACKGROUND_LOCATION',
       'ACCESS_FINE_LOCATION',
@@ -98,22 +109,23 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       }
     ],
     [
-      "expo-build-properties",
+      'expo-build-properties',
       {
         android: {
           minSdkVersion: 24,
-          targetSdkVersion: 34,
+          targetSdkVersion: 34
           // kotlinVersion: '1.7.1'
         }
       }
     ],
     [
-      "@sentry/react-native/expo",
+      '@sentry/react-native/expo',
       {
         organization: env.SENTRY_ORG,
         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"
   },
   "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/drawer": "^6.6.15",
     "@react-navigation/material-top-tabs": "^6.6.5",
     "@react-navigation/native": "^6.1.9",
     "@react-navigation/native-stack": "^6.9.17",
     "@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",
     "@turf/turf": "^6.5.0",
     "axios": "^1.6.1",
     "better-react-native-image-viewing": "^0.2.7",
     "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",
     "moment": "^2.29.4",
     "patch-package": "^8.0.0",
     "promise": "^8.3.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-calendars": "^1.1304.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-image-viewing": "^0.2.2",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "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-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-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-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-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-view-shot": "^3.7.0",
     "react-native-walkthrough-tooltip": "^1.6.0",
@@ -76,11 +76,11 @@
     "zustand": "^4.4.7"
   },
   "devDependencies": {
-    "@babel/core": "^7.20.0",
+    "@babel/core": "^7.25.2",
     "@types/react": "~18.2.14",
     "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
 }

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

@@ -1,6 +1,7 @@
 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 * as Notifications from 'expo-notifications';
 
 import { WarningModal } from '../WarningModal';
 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 { useNotification } from 'src/contexts/NotificationContext';
+import { usePostSaveNotificationTokenMutation } from '@api/user';
+import { usePushNotification } from 'src/contexts/PushNotificationContext';
 
 export const MenuDrawer = (props: any) => {
   const { mutate: deleteUser } = useDeleteUserMutation();
@@ -30,6 +33,9 @@ export const MenuDrawer = (props: any) => {
     action: () => {}
   });
   const { updateNotificationStatus } = useNotification();
+  const { mutateAsync: saveNotificationToken } = usePostSaveNotificationTokenMutation();
+  const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
+  const { isSubscribed, toggleSubscription } = usePushNotification();
 
   const openModal = (type: string, message: string, action: any) => {
     setModalInfo({
@@ -63,6 +69,72 @@ export const MenuDrawer = (props: any) => {
     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 (
     <>
       <View style={styles.container}>
@@ -88,6 +160,26 @@ export const MenuDrawer = (props: any) => {
             red={false}
             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 style={styles.bottomMenu}>
@@ -135,6 +227,12 @@ export const MenuDrawer = (props: any) => {
           modalInfo.action();
           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 { StoreType, storage } from 'src/storage';
 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 { Asset } from 'expo-asset';
 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-updates';
 export * from './use-post-get-map-years';
+export * from './use-post-save-notification-token';
 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,
       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) =>
     request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { token, profile_id }),
   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,
   getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const,
   getMapYears: (userId: number) => ['getMapYears', userId] as const,
+  setNotificationToken: () => ['setNotificationToken'] 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 (
   db: SQLiteDatabase | null,