App.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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 { 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. const IOS_STORE_URL = 'https://apps.apple.com/app/id6502843543';
  32. const ANDROID_STORE_URL =
  33. 'https://play.google.com/store/apps/details?id=com.nomadmania.presentation';
  34. LogBox.ignoreLogs([/defaultProps will be removed/, /IMGElement/]);
  35. const userId = (storage.get('uid', StoreType.STRING) as string) ?? 'not_logged_in';
  36. const token = (storage.get('token', StoreType.STRING) as string) ?? null;
  37. const routingInstrumentation = Sentry.reactNavigationIntegration({
  38. enableTimeToInitialDisplay: true
  39. });
  40. Sentry.init({
  41. dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
  42. integrations: [Sentry.reactNativeTracingIntegration({ routingInstrumentation })],
  43. debug: false,
  44. enableNative: true,
  45. enableNativeCrashHandling: true,
  46. ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded'],
  47. beforeSend(event, hint) {
  48. if (userId) {
  49. event.user = {
  50. ...event.user,
  51. userId: userId
  52. };
  53. }
  54. const isNonError = hint?.originalException instanceof Error === false;
  55. if (isNonError || event.message?.match(/Non-Error exception captured/)) {
  56. return {
  57. ...event,
  58. message: `Processed Non-Error: ${event.message || 'No message'}`,
  59. level: 'warning',
  60. contexts: {
  61. ...event.contexts,
  62. non_error: {
  63. type: typeof hint?.originalException,
  64. value: JSON.stringify(hint?.originalException)
  65. }
  66. }
  67. };
  68. }
  69. return event;
  70. }
  71. });
  72. const linking = {
  73. prefixes: [API_HOST, 'nomadmania://', 'nm://'],
  74. config: {
  75. screens: {
  76. publicProfileView: '/profile/:userId',
  77. inAppEvent: '/event/:url',
  78. inAppSeriesShare: '/series-item/:id',
  79. inAppMapTab: '/map/:lon/:lat',
  80. inAppChatsList: '/messages',
  81. inAppMasterRanking: '/master-ranking',
  82. inAppLpiRanking: '/lpi',
  83. inAppInMemoriam: '/in-memoriam',
  84. inAppInHistory: '/travellers-in-history',
  85. inAppUNMaster: '/un-masters',
  86. inAppStatistics: '/statistics',
  87. inAppTriumphs: '/triumphs',
  88. inAppSeriesRanking: '/series-ranking',
  89. inAppMyFriends: '/my-friends',
  90. inAppFixers: '/fixers',
  91. inAppEarth: '/earth',
  92. inAppSeries: '/series',
  93. inAppCountries: '/countries',
  94. inAppRegions: '/regions',
  95. inAppDare: '/dare',
  96. inAppRegionPreview: {
  97. path: 'region/:id',
  98. alias: [
  99. 'region_mqp/:id',
  100. 'region/:id/',
  101. 'region_mqp/:id/',
  102. ],
  103. },
  104. inAppPhotos: '/photos',
  105. inAppTrips2025: '/trips'
  106. }
  107. }
  108. };
  109. const App = () => {
  110. return (
  111. <GestureHandlerRootView style={{ flex: 1 }}>
  112. <SafeAreaProvider>
  113. <SheetProvider>
  114. <QueryClientProvider client={queryClient}>
  115. <ErrorProvider>
  116. <InnerApp />
  117. </ErrorProvider>
  118. </QueryClientProvider>
  119. </SheetProvider>
  120. </SafeAreaProvider>
  121. </GestureHandlerRootView>
  122. );
  123. };
  124. const InnerApp = () => {
  125. const errorContext = useError();
  126. const navigation = React.useRef<any>(null);
  127. const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
  128. const [latestVersion, setLatestVersion] = useState<string | null>(null);
  129. useEffect(() => {
  130. setupGlobalErrorHandler(navigation);
  131. }, []);
  132. useEffect(() => {
  133. setupInterceptors(errorContext);
  134. }, [errorContext]);
  135. useEffect(() => {
  136. const checkLatestVersion = async () => {
  137. try {
  138. const response = await axios.get(API_URL + '/' + API.LATEST_VERSION, {
  139. headers: {
  140. 'App-Version': APP_VERSION,
  141. Platform: Platform.OS
  142. }
  143. });
  144. const { version } = response.data;
  145. const formatVersion = (versionString: string) => {
  146. return parseInt(versionString.replace(/\./g, ''), 10);
  147. };
  148. const currentVersionInt = formatVersion(APP_VERSION);
  149. const latestVersionInt = formatVersion(version);
  150. if (latestVersionInt > currentVersionInt) {
  151. const dismissedVersion = storage.get('dismissed_update_version', StoreType.STRING) as string | null;
  152. if (dismissedVersion !== version) {
  153. setIsUpdateAvailable(true);
  154. setLatestVersion(version);
  155. }
  156. }
  157. } catch (error) {
  158. console.error('Failed to check latest version:', error);
  159. }
  160. };
  161. checkLatestVersion();
  162. }, []);
  163. useEffect(() => {
  164. if (token) startAutoSyncListener(token);
  165. return () => stopAutoSyncListener();
  166. }, [token]);
  167. const handleUpdatePress = () => {
  168. const storeUrl = Platform.OS === 'ios' ? IOS_STORE_URL : ANDROID_STORE_URL;
  169. Linking.openURL(storeUrl).catch((err) => console.error('Failed to open store URL:', err));
  170. };
  171. return (
  172. <ConnectionProvider>
  173. <RegionProvider>
  174. <NavigationContainer
  175. ref={navigation}
  176. onReady={() => {
  177. routingInstrumentation.registerNavigationContainer(navigation);
  178. }}
  179. linking={linking}
  180. navigationInChildEnabled
  181. >
  182. <Route />
  183. <ConnectionBanner />
  184. <ErrorModal />
  185. <WarningModal
  186. isVisible={isUpdateAvailable}
  187. type="success"
  188. title="Update Available"
  189. message="A new version of the NomadMania app is available. Please update to the latest version."
  190. action={handleUpdatePress}
  191. onClose={() => {
  192. if (latestVersion) {
  193. storage.set('dismissed_update_version', latestVersion);
  194. }
  195. setIsUpdateAvailable(false);
  196. }}
  197. />
  198. </NavigationContainer>
  199. </RegionProvider>
  200. </ConnectionProvider>
  201. );
  202. };
  203. export default Sentry.wrap(App);