App.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import './src/utils/dayjsTimezoneFix';
  2. import 'react-native-gesture-handler';
  3. import 'expo-splash-screen';
  4. import { QueryClientProvider } from '@tanstack/react-query';
  5. import { NavigationContainer } from '@react-navigation/native';
  6. import { queryClient } from 'src/utils/queryClient';
  7. import * as Sentry from '@sentry/react-native';
  8. import { GestureHandlerRootView } from 'react-native-gesture-handler';
  9. import { SheetProvider } from 'react-native-actions-sheet';
  10. import { SafeAreaProvider } from 'react-native-safe-area-context';
  11. import Route from './Route';
  12. import { ConnectionProvider } from 'src/contexts/ConnectionContext';
  13. import ConnectionBanner from 'src/components/ConnectionBanner/ConnectionBanner';
  14. import { RegionProvider } from 'src/contexts/RegionContext';
  15. import { ErrorProvider, useError } from 'src/contexts/ErrorContext';
  16. import { useEffect, useState } from 'react';
  17. import { setupInterceptors } from 'src/utils/request';
  18. import { ErrorModal, WarningModal } from 'src/components';
  19. import React from 'react';
  20. import { AppState, Linking, Platform } from 'react-native';
  21. import { API_HOST, API_URL, APP_VERSION } from 'src/constants';
  22. import axios from 'axios';
  23. import { API } from 'src/types';
  24. import { LogBox } from 'react-native';
  25. import { storage, StoreType } from 'src/storage';
  26. import { setupGlobalErrorHandler } from 'src/utils/globalErrorHandler';
  27. import {
  28. startAutoSyncListener,
  29. stopAutoSyncListener
  30. } from 'src/watermelondb/features/chat/networkSync';
  31. import { fetchActivePoll, PostGetActivePoll } from '@api/app';
  32. const POLL_LAST_SHOWN_KEY = 'poll_popup_last_shown';
  33. const getTodayKey = () => {
  34. const d = new Date();
  35. return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
  36. };
  37. const IOS_STORE_URL = 'https://apps.apple.com/app/id6502843543';
  38. const ANDROID_STORE_URL =
  39. 'https://play.google.com/store/apps/details?id=com.nomadmania.presentation';
  40. LogBox.ignoreLogs([/defaultProps will be removed/, /IMGElement/]);
  41. const userId = (storage.get('uid', StoreType.STRING) as string) ?? 'not_logged_in';
  42. const token = (storage.get('token', StoreType.STRING) as string) ?? null;
  43. const routingInstrumentation = Sentry.reactNavigationIntegration({
  44. enableTimeToInitialDisplay: true
  45. });
  46. Sentry.init({
  47. dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
  48. integrations: [Sentry.reactNativeTracingIntegration({ routingInstrumentation })],
  49. debug: false,
  50. enableNative: true,
  51. enableNativeCrashHandling: true,
  52. ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded'],
  53. beforeSend(event, hint) {
  54. if (userId) {
  55. event.user = {
  56. ...event.user,
  57. userId: userId
  58. };
  59. }
  60. const isNonError = hint?.originalException instanceof Error === false;
  61. if (isNonError || event.message?.match(/Non-Error exception captured/)) {
  62. return {
  63. ...event,
  64. message: `Processed Non-Error: ${event.message || 'No message'}`,
  65. level: 'warning',
  66. contexts: {
  67. ...event.contexts,
  68. non_error: {
  69. type: typeof hint?.originalException,
  70. value: JSON.stringify(hint?.originalException)
  71. }
  72. }
  73. };
  74. }
  75. return event;
  76. }
  77. });
  78. const linking = {
  79. prefixes: [API_HOST, 'nomadmania://', 'nm://'],
  80. config: {
  81. screens: {
  82. publicProfileView: '/profile/:userId',
  83. inAppEvent: '/event/:url',
  84. inAppSeriesShare: '/series-item/:id',
  85. inAppMapTab: '/map/:lon/:lat',
  86. inAppChatsList: '/messages',
  87. inAppMasterRanking: '/master-ranking',
  88. inAppLpiRanking: '/lpi',
  89. inAppInMemoriam: '/in-memoriam',
  90. inAppInHistory: '/travellers-in-history',
  91. inAppUNMaster: '/un-masters',
  92. inAppStatistics: '/statistics',
  93. inAppTriumphs: '/triumphs',
  94. inAppSeriesRanking: '/series-ranking',
  95. inAppMyFriends: '/my-friends',
  96. inAppFixers: '/fixers',
  97. inAppEarth: '/earth',
  98. inAppSeries: '/series',
  99. inAppCountries: '/countries',
  100. inAppRegions: '/regions',
  101. inAppDare: '/dare',
  102. inAppRegionPreview: {
  103. path: 'region/:id',
  104. alias: [
  105. 'region_mqp/:id',
  106. 'region/:id/',
  107. 'region_mqp/:id/',
  108. ],
  109. },
  110. inAppPhotos: '/photos',
  111. inAppTrips2025: '/trips'
  112. }
  113. }
  114. };
  115. const App = () => {
  116. return (
  117. <GestureHandlerRootView style={{ flex: 1 }}>
  118. <SafeAreaProvider>
  119. <SheetProvider>
  120. <QueryClientProvider client={queryClient}>
  121. <ErrorProvider>
  122. <InnerApp />
  123. </ErrorProvider>
  124. </QueryClientProvider>
  125. </SheetProvider>
  126. </SafeAreaProvider>
  127. </GestureHandlerRootView>
  128. );
  129. };
  130. const InnerApp = () => {
  131. const errorContext = useError();
  132. const navigation = React.useRef<any>(null);
  133. const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
  134. const [latestVersion, setLatestVersion] = useState<string | null>(null);
  135. const [activePoll, setActivePoll] = useState<PostGetActivePoll | null>(null);
  136. useEffect(() => {
  137. setupGlobalErrorHandler(navigation);
  138. }, []);
  139. useEffect(() => {
  140. setupInterceptors(errorContext);
  141. }, [errorContext]);
  142. useEffect(() => {
  143. const checkLatestVersion = async () => {
  144. try {
  145. const response = await axios.get(API_URL + '/' + API.LATEST_VERSION, {
  146. headers: {
  147. 'App-Version': APP_VERSION,
  148. Platform: Platform.OS
  149. }
  150. });
  151. const { version } = response.data;
  152. const formatVersion = (versionString: string) => {
  153. return parseInt(versionString.replace(/\./g, ''), 10);
  154. };
  155. const currentVersionInt = formatVersion(APP_VERSION);
  156. const latestVersionInt = formatVersion(version);
  157. if (latestVersionInt > currentVersionInt) {
  158. const dismissedVersion = storage.get('dismissed_update_version', StoreType.STRING) as string | null;
  159. if (dismissedVersion !== version) {
  160. setIsUpdateAvailable(true);
  161. setLatestVersion(version);
  162. }
  163. }
  164. } catch (error) {
  165. console.error('Failed to check latest version:', error);
  166. }
  167. };
  168. checkLatestVersion();
  169. }, []);
  170. useEffect(() => {
  171. if (token) startAutoSyncListener(token);
  172. return () => stopAutoSyncListener();
  173. }, [token]);
  174. useEffect(() => {
  175. const checkActivePoll = async () => {
  176. if (storage.get(POLL_LAST_SHOWN_KEY, StoreType.STRING) === getTodayKey()) return;
  177. const data = await fetchActivePoll(token);
  178. if (data?.result === 'OK' && data.active === 1 && data.url) {
  179. storage.set(POLL_LAST_SHOWN_KEY, getTodayKey());
  180. setActivePoll(data);
  181. }
  182. };
  183. checkActivePoll();
  184. const subscription = AppState.addEventListener('change', (state) => {
  185. if (state === 'active') checkActivePoll();
  186. });
  187. return () => subscription.remove();
  188. }, []);
  189. const handlePollPress = () => {
  190. if (!activePoll?.url) return;
  191. Linking.openURL(activePoll.url).catch((err) => console.error('Failed to open poll URL:', err));
  192. };
  193. const handleUpdatePress = () => {
  194. const storeUrl = Platform.OS === 'ios' ? IOS_STORE_URL : ANDROID_STORE_URL;
  195. Linking.openURL(storeUrl).catch((err) => console.error('Failed to open store URL:', err));
  196. };
  197. return (
  198. <ConnectionProvider>
  199. <RegionProvider>
  200. <NavigationContainer
  201. ref={navigation}
  202. onReady={() => {
  203. routingInstrumentation.registerNavigationContainer(navigation);
  204. }}
  205. linking={linking}
  206. navigationInChildEnabled
  207. >
  208. <Route />
  209. <ConnectionBanner />
  210. <ErrorModal />
  211. <WarningModal
  212. isVisible={isUpdateAvailable}
  213. type="success"
  214. title="Update Available"
  215. message="A new version of the NomadMania app is available. Please update to the latest version."
  216. action={handleUpdatePress}
  217. onClose={() => {
  218. if (latestVersion) {
  219. storage.set('dismissed_update_version', latestVersion);
  220. }
  221. setIsUpdateAvailable(false);
  222. }}
  223. />
  224. <WarningModal
  225. isVisible={!!activePoll && !isUpdateAvailable}
  226. type="poll"
  227. title={activePoll?.title || 'We need your opinion!'}
  228. message={
  229. activePoll?.message ||
  230. 'A new NomadMania poll is live. It only takes a minute - tell us what you think!'
  231. }
  232. buttonTitle={activePoll?.button_text || undefined}
  233. action={handlePollPress}
  234. onClose={() => setActivePoll(null)}
  235. />
  236. </NavigationContainer>
  237. </RegionProvider>
  238. </ConnectionProvider>
  239. );
  240. };
  241. export default Sentry.wrap(App);