Viktoriia 9 ماه پیش
والد
کامیت
cf49869884
10فایلهای تغییر یافته به همراه200 افزوده شده و 160 حذف شده
  1. 1 2
      .gitignore
  2. 0 48
      App.tsx
  3. 48 39
      Route.tsx
  4. 18 7
      app.config.ts
  5. 30 30
      package.json
  6. 5 31
      src/components/MenuDrawer/index.tsx
  7. 95 0
      src/contexts/PushNotificationContext.tsx
  8. 1 1
      src/database/index.ts
  9. 1 1
      src/db/index.ts
  10. 1 1
      src/modules/map/regionData.ts

+ 1 - 2
.gitignore

@@ -38,5 +38,4 @@ package-lock.json
 .vscode
 temp
 android
-ios
-google-services.json
+ios

+ 0 - 48
App.tsx

@@ -15,8 +15,6 @@ import { setupInterceptors } from 'src/utils/request';
 import { ErrorModal } from 'src/components';
 import { NotificationProvider } from 'src/contexts/NotificationContext';
 import React from 'react';
-import * as Notifications from 'expo-notifications';
-import { Platform } from 'react-native';
 
 const routingInstrumentation = new Sentry.ReactNavigationInstrumentation({
   enableTimeToInitialDisplay: true
@@ -30,52 +28,6 @@ Sentry.init({
 });
 
 const App = () => {
-  async function setupNotificationChannels() {
-    if (Platform.OS === 'android') {
-      await Notifications.setNotificationChannelAsync('default', {
-        name: 'default',
-        importance: Notifications.AndroidImportance.MAX,
-        vibrationPattern: [0, 250, 250, 250],
-        lightColor: '#FF231F7C'
-      });
-    }
-  }
-
-  useEffect(() => {
-    setupNotificationChannels();
-  }, []);
-
-  useEffect(() => {
-    let notificationListener: any;
-    let responseListener: any;
-
-    const checkLastNotificationResponse = async () => {
-      const lastNotificationResponse = await Notifications.getLastNotificationResponseAsync();
-      if (lastNotificationResponse) {
-        const data = lastNotificationResponse.notification.request.content.data;
-        console.log('lastNotificationResponse', lastNotificationResponse.notification.request);
-        console.log('dataLast', data);
-      }
-    };
-
-    checkLastNotificationResponse();
-
-    notificationListener = Notifications.addNotificationReceivedListener((notification) => {
-      console.log('notification', notification.request);
-    });
-
-    responseListener = Notifications.addNotificationResponseReceivedListener((response) => {
-      const data = response.notification.request.content.data;
-      console.log('payload', response.notification.request.trigger?.payload);
-      console.log('data', data);
-    });
-
-    return () => {
-      if (notificationListener) notificationListener.remove();
-      if (responseListener) responseListener.remove();
-    };
-  }, []);
-
   return (
     <QueryClientProvider client={queryClient}>
       <NotificationProvider>

+ 48 - 39
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();
 
@@ -410,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>
   );
 };
 

+ 18 - 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,7 +84,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     config: {
       googleMaps: {
         apiKey: env.ANDROID_GOOGLE_MAP_APIKEY
-      },
+      }
     },
     googleServicesFile: './google-services.json',
     permissions: [
@@ -99,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
 }

+ 5 - 31
src/components/MenuDrawer/index.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useState } from 'react';
 import { View, Image, Linking, Text, Switch, Platform } from 'react-native';
 import { CommonActions, useNavigation } from '@react-navigation/native';
 import * as Notifications from 'expo-notifications';
@@ -20,6 +20,7 @@ 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();
@@ -33,10 +34,8 @@ export const MenuDrawer = (props: any) => {
   });
   const { updateNotificationStatus } = useNotification();
   const { mutateAsync: saveNotificationToken } = usePostSaveNotificationTokenMutation();
-  const [isSubscribed, setIsSubscribed] = useState(
-    (storage.get('subscribed', StoreType.BOOLEAN) as boolean) ?? false
-  );
   const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
+  const { isSubscribed, toggleSubscription } = usePushNotification();
 
   const openModal = (type: string, message: string, action: any) => {
     setModalInfo({
@@ -74,8 +73,7 @@ export const MenuDrawer = (props: any) => {
     const deviceData = await registerForPushNotificationsAsync();
 
     if (deviceData?.notificationToken) {
-      storage.set('subscribed', true);
-      setIsSubscribed(true);
+      toggleSubscription();
       await saveNotificationToken({
         token,
         platform: deviceData.platform,
@@ -84,33 +82,9 @@ export const MenuDrawer = (props: any) => {
     }
   };
 
-  useEffect(() => {
-    let notificationListener: any;
-    let responseListener: any;
-
-    if (isSubscribed) {
-      notificationListener = Notifications.addNotificationReceivedListener((notification) => {
-        console.log('notification', notification.request);
-      });
-
-      responseListener = Notifications.addNotificationResponseReceivedListener((response) => {
-        const data = response.notification.request.content.data;
-        console.log('payload', response.notification.request.trigger?.payload);
-        console.log('data', data);
-      });
-    }
-
-    return () => {
-      if (notificationListener) Notifications.removeNotificationSubscription(notificationListener);
-      if (responseListener) Notifications.removeNotificationSubscription(responseListener);
-    };
-  }, [isSubscribed]);
-
   const toggleSwitch = async () => {
     if (isSubscribed) {
-      storage.set('subscribed', false);
-      storage.remove('deviceToken');
-      setIsSubscribed(false);
+      toggleSubscription();
     } else {
       const { status } = await Notifications.getPermissionsAsync();
       if (status !== 'granted') {

+ 95 - 0
src/contexts/PushNotificationContext.tsx

@@ -0,0 +1,95 @@
+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) {
+        navigation.navigate(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;
+        if (Platform.OS === 'ios') {
+          console.log('data ios', response.notification.request.trigger?.payload);
+          screenName = response.notification.request.trigger?.payload?.screen;
+          url = response.notification.request.trigger?.payload?.url;
+        }
+
+        if (screenName) {
+          navigation.navigate(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 - 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,