diff --git a/app.json b/app.json index 93344ef..0829d11 100644 --- a/app.json +++ b/app.json @@ -15,7 +15,7 @@ ], "name": "Assemblr", "slug": "Assemblr", - "version": "1.1.7", + "version": "1.3.4", "orientation": "portrait", "icon": "./src/assets/icon.png", "userInterfaceStyle": "light", diff --git a/package.json b/package.json index c973af9..f694795 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.72.6", + "react-native-animatable": "^1.4.0", "react-native-gesture-handler": "~2.12.0", "react-native-image-zoom-viewer": "^3.0.1", "react-native-keyevent": "^0.3.1", @@ -58,7 +59,8 @@ "react-native-webview": "13.2.2", "react-redux": "^8.1.2", "redux": "^4.2.1", - "rn-openapp": "^2.1.2" + "rn-openapp": "^2.1.2", + "expo-av": "~13.4.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/App.tsx b/src/App.tsx index 6a0732d..4a7287e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,18 @@ -import {StyleSheet, Text, View} from 'react-native'; +import {StyleSheet} from 'react-native'; import {Provider} from "react-redux"; - -import {useFonts} from 'expo-font'; import {store} from "./redux/store"; import React from "react"; import CommonPage from "./screens/CommonPage/CommonPage"; import {BottomSheetModalProvider} from "@gorhom/bottom-sheet"; import {GestureHandlerRootView} from "react-native-gesture-handler"; -import Toast from "react-native-toast-message"; -import Constants from "expo-constants"; export default function App() { - let [fontsLoading] = useFonts({ - // 'SF Pro Text': require('./assets/fonts/SF-Pro-Text-Regular.otf') - }) - if (!fontsLoading) - return Loading... return ( - - + diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index 20ad3a9..cba44b6 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -1,33 +1,10 @@ -import axios, {AxiosHeaders, AxiosRequestConfig, AxiosRequestHeaders, InternalAxiosRequestConfig} from 'axios'; -import * as SecureStore from 'expo-secure-store'; -import {useDispatch} from "react-redux"; -import {logout} from "../features/auth/authSlice"; -import {store} from "../redux/store"; +import axios from 'axios'; + export const baseUrl = 'https://assemblr.denco.store'; // export const baseUrl = 'http://192.168.1.101:5000'; const apiClient = axios.create({ baseURL: baseUrl }); -apiClient.interceptors.request.use(async (config) => { - const accessToken = await SecureStore.getItemAsync('accessToken'); - if (!config.headers) { - config.headers = new AxiosHeaders(); - } - if (accessToken) { - config.headers.set('Authorization', `Bearer ${accessToken}`, true); - } - config.validateStatus = (status) => { - if (status == 401) { - SecureStore.deleteItemAsync('accessToken'); - store.dispatch(logout()); - } - return true; - }; - - return config; -}, function (error) { -}); - export default apiClient; diff --git a/src/api/assemblyApi.ts b/src/api/assemblyApi.ts index 4ce3db3..d3cf8db 100644 --- a/src/api/assemblyApi.ts +++ b/src/api/assemblyApi.ts @@ -15,7 +15,7 @@ const assemblyApi = { let response = await apiClient.post(`${router}/create`, {orderId}); return response.data; }, - close: async (assemblyId: number): Promise<{ ok: boolean, message: string }> => { + close: async (assemblyId: number): Promise<{ ok: boolean, message: string, reward: number }> => { let response = await apiClient.post(`${router}/close`, {assemblyId}); return response.data; }, diff --git a/src/api/balanceApi.ts b/src/api/balanceApi.ts index 2cbc4b5..cb67fbb 100644 --- a/src/api/balanceApi.ts +++ b/src/api/balanceApi.ts @@ -1,4 +1,4 @@ -import {BalanceTransaction} from "../types/balance"; +import {BalanceInfo, BalanceTransaction} from "../types/balance"; import apiClient from "./apiClient"; const router = '/balance'; @@ -6,6 +6,10 @@ const balanceApi = { getTransactions: async (page: number): Promise<{ balanceTransactions: BalanceTransaction[] }> => { const response = await apiClient.get(`${router}/transactions`, {params: {page}}); return response.data; + }, + getBalanceInfo: async (): Promise => { + const response = await apiClient.get(`${router}/info`); + return response.data; } } diff --git a/src/assets/icons/reward.png b/src/assets/icons/reward.png new file mode 100644 index 0000000..72958ba Binary files /dev/null and b/src/assets/icons/reward.png differ diff --git a/src/components/Animations/AnimationsView.tsx b/src/components/Animations/AnimationsView.tsx new file mode 100644 index 0000000..ce04a5e --- /dev/null +++ b/src/components/Animations/AnimationsView.tsx @@ -0,0 +1,93 @@ +import {FC, useEffect, useState} from "react"; +import {StyleSheet, View, Image} from "react-native"; +import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions"; +import * as Animatable from 'react-native-animatable'; +import DText from "../DText/DText"; +import {useSelector} from "react-redux"; +import {RootState, useAppDispatch} from "../../redux/store"; +import {hideReward} from "../../features/animations/animationsSlice"; + +// const width = responsiveWidth(20) +const AnimationsView: FC = () => { + const dispatch = useAppDispatch(); + const state = useSelector((state: RootState) => state.animations); + const [currentAnimation, setCurrentAnimation] = useState('bounceInRight'); + const [animationStage, setAnimationStage] = useState('in'); // Управляет текущей стадией анимации + + useEffect(() => { + // После завершения анимации входа, начинаем анимацию выхода + if (animationStage === 'out') { + setCurrentAnimation('bounceOutRight'); + } + }, [animationStage]); + + const onAnimationEnd = () => { + if (animationStage === 'in') { + // После завершения анимации входа, переходим к анимации выхода + setAnimationStage('out'); + } else if (animationStage === "out") { + dispatch(hideReward()); + } + }; + + const startAnimation = () => { + setCurrentAnimation("bounceInRight"); + setAnimationStage('in'); + } + + useEffect(() => { + if (state.isRewardAnimationVisible) + startAnimation(); + + }, [state.isRewardAnimationVisible]); + + return ( + + + + + + +{state.rewardAmount.toLocaleString('ru-RU')}₽ + + + + ) +} + +const style = StyleSheet.create({ + container: { + position: "absolute", + right: 0, + top: 0, + marginTop: responsiveHeight(10), + + } +}) +export default AnimationsView; \ No newline at end of file diff --git a/src/components/DTitle/DTitle.tsx b/src/components/DTitle/DTitle.tsx index 299c3d5..86b536b 100644 --- a/src/components/DTitle/DTitle.tsx +++ b/src/components/DTitle/DTitle.tsx @@ -1,11 +1,10 @@ -import React, {FC, ReactElement} from "react"; -import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native'; -import {RFPercentage} from "react-native-responsive-fontsize"; +import React, {FC, ReactNode} from "react"; +import {StyleProp, StyleSheet, TextStyle} from 'react-native'; import DText from "../DText/DText"; -import {responsiveScreenFontSize, responsiveWidth} from "react-native-responsive-dimensions"; +import {responsiveScreenFontSize} from "react-native-responsive-dimensions"; type Props = { - children: string; + children: ReactNode; style?: StyleProp; } const DTitle: FC = ({children, style}) => { diff --git a/src/contexts/apiContext.tsx b/src/contexts/apiContext.tsx index 17b85a8..0d2f3a6 100644 --- a/src/contexts/apiContext.tsx +++ b/src/contexts/apiContext.tsx @@ -5,7 +5,7 @@ type ApiState = { }; const apiContext = createContext(undefined); -const useApiContext = () => { +export const useApiContext = () => { const context = useContext(apiContext); if (!context) throw new Error('useApiContext must be used within ApiContextProvider'); return context; diff --git a/src/features/animations/animationsSlice.ts b/src/features/animations/animationsSlice.ts new file mode 100644 index 0000000..2b0e029 --- /dev/null +++ b/src/features/animations/animationsSlice.ts @@ -0,0 +1,29 @@ +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; + +interface AnimationsSlice { + isRewardAnimationVisible: boolean; + rewardAmount: number; +} + +const initialState: AnimationsSlice = { + isRewardAnimationVisible: false, + rewardAmount: 0 +} + +export const animationsSlice = createSlice({ + name: 'animations', + initialState, + reducers: { + showReward: (state, action: PayloadAction<{ reward: number }>) => { + state.rewardAmount = action.payload.reward; + state.isRewardAnimationVisible = true; + }, + hideReward: (state) => { + state.isRewardAnimationVisible = false; + state.rewardAmount = 0; + } + } +}) + +export const {showReward, hideReward} = animationsSlice.actions; +export default animationsSlice.reducer; \ No newline at end of file diff --git a/src/features/balance/balanceSlice.ts b/src/features/balance/balanceSlice.ts index bd10df2..d00c621 100644 --- a/src/features/balance/balanceSlice.ts +++ b/src/features/balance/balanceSlice.ts @@ -1,36 +1,71 @@ -import {BalanceTransaction} from "../../types/balance"; -import {createAsyncThunk, createSlice, PayloadAction} from "@reduxjs/toolkit"; -import {RootState} from "../../redux/store"; +import {BalanceInfo, BalanceTransaction} from "../../types/balance"; +import {createAsyncThunk, createSlice} from "@reduxjs/toolkit"; +import balanceApi from "../../api/balanceApi"; const name = 'balance'; +interface TransactionsState { + items: BalanceTransaction[]; + isLoading: boolean; + currentPage: number; + hasNext: boolean; +} + interface BalanceState { - transactions: BalanceTransaction[]; balance: number; - page: number; - loading: boolean; + transactions: TransactionsState; } +const transactionsInitialState: TransactionsState = { + currentPage: 1, + hasNext: true, + isLoading: false, + items: [] +} const initialState: BalanceState = { - transactions: [], balance: 0, - page: 1, - loading: false + transactions: transactionsInitialState } + +export const fetchTransactions = createAsyncThunk( + `${name}/fetchTransactions`, + async (page: number, _): Promise => { + const response = await balanceApi.getTransactions(page); + return response.balanceTransactions; + } +) + +export const fetchBalance = createAsyncThunk( + `${name}/fetchBalance`, + async (_): Promise => { + return await balanceApi.getBalanceInfo(); + } +) export const balanceSlice = createSlice({ name: name, initialState, reducers: { - appendTransactions: (state, payload: PayloadAction) => { - state.transactions.push(...payload.payload); - state.page = state.page + 1; - state.loading = false - }, - setIsLoading: (state, action: PayloadAction) => { - state.loading = action.payload; + refreshTransactions: (state) => { + state.transactions = transactionsInitialState; } }, + extraReducers: (builder) => { + builder.addCase(fetchTransactions.pending, (state, action) => { + state.transactions.isLoading = true; + }) + builder.addCase(fetchTransactions.fulfilled, (state, action) => { + state.transactions.isLoading = false; + state.transactions.hasNext = action.payload.length > 0; + state.transactions.items = [...state.transactions.items, ...action.payload]; + state.transactions.currentPage = state.transactions.currentPage + 1; + }) + builder.addCase(fetchBalance.fulfilled, (state, action) => { + state.balance = action.payload.balance; + }) + } }) -export const {appendTransactions, setIsLoading} = balanceSlice.actions; + +export const {refreshTransactions} = balanceSlice.actions; + export default balanceSlice.reducer; \ No newline at end of file diff --git a/src/features/balance/service.ts b/src/features/balance/service.ts deleted file mode 100644 index 260c5a8..0000000 --- a/src/features/balance/service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {createApi} from '@reduxjs/toolkit/query/react' - - -import {axiosBaseQuery} from "../../redux/general"; -import {EndpointBuilder} from "@reduxjs/toolkit/dist/query/endpointDefinitions"; -import {BalanceTransaction} from "../../types/balance"; - -export const balanceApi = createApi({ - reducerPath: 'balanceApi', - baseQuery: axiosBaseQuery(), - endpoints: (builder) => ({ - getTransactions: builder.query({query: () => ({url: "/transactions", method: 'get'})}) - }) -}) -export const {useGetTransactionsQuery} = balanceApi; \ No newline at end of file diff --git a/src/redux/store.ts b/src/redux/store.ts index 074aacd..6dd1c6b 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -12,6 +12,8 @@ import ordersFilterReducer from 'features/ordersFilter/ordersFilterSlice'; import shippingWarehouseSelectReducer from 'features/shippingWarehouseSelect/shippingWarehouseSelectSlice'; import citySelectReducer from 'features/citySelect/citySelectSlice'; import cancelAssemblyModal from 'features/cancelAssemblyModal/cancelAssemblyModalSlice'; +import balanceReducer from 'features/balance/balanceSlice'; +import animationsReducer from 'features/animations/animationsSlice'; import {useDispatch} from "react-redux"; export const store = configureStore({ @@ -27,7 +29,9 @@ export const store = configureStore({ ordersFilter: ordersFilterReducer, shippingWarehouseSelect: shippingWarehouseSelectReducer, citySelect: citySelectReducer, - cancelAssemblyModal: cancelAssemblyModal + cancelAssemblyModal: cancelAssemblyModal, + balance: balanceReducer, + animations: animationsReducer }, }); diff --git a/src/screens/CommonPage/CommonPage.tsx b/src/screens/CommonPage/CommonPage.tsx index be0f491..ac6665b 100644 --- a/src/screens/CommonPage/CommonPage.tsx +++ b/src/screens/CommonPage/CommonPage.tsx @@ -5,7 +5,7 @@ import React, {useEffect} from "react"; import {background} from "../../css/colors"; import {useDispatch, useSelector} from "react-redux"; import {RootState} from "../../redux/store"; -import {login} from "../../features/auth/authSlice"; +import {login, logout} from "../../features/auth/authSlice"; import * as SecureStore from 'expo-secure-store'; import Toast from "react-native-toast-message"; import toastConfig from "../../components/Toast/Toast"; @@ -21,9 +21,7 @@ import {ASSEMBLY_STATE} from "../../types/assembly"; import Constants from "expo-constants"; import * as FileSystem from 'expo-file-system'; import applicationApi from "../../api/applicationApi"; -import {ActivityAction, startActivityAsync} from "expo-intent-launcher"; -import {RenderTargetOptions} from "@shopify/flash-list"; -import KeyEvent from 'react-native-keyevent'; +import {startActivityAsync} from "expo-intent-launcher"; import { openLoadingModal, setIndeterminate, @@ -31,8 +29,12 @@ import { setProgress } from "../../features/loadingModal/loadingModalSlice"; import CancelAssemblyModal from "../../components/Modals/CancelAssemblyModal/CancelAssemblyModal"; +import {AxiosHeaders} from "axios"; +import apiClient from "../../api/apiClient"; +import AnimationsView from "../../components/Animations/AnimationsView"; function CommonPage() { + const dim = useSelector((state: RootState) => state.interface.dim); const isAuthorized = useSelector((state: RootState) => state.auth.isAuthorized); @@ -85,13 +87,42 @@ function CommonPage() { }) }); } + + + const configApi = () => { + + apiClient.interceptors.request.use(async (config) => { + const accessToken = await SecureStore.getItemAsync('accessToken'); + if (!config.headers) { + config.headers = new AxiosHeaders(); + } + if (accessToken) { + config.headers.set('Authorization', `Bearer ${accessToken}`, true); + } + config.validateStatus = (status) => { + if (status == 401) { + SecureStore.deleteItemAsync('accessToken'); + dispatch(logout()); + } + return true; + }; + + return config; + }, function (error) { + }); + } + useEffect(() => { + configApi(); checkUpdates(); + loadSettings(); }, []); return ( + {isAuthorized ? : } + diff --git a/src/screens/MainScreen/MainScreen.tsx b/src/screens/MainScreen/MainScreen.tsx index 56f13ad..3182c0c 100644 --- a/src/screens/MainScreen/MainScreen.tsx +++ b/src/screens/MainScreen/MainScreen.tsx @@ -1,4 +1,4 @@ -import {StyleSheet, Text, View, ImageBackground, Image} from 'react-native'; +import {StyleSheet, Image} from 'react-native'; import {createBottomTabNavigator} from "@react-navigation/bottom-tabs"; import BarcodeScreen from "../BarcodeScreen/BarcodeScreen"; import {DefaultTheme, NavigationContainer} from "@react-navigation/native"; @@ -6,10 +6,10 @@ import moneyScreen from "../MoneyScreen/MoneyScreen"; import profileScreen from "../ProfileScreen/ProfileScreen"; import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions"; import {RFPercentage} from "react-native-responsive-fontsize"; -import OrderScreen, {OrderScreenController} from "../OrderScreen/OrderScreen"; +import {OrderScreenController} from "../OrderScreen/OrderScreen"; import OrdersScreen from "../OrdersScreen/OrdersScreen"; import {background} from "../../css/colors"; -import * as fs from "fs"; + export type TabNavigatorParamList = { Home: undefined; @@ -30,16 +30,16 @@ const CustomTab = ({name, component, icon, hidden}: CustomTabProps) => ({ name, component, options: { - tabBarIcon: ({focused, color, size}: { focused: boolean; color: string; size: number }) => ( - { + return ( - ), + />) + }, tabBarLabel: () => null, } }); @@ -90,6 +90,7 @@ function MainScreen() { }, }}> + = ({order}) => { const navigator = useNavigation>(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const assembly = useSelector((state: RootState) => state.assembly.assembly); const assemblyState = useSelector((state: RootState) => state.assembly.localState); const initialOrder = useSelector((state: RootState) => state.assembly.initialOrder); @@ -144,12 +146,19 @@ const OrderScreen: FC = ({order}) => { label={"Завершить сборку"} onPress={() => { if (!assembly) return; - assemblyApi.close(assembly.databaseId).then(({ok, message}) => { + assemblyApi.close(assembly.databaseId).then(({ok, message, reward}) => { Toast.show({ type: ok ? 'success' : 'error', text1: 'Завершение сборки', text2: message }); + if (ok) { + dispatch(showReward({reward})); + dispatch(fetchBalance()) + dispatch(refreshTransactions()) + + } + dispatch(endAssembly()); navigator.navigate('Barcode'); }) diff --git a/src/screens/ProfileScreen/ProfileScreen.tsx b/src/screens/ProfileScreen/ProfileScreen.tsx index 490ce6e..9c6f5c9 100644 --- a/src/screens/ProfileScreen/ProfileScreen.tsx +++ b/src/screens/ProfileScreen/ProfileScreen.tsx @@ -1,130 +1,54 @@ -import {View, StyleSheet, TouchableOpacity, Image, ScrollView, GestureResponderEvent} from "react-native"; -import {responsiveFontSize, responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions"; -import {blue, gray} from "../../css/colors"; +import {View, StyleSheet} from "react-native"; +import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions"; +import {blue} from "../../css/colors"; import {RFPercentage} from "react-native-responsive-fontsize"; -import DText from "../../components/DText/DText"; import DTitle from "../../components/DTitle/DTitle"; import Separator from "../../components/Separator/Separator"; -import {BottomSheetModal} from "@gorhom/bottom-sheet"; -import {useMemo, useRef, useState} from "react"; -import assemblyApi from "../../api/assemblyApi"; -import Toast from "react-native-toast-message"; -import {useDispatch} from "react-redux"; -import {reset} from "../../features/assembly/assemblySlice"; +import {useEffect} from "react"; +import {useSelector} from "react-redux"; +import SettingsView from "./SettingsView"; +import TransactionsView from "./TransactionView"; +import {RootState, useAppDispatch} from "../../redux/store"; +import {fetchBalance, fetchTransactions, refreshTransactions} from "../../features/balance/balanceSlice"; +import {formatBalanceNumber} from "../../utils/formatters"; -type SettingsElementProps = { - icon: any; - title: string; - onPress?: (event: GestureResponderEvent) => void - -} -const SettingsElement: React.FC = ({icon, title, onPress}) => { - return ( - - - - - - - {title} - - - - ) -} -type HistoryElementProps = { - cost: number; - description: string; - color: string; -} -const HistoryElement: React.FC = ({cost, description, color}) => { - const formatNumber = (n: number): string => n >= 0 ? `+${n}` : `${n}`; - return ( - - {formatNumber(cost)} руб - {description} - - ) -} - function ProfileScreen() { - - const bottomSheetModalRef = useRef(null); - const snapPoints = useMemo(() => ['25%', '40%'], []); - const [modalVisible, setModalVisible] = useState(false); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); + const balanceState = useSelector((state: RootState) => state.balance); + const onTransactionsEndReached = () => { + if (balanceState.transactions.isLoading || !balanceState.transactions.hasNext) return; + dispatch(fetchTransactions(balanceState.transactions.currentPage)); + }; + const onTransactionsRefresh = () => { + dispatch(refreshTransactions()); + } + useEffect(() => { + dispatch(fetchBalance()); + }, []); return ( - Ваш баланс: 228 руб - Собрано товаров: 1337 шт + Ваш + баланс: {formatBalanceNumber(balanceState.balance, false)} руб - - - - - { - assemblyApi.cancel().then(response => { - - Toast.show({ - type: response.ok ? "success" : "error", - text1: "Отмена сборки", - text2: response.message, - }); - dispatch(reset()); - }) - }} icon={require('assets/icons/settings/close.png')} title={'Отменить сборку'}/> - + История операций - - - - - - - - - - - - - + - { - setModalVisible(false); - }} - > - - хуй - - - - - ) } @@ -151,7 +75,8 @@ const styles = StyleSheet.create({ historyContainer: { paddingVertical: responsiveHeight(2), flex: 1, - // backgroundColor: "red" + gap: responsiveHeight(1), + marginBottom: responsiveHeight(6) }, historyElements: { marginTop: responsiveHeight(3), diff --git a/src/screens/ProfileScreen/SettingsView.tsx b/src/screens/ProfileScreen/SettingsView.tsx index 919f6b6..28fd0a6 100644 --- a/src/screens/ProfileScreen/SettingsView.tsx +++ b/src/screens/ProfileScreen/SettingsView.tsx @@ -22,7 +22,7 @@ const SettingsElement: FC = ({icon, title, onPress}) => { - {title} + {title} @@ -59,10 +59,12 @@ const styles = StyleSheet.create({ actionsCarouselElementContainer: { display: "flex", alignItems: "center", + maxWidth: RFPercentage(10), }, actionsCarouselImageWrapper: { width: RFPercentage(10), height: RFPercentage(10), + backgroundColor: "white", borderRadius: 100, borderWidth: RFPercentage(0.3), diff --git a/src/screens/ProfileScreen/TransactionView.tsx b/src/screens/ProfileScreen/TransactionView.tsx index 1480600..89885cc 100644 --- a/src/screens/ProfileScreen/TransactionView.tsx +++ b/src/screens/ProfileScreen/TransactionView.tsx @@ -7,18 +7,19 @@ import {FC} from "react"; import {RFPercentage} from "react-native-responsive-fontsize"; import {FlashList} from "@shopify/flash-list"; import flashListSeparator from "../../components/FlashListSeparator/FlashListSeparator"; +import {formatBalanceNumber} from "../../utils/formatters"; type TransactionElementProps = { transaction: BalanceTransaction; } const TransactionElement: FC = ({transaction}) => { - const formatNumber = (n: number): string => n >= 0 ? `+${n}` : `${n}`; + return ( {formatNumber(transaction.amount)} руб + }}>{formatBalanceNumber(transaction.amount)} руб = ({transaction}) => { type TransactionsViewProps = { transactions: BalanceTransaction[]; onEndReached: () => void; + onRefresh: () => void; + isLoading: boolean; } -const TransactionsView: FC = ({transactions, onEndReached}) => { - console.log('----------------------------------') - console.log(JSON.stringify(transactions, null, 2)) +const TransactionsView: FC = ({transactions, isLoading, onEndReached, onRefresh}) => { return ( = ({transactions, onEndReached transaction={item.item} />} onEndReached={onEndReached} + refreshing={isLoading} > ) diff --git a/src/types/balance.ts b/src/types/balance.ts index b49a11c..3569df5 100644 --- a/src/types/balance.ts +++ b/src/types/balance.ts @@ -11,4 +11,8 @@ export type BalanceTransaction = { description: string; jsonData?: object; createdAt: string; +} + +export type BalanceInfo = { + balance: number; } \ No newline at end of file diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts new file mode 100644 index 0000000..f908c02 --- /dev/null +++ b/src/utils/formatters.ts @@ -0,0 +1,8 @@ +export const formatBalanceNumber = (n: number, addSign = true) => { + const roundNumber = (n: number, digits: number): number => parseInt(n.toFixed(digits)) + const formatNumber = (n: number): string => n + >= 0 ? `${addSign ? '+' : ''}${roundNumber(n, 2).toLocaleString('ru-RU')}` : + `${roundNumber(n, 2).toLocaleString('ru-RU')}`; + + return formatNumber(n); +} \ No newline at end of file