diff --git a/.prettierrc.js b/.prettierrc.js index 793702e5..66b4ac71 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,9 +1,10 @@ module.exports = { - bracketSpacing: false, - jsxBracketSameLine: true, - singleQuote: true, - trailingComma: 'all', - arrowParens: 'avoid', - tabWidth: 4, - useTabs: true, + bracketSpacing: true, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', + arrowParens: 'avoid', + tabWidth: 4, + useTabs: true, + semi: false, }; diff --git a/package-lock.json b/package-lock.json index a4a13b5f..3407049a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1281,6 +1281,22 @@ "chalk": "^4.0.0" } }, + "@react-native-async-storage/async-storage": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.6.tgz", + "integrity": "sha512-kVfy6O5Xbce9GfD9Islxn5JaOczNz6dF3Ce/7tP4foVLPNwo7UfdgXeKZ7iac07ZbvDvViSUuNyzzrN81FgqkQ==", + "requires": { + "merge-options": "^3.0.4" + } + }, + "@react-native-community/async-storage": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@react-native-community/async-storage/-/async-storage-1.12.1.tgz", + "integrity": "sha512-70WGaH3PKYASi4BThuEEKMkyAgE9k7VytBqmgPRx3MzJx9/MkspwqJGmn3QLCgHLIFUgF1pit2mWICbRJ3T3lg==", + "requires": { + "deep-assign": "^3.0.0" + } + }, "@react-native-community/cli-debugger-ui": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-5.0.1.tgz", @@ -1456,6 +1472,11 @@ "integrity": "sha512-W/J0fNYVO01tioHjvYWQ9m6RgndVtbElzYozBq1ZPrHO/iCzlqoySHl4gO/fpCl9QEFjvJfjPgtPMTMlsoq5DQ==", "dev": true }, + "@react-native-community/netinfo": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-6.0.0.tgz", + "integrity": "sha512-Z9M8VGcF2IZVOo2x+oUStvpCW/8HjIRi4+iQCu5n+PhC7OqCQX58KYAzdBr///alIfRXiu6oMb+lK+rXQH1FvQ==" + }, "@react-native/assets": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz", @@ -2066,6 +2087,14 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -2393,6 +2422,16 @@ "unset-value": "^1.0.0" } }, + "cachios": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cachios/-/cachios-3.0.0.tgz", + "integrity": "sha512-DLvbmAhX6r3n/bOdcIcF+mMnFwuIK5uuf7R2piU5eQeWymWSncxy3aQjb5apJt08jelEwA5IOf4QximSKiAeiw==", + "requires": { + "axios": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0", + "node-cache": "^4.1.1 || ^5.0.0", + "object-hash": "^2.0.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2797,6 +2836,14 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz", + "integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==", + "requires": { + "is-obj": "^1.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -3771,6 +3818,11 @@ "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.121.0.tgz", "integrity": "sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg==" }, + "follow-redirects": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz", + "integrity": "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -4365,6 +4417,16 @@ "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", "dev": true }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -5584,6 +5646,14 @@ "object-visit": "^1.0.0" } }, + "merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "requires": { + "is-plain-obj": "^2.1.0" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6045,6 +6115,21 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + } + } + }, "node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -6214,6 +6299,11 @@ } } }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -6862,6 +6952,14 @@ "nullthrows": "^1.1.1" } }, + "react-native-expire-storage": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/react-native-expire-storage/-/react-native-expire-storage-0.0.3.tgz", + "integrity": "sha512-JFaAe6nOJ7HfoIVXl6rLnqUgQfmf2Mf58ITAQeYDdlo6I+u0coG8o8QIMnK8I9CoZZ7ym+W1CtVWNpaWWSsw6A==", + "requires": { + "@react-native-community/async-storage": "^1.6.2" + } + }, "react-native-masked-text": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/react-native-masked-text/-/react-native-masked-text-1.13.0.tgz", diff --git a/package.json b/package.json index 41357341..9087f493 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,16 @@ "pod": "cd ./ios && pod install && cd ../" }, "dependencies": { + "@react-native-async-storage/async-storage": "^1.15.6", + "@react-native-community/netinfo": "^6.0.0", + "axios": "^0.21.1", + "cachios": "^3.0.0", "jet-tools": "^1.1.0", "lodash": "^4.17.21", "moment": "^2.29.1", "react": "17.0.1", "react-native": "0.64.2", + "react-native-expire-storage": "0.0.3", "react-native-masked-text": "^1.13.0", "react-native-raw-bottom-sheet": "^2.2.0", "react-native-splash-screen": "^3.2.0", diff --git a/src/api/http.decorstors.ts b/src/api/http.decorstors.ts new file mode 100644 index 00000000..5a7761b3 --- /dev/null +++ b/src/api/http.decorstors.ts @@ -0,0 +1,60 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' +import { AxiosRequestConfig, AxiosResponse } from 'axios' +import { NetworkService } from '@/services' +import { ApiResponse } from './interfaces' + +const PREFIX = '@http-offline' +const getAsyncStorageKey = key => `${PREFIX}/${key}` + +const setToStorage = async (key: string, data: unknown) => { + await AsyncStorage.setItem(getAsyncStorageKey(key), JSON.stringify(data)) +} + +const getFromStorage = async (key: string) => { + try { + const data = await AsyncStorage.getItem(getAsyncStorageKey(key)) + return JSON.parse(data) + } catch (e) { + return null + } +} + +export const OfflineDecorator = async ( + requestCall: () => ApiResponse, + key: string, +): ApiResponse => { + if (NetworkService.getInternetConnection()) { + const response: AxiosResponse = await requestCall() + setToStorage(key, response.data) + return response + } else { + const data = await getFromStorage(key) + return { + data, + status: 200, + statusText: 'Ok', + headers: {}, + config: null, + } + } +} + +export const generateKeyFromRequest = ( + url: string, + params?: AxiosRequestConfig, +) => { + let result = url + if (params) { + if (params.params && Object.keys(params.params).length) { + const keys = Object.keys(params.params) + const toResult: string[] = [] + keys.map((it: string) => + toResult.push(`${it}=${JSON.stringify(params.params[it])}`), + ) + toResult.sort() + result = `${url}?${toResult.join('&')}` + } + } + + return result +} diff --git a/src/api/http.service.ts b/src/api/http.service.ts new file mode 100644 index 00000000..10994442 --- /dev/null +++ b/src/api/http.service.ts @@ -0,0 +1,94 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import cachios from 'cachios' +import { config } from '@/config' +import { GlobalContainerService } from '@/services' +import Storage from 'react-native-expire-storage' + +const store = () => GlobalContainerService.getFromGlobalContainer('store') +const getDispatchSaveTokenPair = () => + GlobalContainerService.getFromGlobalContainer('dispatchSaveTokenPair') + +const axiosInstance = axios.create({ + baseURL: config.baseUrl, + headers: { + 'Content-Type': 'application/json', + }, + timeout: 180000, +}) + +axiosInstance.interceptors.request.use((config: any) => { + const token = store().getState().auth.accessToken + if (token) { + config.headers['authorization'] = `Bearer ${token}` + } + return config +}) + +const cachiosInstance = cachios.create(axiosInstance) + +cachiosInstance.cache = { + get: async (cacheKey: string) => { + const res = await Storage.getItem(cacheKey) + return res ? res : undefined + }, + set: async (cacheKey: string, cacheValue: any, ttl: number) => { + await Storage.setItem(cacheKey, cacheValue, ttl) + }, +} + +const requestAccessToken = async () => { + const response = await axiosInstance.post('/app/auth/reset-token', { + refreshToken: store().getState().auth.refreshToken, + }) + getDispatchSaveTokenPair()({ + accessToken: response.data.accessToken, + }) +} + +const request = async (func: Function): Promise> => { + try { + let response = await func() + return response as any as AxiosResponse + } catch (e) { + if (e.response.status === 401) { + await requestAccessToken() + return (await func()) as any as AxiosResponse + } + throw e + } +} + +export interface ICacheRequestConfig extends AxiosRequestConfig { + ttl?: number + force?: boolean + needCache?: true +} + +interface INonCacheRequestConfig extends AxiosRequestConfig { + needCache?: false +} + +type GetRequstConfig = ICacheRequestConfig | INonCacheRequestConfig + +const api = { + get: (url: string, params?: GetRequstConfig) => { + if (params && params.needCache) { + if (!params.ttl) params.ttl = 60 * 60 * 5 + return request(() => cachiosInstance.get(url, params)) + } else { + return request(() => axiosInstance.get(url, params)) + } + }, + post: (url: string, data: any, params?: AxiosRequestConfig) => + request(() => axiosInstance.post(url, data, params)), + + put: (url: string, data: any, params?: AxiosRequestConfig) => + request(() => axiosInstance.put(url, data, params)), + + patch: (url: string, data: any, params?: AxiosRequestConfig) => + request(() => axiosInstance.patch(url, data, params)), + + delete: (url: string, params?: AxiosRequestConfig) => + request(() => axiosInstance.delete(url, params)), +} +export default api diff --git a/src/api/interfaces/api-helpers.interfaces.ts b/src/api/interfaces/api-helpers.interfaces.ts new file mode 100644 index 00000000..f7a1e748 --- /dev/null +++ b/src/api/interfaces/api-helpers.interfaces.ts @@ -0,0 +1,3 @@ +import { AxiosResponse } from 'axios' + +export type ApiResponse = Promise> diff --git a/src/api/interfaces/index.ts b/src/api/interfaces/index.ts new file mode 100644 index 00000000..2cbb13ea --- /dev/null +++ b/src/api/interfaces/index.ts @@ -0,0 +1 @@ +export * from './api-helpers.interfaces' diff --git a/src/config/index.ts b/src/config/index.ts index e1928ae9..bb098137 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,7 +2,9 @@ import {fonts} from './fonts'; /** * Dev */ -export const dymanicConfig = {}; +export const dymanicConfig = { + baseUrl: 'http://localhost:3000/app/', +}; // /** // * Prod diff --git a/src/services/global-container.service.ts b/src/services/global-container.service.ts new file mode 100644 index 00000000..c760b8ba --- /dev/null +++ b/src/services/global-container.service.ts @@ -0,0 +1,18 @@ +const globalData: { + store?: any + internetConnect?: boolean +} = { + store: null, + internetConnect: true, +} + +const setToGlobalContainer = (key: string, value: any) => { + globalData[key] = value +} + +const getFromGlobalContainer = (key: string) => globalData[key] + +export const GlobalContainerService = { + setToGlobalContainer, + getFromGlobalContainer, +} diff --git a/src/services/index.ts b/src/services/index.ts index e69de29b..ca09159b 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -0,0 +1,2 @@ +export * from './global-container.service' +export * from './network.service' diff --git a/src/services/network.service.ts b/src/services/network.service.ts new file mode 100644 index 00000000..f2c7a00b --- /dev/null +++ b/src/services/network.service.ts @@ -0,0 +1,19 @@ +import NetInfo from '@react-native-community/netinfo' + +const localData = { + internerIsConnected: true, +} + +const init = () => { + const unsubscribe = NetInfo.addEventListener(state => { + localData.internerIsConnected = state.isConnected + }) + unsubscribe() +} + +const getInternetConnection = () => localData.internerIsConnected + +export const NetworkService = { + init, + getInternetConnection, +}