mmkv added

This commit is contained in:
Samandar Turgunboyev
2025-12-04 15:39:48 +05:00
parent 684d09e6b5
commit 87bd8cdea6
15 changed files with 1470 additions and 1450 deletions

208
App.tsx
View File

@@ -1,5 +1,4 @@
// App.tsx // App.tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -20,6 +19,13 @@ import Toast from 'react-native-toast-message';
import SplashScreen from './src/components/SplashScreen'; import SplashScreen from './src/components/SplashScreen';
// Screens // Screens
import {
authEvents,
getLanguage,
getToken,
loadInitialAuthData,
storage,
} from 'helpers/event';
import Login from 'screens/auth/login/ui'; import Login from 'screens/auth/login/ui';
import Confirm from 'screens/auth/login/ui/Confirm'; import Confirm from 'screens/auth/login/ui/Confirm';
import Register from 'screens/auth/registeration/ui'; import Register from 'screens/auth/registeration/ui';
@@ -47,137 +53,80 @@ import PaymentQrCode from 'screens/wallet/successPayment/ui/PaymentQrCode';
import Onboarding from 'screens/welcome/Onboarding'; import Onboarding from 'screens/welcome/Onboarding';
LogBox.ignoreLogs([ LogBox.ignoreLogs([
'Non-serializable values were found in the navigation state',
'ViewPropTypes will be removed', '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 Stack = createNativeStackNavigator();
const screenWidth = Dimensions.get('window').width; const screenWidth = Dimensions.get('window').width;
const queryClient = new QueryClient(); 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 yoq',
// 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 yoq',
// 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() { export default function App() {
const [initialRoute, setInitialRoute] = useState<string | null>(null); const [initialRoute, setInitialRoute] = useState<string | null>(null);
const slideAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(0)).current;
const [isSplashVisible, setIsSplashVisible] = useState(true); const [isSplashVisible, setIsSplashVisible] = useState(true);
const isMounted = useRef(false);
const currentAnimation = useRef<Animated.CompositeAnimation | null>(null);
// useEffect(() => { useEffect(() => {
// requestNotificationPermission(); loadInitialAuthData();
}, []);
// const messagingInstance = getMessaging(); useEffect(() => {
const logoutListener = () => {
if (navigationRef.isReady()) {
navigationRef.reset({
index: 0,
routes: [{ name: 'Login' }],
});
}
};
// const unsubscribe = onMessage(messagingInstance, async remoteMessage => { authEvents.on('logout', logoutListener);
// console.log('Foreground message:', remoteMessage);
// await saveNotification(remoteMessage);
// await onDisplayNotification(remoteMessage);
// });
// const unsubscribeOpened = onNotificationOpenedApp( return () => {
// messagingInstance, authEvents.removeListener('logout', logoutListener);
// remoteMessage => { };
// console.log('Backgrounddan ochildi:', remoteMessage); }, []);
// saveNotification(remoteMessage);
// },
// );
// (async () => { useEffect(() => {
// const remoteMessage = await getInitialNotification(messagingInstance); isMounted.current = true;
// if (remoteMessage) { return () => {
// console.log('Killeddan ochildi:', remoteMessage); isMounted.current = false;
// saveNotification(remoteMessage); if (currentAnimation.current) {
// } currentAnimation.current.stop();
// })(); }
};
// return () => { }, []);
// unsubscribe();
// unsubscribeOpened();
// };
// }, []);
useEffect(() => { useEffect(() => {
const initializeApp = async () => { const initializeApp = async () => {
try { try {
const [seen, token, lang] = await Promise.all([ const seen = storage.getString('hasSeenOnboarding');
AsyncStorage.getItem('hasSeenOnboarding'), const token = getToken();
AsyncStorage.getItem('token'), const lang = getLanguage();
AsyncStorage.getItem('language'), if (lang) {
]); try {
await i18n.changeLanguage(lang);
if (lang) await i18n.changeLanguage(lang); } catch (e) {
console.warn('i18n.changeLanguage failed:', e);
}
}
const initialRouteName = !seen const initialRouteName = !seen
? 'Onboarding' ? 'Onboarding'
: token : token
? 'Home' ? 'Home'
: 'select-auth'; : 'select-auth';
if (isMounted.current) {
setInitialRoute(initialRouteName); setInitialRoute(initialRouteName);
}
} catch (error) { } catch (error) {
console.error('App initialization error:', error); console.error('App initialization error:', error);
setInitialRoute('select-auth'); if (isMounted.current) setInitialRoute('select-auth');
} }
}; };
@@ -186,52 +135,39 @@ export default function App() {
const handleSplashFinish = useMemo( const handleSplashFinish = useMemo(
() => () => { () => () => {
Animated.timing(slideAnim, { // create animation and keep ref so we can stop it on unmount
const animation = Animated.timing(slideAnim, {
toValue: -screenWidth, toValue: -screenWidth,
duration: 600, duration: 600,
useNativeDriver: true, useNativeDriver: true,
}).start(() => setIsSplashVisible(false)); });
currentAnimation.current = animation;
animation.start(() => {
// ensure component still mounted before setting state
if (isMounted.current) {
setIsSplashVisible(false);
}
currentAnimation.current = null;
});
}, },
[slideAnim], [slideAnim],
); );
const handleOnboardingFinish = useMemo( const handleOnboardingFinish = useMemo(
() => async (navigation: any) => { () => async (navigation: any) => {
await AsyncStorage.setItem('hasSeenOnboarding', 'true'); try {
storage.set('hasSeenOnboarding', 'true');
} catch (e) {
console.warn('Failed to set hasSeenOnboarding', e);
}
navigation.replace('select-auth'); navigation.replace('select-auth');
}, },
[], [],
); );
// const [firebaseToken, setFirebseToken] = useState<{
// fcmToken: string;
// deviceId: string;
// deviceName: string;
// } | null>();
// const app = getApp();
// const messaging = getMessaging(app);
// const getDeviceData = async () => { if (initialRoute === null) {
// try { return <View style={{ flex: 1, backgroundColor: '#000' }} />;
// 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 ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@@ -245,6 +181,7 @@ export default function App() {
animation: 'none', animation: 'none',
gestureEnabled: false, gestureEnabled: false,
}} }}
// initialRouteName set once (we ensure it's available before first render)
initialRouteName={initialRoute} initialRouteName={initialRoute}
> >
<Stack.Screen name="Onboarding"> <Stack.Screen name="Onboarding">
@@ -273,9 +210,6 @@ export default function App() {
<Stack.Screen name="PaymentQrCode" component={PaymentQrCode} /> <Stack.Screen name="PaymentQrCode" component={PaymentQrCode} />
<Stack.Screen name="Profile" component={Profile} /> <Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} /> <Stack.Screen name="Settings" component={Settings} />
{/* {Platform.OS === 'android' && (
<Stack.Screen name="Notifications" component={Notifications} />
)} */}
<Stack.Screen name="Warehouses" component={Warehouses} /> <Stack.Screen name="Warehouses" component={Warehouses} />
<Stack.Screen name="Support" component={Support} /> <Stack.Screen name="Support" component={Support} />
<Stack.Screen name="ListBranches" component={ListBranches} /> <Stack.Screen name="ListBranches" component={ListBranches} />

View File

@@ -85,8 +85,8 @@ android {
applicationId "uz.felix.cpost" applicationId "uz.felix.cpost"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 6 versionCode 7
versionName "0.6" versionName "0.7"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,8 @@
module.exports = { module.exports = {
presets: ['module:@react-native/babel-preset'], presets: ['module:@react-native/babel-preset'],
plugins: [ plugins: [
'react-native-reanimated/plugin', // 'react-native-reanimated/plugin',
'react-native-worklets/plugin',
'@babel/plugin-transform-export-namespace-from', '@babel/plugin-transform-export-namespace-from',
[ [
'module-resolver', 'module-resolver',

362
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@
"@react-navigation/native-stack": "^7.3.25", "@react-navigation/native-stack": "^7.3.25",
"@tanstack/react-query": "^5.84.2", "@tanstack/react-query": "^5.84.2",
"axios": "^1.11.0", "axios": "^1.11.0",
"events": "^3.3.0",
"i18next": "^25.3.2", "i18next": "^25.3.2",
"lottie-react-native": "^7.3.0", "lottie-react-native": "^7.3.0",
"react": "19.1.0", "react": "19.1.0",
@@ -41,7 +42,9 @@
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-localize": "^3.5.1", "react-native-localize": "^3.5.1",
"react-native-mask-input": "^1.2.3", "react-native-mask-input": "^1.2.3",
"react-native-mmkv": "^4.0.1",
"react-native-modal": "^14.0.0-rc.1", "react-native-modal": "^14.0.0-rc.1",
"react-native-nitro-modules": "^0.31.10",
"react-native-push-notification": "^8.1.1", "react-native-push-notification": "^8.1.1",
"react-native-reanimated": "^4.0.2", "react-native-reanimated": "^4.0.2",
"react-native-safe-area-context": "^5.6.0", "react-native-safe-area-context": "^5.6.0",

View File

@@ -1,6 +1,8 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; // axiosInstance
import axios, { AxiosError } from 'axios'; import axios from 'axios';
import { navigate } from 'components/NavigationRef'; import { authEvents, getLanguage, getToken, setToken } from 'helpers/event';
let isLoggingOut = false;
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: 'https://api.cpcargo.uz/api/v1', baseURL: 'https://api.cpcargo.uz/api/v1',
@@ -10,30 +12,38 @@ const axiosInstance = axios.create({
}, },
}); });
axiosInstance.interceptors.request.use(async config => { axiosInstance.interceptors.request.use(config => {
// Tokenni olish const token = getToken();
const token = await AsyncStorage.getItem('token'); const lang = getLanguage();
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
// Languageni olish if (lang) {
const language = await AsyncStorage.getItem('language'); config.headers['Accept-Language'] = lang;
if (language) {
config.headers['Accept-Language'] = language;
} }
return config; return config;
}); });
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
response => response, res => res,
async (error: AxiosError) => { err => {
if (error.response?.status === 401) { const status = err.response?.status;
await AsyncStorage.removeItem('token');
navigate('Login'); if (status === 401 && !isLoggingOut) {
isLoggingOut = true;
setToken(null);
authEvents.emit('logout');
setTimeout(() => {
isLoggingOut = false;
}, 500);
} }
return Promise.reject(error);
return Promise.reject(err);
}, },
); );

39
src/helpers/event.ts Normal file
View File

@@ -0,0 +1,39 @@
// helpers/event.ts
import { EventEmitter } from 'events';
import { createMMKV } from 'react-native-mmkv';
export const authEvents = new EventEmitter();
// MMKV instance
export const storage = createMMKV();
// Memory cachez
let tokenCache: string | null = null;
let languageCache: string | null = null;
// Load on app start
export function loadInitialAuthData() {
tokenCache = storage.getString('token') || null;
languageCache = storage.getString('language') || null;
}
export function getToken() {
return tokenCache;
}
export function setToken(token: string | null) {
tokenCache = token;
if (token) {
storage.set('token', token);
} else {
storage.remove('token');
}
}
export function getLanguage() {
return languageCache;
}
export function setLanguage(lang: string) {
languageCache = lang;
storage.set('language', lang);
}

View File

@@ -1,4 +1,3 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
@@ -6,6 +5,7 @@ import { authApi } from 'api/auth';
import { otpPayload, resendPayload } from 'api/auth/type'; import { otpPayload, resendPayload } from 'api/auth/type';
import AppText from 'components/AppText'; import AppText from 'components/AppText';
import ErrorNotification from 'components/ErrorNotification'; import ErrorNotification from 'components/ErrorNotification';
import { setToken } from 'helpers/event';
import formatPhone from 'helpers/formatPhone'; import formatPhone from 'helpers/formatPhone';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -77,7 +77,7 @@ const Confirm = () => {
const { mutate, isPending } = useMutation({ const { mutate, isPending } = useMutation({
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload), mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
onSuccess: async res => { onSuccess: async res => {
await AsyncStorage.setItem('token', res.data.accessToken); setToken(res.data.accessToken);
navigation.navigate('Home'); navigation.navigate('Home');
setVisible(false); setVisible(false);
}, },

View File

@@ -1,4 +1,3 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
// import { getApp } from '@react-native-firebase/app'; // import { getApp } from '@react-native-firebase/app';
// import { getMessaging, getToken } from '@react-native-firebase/messaging'; // import { getMessaging, getToken } from '@react-native-firebase/messaging';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
@@ -8,6 +7,7 @@ import { authApi } from 'api/auth';
import { otpPayload, resendPayload } from 'api/auth/type'; import { otpPayload, resendPayload } from 'api/auth/type';
import AppText from 'components/AppText'; import AppText from 'components/AppText';
import ErrorNotification from 'components/ErrorNotification'; import ErrorNotification from 'components/ErrorNotification';
import { setToken } from 'helpers/event';
import formatPhone from 'helpers/formatPhone'; import formatPhone from 'helpers/formatPhone';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -83,7 +83,7 @@ const Confirm = ({
const { mutate, isPending } = useMutation({ const { mutate, isPending } = useMutation({
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload), mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
onSuccess: async res => { onSuccess: async res => {
await AsyncStorage.setItem('token', res.data.accessToken); setToken(res.data.accessToken);
navigation.navigate('Confirm'); navigation.navigate('Confirm');
setErrorConfirm(null); setErrorConfirm(null);
}, },

View File

@@ -54,7 +54,7 @@ const CargoPrices = (props: CargoPricesProps) => {
<View style={{ marginTop: 10, gap: 10, marginBottom: 20 }}> <View style={{ marginTop: 10, gap: 10, marginBottom: 20 }}>
{data && {data &&
data.map(ref => ( data.map(ref => (
<View style={styles.cardWhite}> <View style={styles.cardWhite} key={ref.id}>
<View style={styles.priceCard}> <View style={styles.priceCard}>
<AppText style={styles.titleBlack}>{ref.title}</AppText> <AppText style={styles.titleBlack}>{ref.title}</AppText>
<AppText style={[styles.titleBlack, { fontSize: 16 }]}> <AppText style={[styles.titleBlack, { fontSize: 16 }]}>
@@ -152,7 +152,7 @@ const CargoPrices = (props: CargoPricesProps) => {
<View style={{ marginTop: 20, gap: 10, marginBottom: 20 }}> <View style={{ marginTop: 20, gap: 10, marginBottom: 20 }}>
{data && {data &&
data.map(ref => ( data.map(ref => (
<View style={styles.cardWhite}> <View style={styles.cardWhite} key={ref.id}>
<View style={styles.priceCard}> <View style={styles.priceCard}>
<AppText style={styles.titleBlack}>{ref.title}</AppText> <AppText style={styles.titleBlack}>{ref.title}</AppText>
<AppText style={[styles.titleBlack, { fontSize: 16 }]}> <AppText style={[styles.titleBlack, { fontSize: 16 }]}>

View File

@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import AppText from 'components/AppText'; import AppText from 'components/AppText';
import { setToken } from 'helpers/event';
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@@ -36,7 +36,7 @@ const ProfilePages = (props: componentNameProps) => {
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
try { try {
await AsyncStorage.removeItem('token'); setToken(null);
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: 'Login' }], // login sahifasiga qaytarish routes: [{ name: 'Login' }], // login sahifasiga qaytarish

View File

@@ -1,4 +1,3 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import packetsApi from 'api/packets'; import packetsApi from 'api/packets';
import Layout from 'components/Layout'; import Layout from 'components/Layout';
@@ -15,7 +14,6 @@ import {
View, View,
useWindowDimensions, useWindowDimensions,
} from 'react-native'; } from 'react-native';
import Serach from 'svg/Serach';
import { DataInfo } from '../lib/data'; import { DataInfo } from '../lib/data';
import Filter from './Filter'; import Filter from './Filter';
import Order from './Order'; import Order from './Order';
@@ -61,9 +59,6 @@ const Status = () => {
}), }),
}); });
console.log(AsyncStorage.getItem('token'));
console.log('statusData', statusData);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const scaleAnim = React.useRef(new Animated.Value(0.8)).current; const scaleAnim = React.useRef(new Animated.Value(0.8)).current;
const opacityAnim = React.useRef(new Animated.Value(0)).current; const opacityAnim = React.useRef(new Animated.Value(0)).current;
@@ -140,11 +135,6 @@ const Status = () => {
[refreshing, isFetching, onRefresh], [refreshing, isFetching, onRefresh],
); );
const searchIcon = useMemo(
() => <Serach color="#D8DADC" width={20 * scale} height={20 * scale} />,
[scale],
);
if (isLoading || isFetching) { if (isLoading || isFetching) {
return ( return (
<Layout> <Layout>

View File

@@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import AnimatedScreen from 'components/AnimatedScreen'; import AnimatedScreen from 'components/AnimatedScreen';
import { getLanguage, storage } from 'helpers/event';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import FirstStep from 'screens/welcome/FirstStep'; import FirstStep from 'screens/welcome/FirstStep';
import SecondStep from 'screens/welcome/SecondStep'; import SecondStep from 'screens/welcome/SecondStep';
@@ -17,7 +17,7 @@ const Onboarding = ({ onFinish }: OnboardingProps) => {
useEffect(() => { useEffect(() => {
const loadLang = async () => { const loadLang = async () => {
const savedLang = await AsyncStorage.getItem('selectedLanguage'); const savedLang = getLanguage();
if (savedLang === 'ru' || savedLang === 'uz') { if (savedLang === 'ru' || savedLang === 'uz') {
setLang(savedLang); setLang(savedLang);
setStep(1); setStep(1);
@@ -30,7 +30,7 @@ const Onboarding = ({ onFinish }: OnboardingProps) => {
if (step < 3) { if (step < 3) {
setStep(step + 1); setStep(step + 1);
} else { } else {
await AsyncStorage.setItem('hasSeenOnboarding', 'true'); storage.set('hasSeenOnboarding', 'true');
onFinish(); onFinish();
} }
}, [step, onFinish]); }, [step, onFinish]);

View File

@@ -1,9 +1,9 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import { setLanguage } from 'helpers/event';
import i18n from 'i18n/i18n'; import i18n from 'i18n/i18n';
export const changeLanguage = async (lang: string) => { export const changeLanguage = async (lang: string) => {
try { try {
await AsyncStorage.setItem('language', lang); setLanguage(lang);
await i18n.changeLanguage(lang); await i18n.changeLanguage(lang);
} catch (error) {} } catch (error) {}
}; };