From fcd704b2588ac0f59972e5947213e7347ad253bb Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Thu, 11 Dec 2025 15:49:53 +0500 Subject: [PATCH] mmkv bug fix --- App.tsx | 245 ++++++++++++------ package-lock.json | 13 +- package.json | 2 +- src/api/axios.ts | 42 ++- src/helpers/event.ts | 57 ---- src/screens/auth/login/ui/Confirm.tsx | 10 +- src/screens/auth/registeration/ui/Confirm.tsx | 9 +- .../profile/myProfile/ui/ProfilePages.tsx | 5 +- src/screens/welcome/Onboarding.tsx | 6 +- 9 files changed, 194 insertions(+), 195 deletions(-) diff --git a/App.tsx b/App.tsx index 14c520d..b72a081 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,5 @@ // App.tsx +import AsyncStorage from '@react-native-async-storage/async-storage'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -7,18 +8,18 @@ import { navigationRef } from 'components/NavigationRef'; import i18n from 'i18n/i18n'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { I18nextProvider } from 'react-i18next'; -import { Animated, Dimensions, LogBox, StatusBar, View } from 'react-native'; +import { + Animated, + Dimensions, + LogBox, + StatusBar, + StyleSheet, + View, +} from 'react-native'; import Toast from 'react-native-toast-message'; import SplashScreen from './src/components/SplashScreen'; // Screens -import { - authEvents, - getLanguage, - getToken, - loadInitialAuthData, - storage, -} from 'helpers/event'; import Login from 'screens/auth/login/ui'; import Confirm from 'screens/auth/login/ui/Confirm'; import Register from 'screens/auth/registeration/ui'; @@ -46,81 +47,137 @@ import PaymentQrCode from 'screens/wallet/successPayment/ui/PaymentQrCode'; import Onboarding from 'screens/welcome/Onboarding'; LogBox.ignoreLogs([ + 'Non-serializable values were found in the navigation state', 'ViewPropTypes will be removed', - // NOTE: I recommend NOT ignoring "Non-serializable values were found in the navigation state" - // because it hides bugs that can cause crashes. Keep only if you understand the implications. ]); const Stack = createNativeStackNavigator(); const screenWidth = Dimensions.get('window').width; const queryClient = new QueryClient(); +// const saveNotification = async (remoteMessage: any) => { +// try { +// const stored = await AsyncStorage.getItem('notifications'); +// const notifications = stored ? JSON.parse(stored) : []; + +// const newNotification = { +// id: Date.now(), +// title: +// remoteMessage.notification?.title || +// remoteMessage.data?.title || +// 'Yangi bildirishnoma', +// message: +// remoteMessage.notification?.body || +// remoteMessage.data?.body || +// 'Matn yo‘q', +// sentTime: remoteMessage.sentTime || Date.now(), +// }; + +// await AsyncStorage.setItem( +// 'notifications', +// JSON.stringify([newNotification, ...notifications]), +// ); +// } catch (e) { +// console.error('Notification saqlashda xato:', e); +// } +// }; + +// async function onDisplayNotification(remoteMessage: any) { +// const channelId = await notifee.createChannel({ +// id: 'default', +// name: 'Umumiy bildirishnomalar', +// sound: 'default', +// importance: AndroidImportance.HIGH, +// }); + +// await notifee.displayNotification({ +// title: +// remoteMessage.notification?.title || +// remoteMessage.data?.title || +// 'Yangi xabar', +// body: +// remoteMessage.notification?.body || +// remoteMessage.data?.body || +// 'Matn yo‘q', +// android: { +// channelId, +// largeIcon: 'ic_launcher_foreground', +// sound: 'default', +// pressAction: { +// id: 'default', +// }, +// }, +// }); +// } + +// async function requestNotificationPermission() { +// if (Platform.OS === 'android' && Platform.Version >= 33) { +// const granted = await PermissionsAndroid.request( +// PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, +// ); +// console.log('POST_NOTIFICATIONS permission:', granted); +// } +// } + export default function App() { const [initialRoute, setInitialRoute] = useState(null); const slideAnim = useRef(new Animated.Value(0)).current; const [isSplashVisible, setIsSplashVisible] = useState(true); - const isMounted = useRef(false); - const currentAnimation = useRef(null); - const token = getToken(); - useEffect(() => { - loadInitialAuthData(); - }, []); + // useEffect(() => { + // requestNotificationPermission(); - useEffect(() => { - const logoutListener = () => { - if (navigationRef.isReady()) { - navigationRef.reset({ - index: 0, - routes: [{ name: 'Login' }], - }); - } - }; + // const messagingInstance = getMessaging(); - authEvents.on('logout', logoutListener); + // const unsubscribe = onMessage(messagingInstance, async remoteMessage => { + // console.log('Foreground message:', remoteMessage); + // await saveNotification(remoteMessage); + // await onDisplayNotification(remoteMessage); + // }); - return () => { - authEvents.removeListener('logout', logoutListener); - }; - }, []); + // const unsubscribeOpened = onNotificationOpenedApp( + // messagingInstance, + // remoteMessage => { + // console.log('Backgrounddan ochildi:', remoteMessage); + // saveNotification(remoteMessage); + // }, + // ); - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - if (currentAnimation.current) { - currentAnimation.current.stop(); - } - }; - }, []); + // (async () => { + // const remoteMessage = await getInitialNotification(messagingInstance); + // if (remoteMessage) { + // console.log('Killeddan ochildi:', remoteMessage); + // saveNotification(remoteMessage); + // } + // })(); + + // return () => { + // unsubscribe(); + // unsubscribeOpened(); + // }; + // }, []); useEffect(() => { const initializeApp = async () => { try { - const seen = storage.getString('hasSeenOnboarding'); - const token = getToken(); - const lang = getLanguage(); + const [seen, token, lang] = await Promise.all([ + AsyncStorage.getItem('hasSeenOnboarding'), + AsyncStorage.getItem('token'), - if (lang) { - try { - await i18n.changeLanguage(lang); - } catch (e) { - console.warn('i18n.changeLanguage failed:', e); - } - } + AsyncStorage.getItem('language'), + ]); + + if (lang) await i18n.changeLanguage(lang); const initialRouteName = !seen ? 'Onboarding' : token ? 'Home' : 'select-auth'; - - if (isMounted.current) { - setInitialRoute(initialRouteName); - } + setInitialRoute(initialRouteName); } catch (error) { console.error('App initialization error:', error); - if (isMounted.current) setInitialRoute('select-auth'); + setInitialRoute('select-auth'); } }; @@ -129,50 +186,52 @@ export default function App() { const handleSplashFinish = useMemo( () => () => { - // create animation and keep ref so we can stop it on unmount - const animation = Animated.timing(slideAnim, { + Animated.timing(slideAnim, { toValue: -screenWidth, duration: 600, useNativeDriver: true, - }); - currentAnimation.current = animation; - animation.start(() => { - // ensure component still mounted before setting state - if (isMounted.current) { - setIsSplashVisible(false); - } - currentAnimation.current = null; - }); + }).start(() => setIsSplashVisible(false)); }, [slideAnim], ); const handleOnboardingFinish = useMemo( () => async (navigation: any) => { - try { - storage.set('hasSeenOnboarding', 'true'); - } catch (e) { - console.warn('Failed to set hasSeenOnboarding', e); - } + await AsyncStorage.setItem('hasSeenOnboarding', 'true'); navigation.replace('select-auth'); }, [], ); + // const [firebaseToken, setFirebseToken] = useState<{ + // fcmToken: string; + // deviceId: string; + // deviceName: string; + // } | null>(); + // const app = getApp(); + // const messaging = getMessaging(app); - if (!initialRoute || isSplashVisible || !currentAnimation) { - return ( - - - - ); - } + // const getDeviceData = async () => { + // try { + // const fcmToken = await getToken(messaging); + // return { + // fcmToken, + // deviceId: await DeviceInfo.getUniqueId(), + // deviceName: await DeviceInfo.getDeviceName(), + // }; + // } catch (e) { + // console.log('Xato:', e); + // return null; + // } + // }; + // console.log(firebaseToken); + + // useEffect(() => { + // getDeviceData().then(data => { + // setFirebseToken(data); + // }); + // }, []); + + if (!initialRoute) return null; return ( @@ -183,7 +242,7 @@ export default function App() { + {/* {Platform.OS === 'android' && ( + + )} */} @@ -229,6 +291,19 @@ export default function App() { + {/* Splash transition */} + {isSplashVisible && ( + + + + )} + diff --git a/package-lock.json b/package-lock.json index 73fbb41..07898a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "react-native-linear-gradient": "^2.8.3", "react-native-localize": "^3.5.1", "react-native-mask-input": "^1.2.3", - "react-native-mmkv": "^4.0.1", + "react-native-mmkv": "^2.2.4", "react-native-modal": "^14.0.0-rc.1", "react-native-nitro-modules": "^0.31.10", "react-native-push-notification": "^8.1.1", @@ -13146,14 +13146,13 @@ } }, "node_modules/react-native-mmkv": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.0.1.tgz", - "integrity": "sha512-0JjO0U33b2hngFACsGwxoMCOZlCChP6R42aqvU85kXBaxY/kltSYr0FW9T6lkU3uEkE4IWMV1eLjoJplEY920w==", - "license": "MIT", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz", + "integrity": "sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg==", + "license": "(MIT AND BSD-3-Clause)", "peerDependencies": { "react": "*", - "react-native": "*", - "react-native-nitro-modules": "*" + "react-native": ">=0.71.0" } }, "node_modules/react-native-modal": { diff --git a/package.json b/package.json index c196b14..36d3945 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "react-native-linear-gradient": "^2.8.3", "react-native-localize": "^3.5.1", "react-native-mask-input": "^1.2.3", - "react-native-mmkv": "^4.0.1", + "react-native-mmkv": "^2.2.4", "react-native-modal": "^14.0.0-rc.1", "react-native-nitro-modules": "^0.31.10", "react-native-push-notification": "^8.1.1", diff --git a/src/api/axios.ts b/src/api/axios.ts index f1acc08..60c9f9e 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -1,8 +1,6 @@ -// axiosInstance -import axios from 'axios'; -import { authEvents, getLanguage, getToken, setToken } from 'helpers/event'; - -let isLoggingOut = false; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import axios, { AxiosError } from 'axios'; +import { navigate } from 'components/NavigationRef'; const axiosInstance = axios.create({ baseURL: 'https://api.cpcargo.uz/api/v1', @@ -12,38 +10,30 @@ const axiosInstance = axios.create({ }, }); -axiosInstance.interceptors.request.use(config => { - const token = getToken(); - const lang = getLanguage(); - +axiosInstance.interceptors.request.use(async config => { + // Tokenni olish + const token = await AsyncStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } - if (lang) { - config.headers['Accept-Language'] = lang; + // Language’ni olish + const language = await AsyncStorage.getItem('language'); + if (language) { + config.headers['Accept-Language'] = language; } return config; }); axiosInstance.interceptors.response.use( - res => res, - err => { - const status = err.response?.status; - - if (status === 401 && !isLoggingOut) { - isLoggingOut = true; - - setToken(null); - authEvents.emit('logout'); - - setTimeout(() => { - isLoggingOut = false; - }, 500); + response => response, + async (error: AxiosError) => { + if (error.response?.status === 401) { + await AsyncStorage.removeItem('token'); + navigate('Login'); } - - return Promise.reject(err); + return Promise.reject(error); }, ); diff --git a/src/helpers/event.ts b/src/helpers/event.ts index cb46ada..e69de29 100644 --- a/src/helpers/event.ts +++ b/src/helpers/event.ts @@ -1,57 +0,0 @@ -// helpers/event.ts -import { EventEmitter } from 'events'; -import { createMMKV } from 'react-native-mmkv'; - -export const authEvents = new EventEmitter(); - -export const enum AUTH { - USER = 'user', - AUTH = 'auth', -} - -// MMKV instance -export const storage = createMMKV(); - -// Memory cache -let tokenCache: string | null = null; -let languageCache: string | null = null; - -// Load on app start (synchronous) -export function loadInitialAuthData() { - tokenCache = storage.getString('token') || null; - languageCache = storage.getString('language') || null; -} - -export function getToken() { - return storage.getString('token'); -} - -export function setToken(token: string | null) { - tokenCache = token; - if (token) { - storage.set('token', token); - } else { - storage.remove('token'); - } -} - -export function saveAuth() { - storage.set('user', AUTH.USER); -} - -export function removeAuth() { - storage.set('user', AUTH.AUTH); -} - -export function getAuth() { - storage.getString('user'); -} - -export function getLanguage() { - return languageCache; -} - -export function setLanguage(lang: string) { - languageCache = lang; - storage.set('language', lang); -} diff --git a/src/screens/auth/login/ui/Confirm.tsx b/src/screens/auth/login/ui/Confirm.tsx index 9d06dfc..c633858 100644 --- a/src/screens/auth/login/ui/Confirm.tsx +++ b/src/screens/auth/login/ui/Confirm.tsx @@ -1,3 +1,4 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useMutation } from '@tanstack/react-query'; @@ -5,7 +6,6 @@ import { authApi } from 'api/auth'; import { otpPayload, resendPayload } from 'api/auth/type'; import AppText from 'components/AppText'; import ErrorNotification from 'components/ErrorNotification'; -import { saveAuth, setToken } from 'helpers/event'; import formatPhone from 'helpers/formatPhone'; import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -77,12 +77,8 @@ const Confirm = () => { const { mutate, isPending } = useMutation({ mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload), onSuccess: async res => { - setToken(res.data.accessToken); - saveAuth(); - navigation.reset({ - index: 0, - routes: [{ name: 'Home' }], // login sahifasiga qaytarish - }); + await AsyncStorage.setItem('token', res.data.accessToken); + navigation.navigate('Home'); setVisible(false); }, onError: (err: any) => { diff --git a/src/screens/auth/registeration/ui/Confirm.tsx b/src/screens/auth/registeration/ui/Confirm.tsx index 0273f30..dc54f74 100644 --- a/src/screens/auth/registeration/ui/Confirm.tsx +++ b/src/screens/auth/registeration/ui/Confirm.tsx @@ -1,3 +1,4 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; // import { getApp } from '@react-native-firebase/app'; // import { getMessaging, getToken } from '@react-native-firebase/messaging'; import { useNavigation } from '@react-navigation/native'; @@ -7,7 +8,6 @@ import { authApi } from 'api/auth'; import { otpPayload, resendPayload } from 'api/auth/type'; import AppText from 'components/AppText'; import ErrorNotification from 'components/ErrorNotification'; -import { setToken } from 'helpers/event'; import formatPhone from 'helpers/formatPhone'; import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -83,11 +83,8 @@ const Confirm = ({ const { mutate, isPending } = useMutation({ mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload), onSuccess: async res => { - setToken(res.data.accessToken); - navigation.reset({ - index: 0, - routes: [{ name: 'Home' }], // login sahifasiga qaytarish - }); + await AsyncStorage.setItem('token', res.data.accessToken); + navigation.navigate('Confirm'); setErrorConfirm(null); }, onError: (err: any) => { diff --git a/src/screens/profile/myProfile/ui/ProfilePages.tsx b/src/screens/profile/myProfile/ui/ProfilePages.tsx index 9ea52e2..c039f74 100644 --- a/src/screens/profile/myProfile/ui/ProfilePages.tsx +++ b/src/screens/profile/myProfile/ui/ProfilePages.tsx @@ -1,7 +1,7 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import AppText from 'components/AppText'; -import { removeAuth, setToken } from 'helpers/event'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -36,8 +36,7 @@ const ProfilePages = (props: componentNameProps) => { style: 'destructive', onPress: async () => { try { - setToken(''); - removeAuth(); + await AsyncStorage.removeItem('token'); navigation.reset({ index: 0, routes: [{ name: 'Login' }], // login sahifasiga qaytarish diff --git a/src/screens/welcome/Onboarding.tsx b/src/screens/welcome/Onboarding.tsx index b2399f3..f20b0d9 100644 --- a/src/screens/welcome/Onboarding.tsx +++ b/src/screens/welcome/Onboarding.tsx @@ -1,5 +1,5 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import AnimatedScreen from 'components/AnimatedScreen'; -import { getLanguage, storage } from 'helpers/event'; import { useCallback, useEffect, useMemo, useState } from 'react'; import FirstStep from 'screens/welcome/FirstStep'; import SecondStep from 'screens/welcome/SecondStep'; @@ -17,7 +17,7 @@ const Onboarding = ({ onFinish }: OnboardingProps) => { useEffect(() => { const loadLang = async () => { - const savedLang = getLanguage(); + const savedLang = await AsyncStorage.getItem('selectedLanguage'); if (savedLang === 'ru' || savedLang === 'uz') { setLang(savedLang); setStep(1); @@ -30,7 +30,7 @@ const Onboarding = ({ onFinish }: OnboardingProps) => { if (step < 3) { setStep(step + 1); } else { - storage.set('hasSeenOnboarding', 'true'); + await AsyncStorage.setItem('hasSeenOnboarding', 'true'); onFinish(); } }, [step, onFinish]);