Viktoriia 5 روز پیش
والد
کامیت
07ffd2a1e6

+ 45 - 1
App.tsx

@@ -18,7 +18,7 @@ import { useEffect, useState } from 'react';
 import { setupInterceptors } from 'src/utils/request';
 import { ErrorModal, WarningModal } from 'src/components';
 import React from 'react';
-import { Linking, Platform } from 'react-native';
+import { AppState, Linking, Platform } from 'react-native';
 import { API_HOST, API_URL, APP_VERSION } from 'src/constants';
 import axios from 'axios';
 import { API } from 'src/types';
@@ -29,6 +29,13 @@ import {
   startAutoSyncListener,
   stopAutoSyncListener
 } from 'src/watermelondb/features/chat/networkSync';
+import { fetchActivePoll, PostGetActivePoll } from '@api/app';
+
+const POLL_LAST_SHOWN_KEY = 'poll_popup_last_shown';
+const getTodayKey = () => {
+  const d = new Date();
+  return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
+};
 
 const IOS_STORE_URL = 'https://apps.apple.com/app/id6502843543';
 const ANDROID_STORE_URL =
@@ -136,6 +143,7 @@ const InnerApp = () => {
   const navigation = React.useRef<any>(null);
   const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
   const [latestVersion, setLatestVersion] = useState<string | null>(null);
+  const [activePoll, setActivePoll] = useState<PostGetActivePoll | null>(null);
 
   useEffect(() => {
     setupGlobalErrorHandler(navigation);
@@ -183,6 +191,30 @@ const InnerApp = () => {
     return () => stopAutoSyncListener();
   }, [token]);
 
+  useEffect(() => {
+    const checkActivePoll = async () => {
+      if (storage.get(POLL_LAST_SHOWN_KEY, StoreType.STRING) === getTodayKey()) return;
+
+      const data = await fetchActivePoll(token);
+      if (data?.result === 'OK' && data.active === 1 && data.url) {
+        storage.set(POLL_LAST_SHOWN_KEY, getTodayKey());
+        setActivePoll(data);
+      }
+    };
+
+    checkActivePoll();
+
+    const subscription = AppState.addEventListener('change', (state) => {
+      if (state === 'active') checkActivePoll();
+    });
+    return () => subscription.remove();
+  }, []);
+
+  const handlePollPress = () => {
+    if (!activePoll?.url) return;
+    Linking.openURL(activePoll.url).catch((err) => console.error('Failed to open poll URL:', err));
+  };
+
   const handleUpdatePress = () => {
     const storeUrl = Platform.OS === 'ios' ? IOS_STORE_URL : ANDROID_STORE_URL;
     Linking.openURL(storeUrl).catch((err) => console.error('Failed to open store URL:', err));
@@ -215,6 +247,18 @@ const InnerApp = () => {
               setIsUpdateAvailable(false);
             }}
           />
+          <WarningModal
+            isVisible={!!activePoll && !isUpdateAvailable}
+            type="poll"
+            title={activePoll?.title || 'We need your opinion!'}
+            message={
+              activePoll?.message ||
+              'A new NomadMania poll is live. It only takes a minute - tell us what you think!'
+            }
+            buttonTitle={activePoll?.button_text || undefined}
+            action={handlePollPress}
+            onClose={() => setActivePoll(null)}
+          />
         </NavigationContainer>
       </RegionProvider>
     </ConnectionProvider>

+ 24 - 0
src/components/WarningModal/index.tsx

@@ -185,6 +185,30 @@ export const WarningModal = ({
           borderColor: Colors.DARK_BLUE
         }
       ]
+    },
+    poll: {
+      message,
+      buttons: [
+        {
+          text: 'Later',
+          textColor: Colors.DARK_BLUE,
+          color: Colors.WHITE,
+          action: () => {
+            onClose();
+          },
+          borderColor: Colors.DARK_BLUE
+        },
+        {
+          text: buttonTitle ?? 'Take the poll',
+          textColor: Colors.WHITE,
+          color: Colors.ORANGE,
+          action: () => {
+            onClose();
+            action && action();
+          },
+          borderColor: Colors.ORANGE
+        }
+      ]
     }
   };
 

+ 19 - 1
src/modules/api/app/app-api.ts

@@ -1,5 +1,8 @@
+import axios from 'axios';
+import { Platform } from 'react-native';
 import { request } from '../../../utils';
 import { API } from '../../../types';
+import { API_URL, APP_VERSION } from '../../../constants';
 import { ResponseType } from '../response-type';
 
 export interface PostGetLastUpdate extends ResponseType {
@@ -10,6 +13,15 @@ export interface PostGetPremiumStatus extends ResponseType {
   'premium-status': number;
 }
 
+export interface PostGetActivePoll extends ResponseType {
+  active: 0 | 1;
+  id?: number;
+  url?: string;
+  title?: string;
+  message?: string;
+  button_text?: string;
+}
+
 export const appApi = {
   deleteUser: (token: string) => request.postForm(API.DELETE_USER, { token }),
   getLastRegionsUpdate: (date: string) =>
@@ -18,5 +30,11 @@ export const appApi = {
     request.postForm<PostGetLastUpdate>(API.GET_LAST_DARE_DB_UPDATE, { date }),
   premiumStatus: (token: string) =>
     request.postForm<PostGetPremiumStatus>(API.GET_PREMIUM_STATUS, { token }),
-  getLastMapTilesUpdate: () => request.postForm<PostGetLastUpdate>(API.GET_LAST_MAP_TILES_UPDATE)
+  getLastMapTilesUpdate: () => request.postForm<PostGetLastUpdate>(API.GET_LAST_MAP_TILES_UPDATE),
+  getActivePoll: (token?: string | null) =>
+    axios.postForm<PostGetActivePoll>(
+      `${API_URL}/${API.GET_ACTIVE_POLL}`,
+      token ? { token } : {},
+      { headers: { 'App-Version': APP_VERSION, Platform: Platform.OS }, timeout: 10000 }
+    )
 };

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

@@ -4,4 +4,5 @@ export const appQueryKeys = {
   getLastDareUpdate: () => ['getLastDareUpdate'] as const,
   premiumStatus: (token: string) => ['premiumStatus', token] as const,
   getLastMapTilesUpdate: () => ['getLastMapTilesUpdate'] as const,
+  activePoll: () => ['activePoll'] as const,
 };

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

@@ -3,3 +3,4 @@ export * from './use-post-last-regions-db-update';
 export * from './use-post-last-dare-db-update';
 export * from './use-post-premium-status';
 export * from './use-post-last-map-tiles-update';
+export * from './use-post-active-poll';

+ 22 - 0
src/modules/api/app/queries/use-post-active-poll.tsx

@@ -0,0 +1,22 @@
+import { appQueryKeys } from '../app-query-keys';
+import { type PostGetActivePoll, appApi } from '../app-api';
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchActivePoll = async (token?: string | null) => {
+  try {
+    const data: PostGetActivePoll = await queryClient.query({
+      queryKey: appQueryKeys.activePoll(),
+      queryFn: async () => {
+        const response = await appApi.getActivePoll(token);
+        return response.data;
+      },
+      gcTime: 0,
+      staleTime: 0
+    });
+
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch active poll:', error);
+    return null;
+  }
+};

+ 4 - 2
src/types/api.ts

@@ -226,7 +226,8 @@ export enum API_ENDPOINT {
   GET_TRIPS_FOR_REGION = 'get-trips-for-region',
   GET_REGIONS_THAT_HAVE_TRIPS = 'get-regions-that-have-trips',
   GET_MAP_DATA = 'get-map-data',
-  GET_SINGLE_SERIES_ITEM = 'get-single-item'
+  GET_SINGLE_SERIES_ITEM = 'get-single-item',
+  GET_ACTIVE_POLL = 'active-poll'
 }
 
 export enum API {
@@ -426,7 +427,8 @@ export enum API {
   GET_TRIPS_FOR_REGION = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIPS_FOR_REGION}`,
   GET_REGIONS_THAT_HAVE_TRIPS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_REGIONS_THAT_HAVE_TRIPS}`,
   GET_MAP_DATA = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_MAP_DATA}`,
-  GET_SINGLE_SERIES_ITEM = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SINGLE_SERIES_ITEM}`
+  GET_SINGLE_SERIES_ITEM = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SINGLE_SERIES_ITEM}`,
+  GET_ACTIVE_POLL = `${API_ROUTE.APP}/${API_ENDPOINT.GET_ACTIVE_POLL}`
 }
 
 export type BaseAxiosError = AxiosError;