Lazar Kostic 3 лет назад
Родитель
Сommit
b584e8774b
64 измененных файлов: 1406 добавлений и 643 удалений
  1. 26
    8
      App.js
  2. 14
    6
      components/CustomDrawer/CustomDrawer.jsx
  3. 3
    0
      constants/localStorage.js
  4. 0
    126
      context/AuthContext.js
  5. 9
    11
      navigation/RootNavigation.js
  6. 6
    4
      package.json
  7. 11
    0
      request/apiEndpoints.js
  8. 112
    0
      request/index.js
  9. 18
    0
      request/loginRequest.js
  10. 8
    10
      screens/HomeScreen.jsx
  11. 62
    15
      screens/LoginScreen.jsx
  12. 4
    7
      screens/PostDetailsScreen.jsx
  13. 61
    35
      screens/RegisterScreen.jsx
  14. 0
    6
      service/apiEndpoints.js
  15. 6
    0
      service/asyncStorage.js
  16. 0
    1
      service/endpointDef.js
  17. 0
    41
      service/index.js
  18. 0
    30
      service/post.js
  19. 0
    22
      service/tokenService/index.js
  20. 0
    36
      service/tokenService/tokenApiClient.js
  21. 0
    17
      service/tokenService/validator.js
  22. 0
    40
      service/user.js
  23. 21
    0
      store/actions/actionHelpers/index.js
  24. 5
    0
      store/actions/app/appActionConstants.js
  25. 11
    0
      store/actions/app/appActions.js
  26. 10
    0
      store/actions/authProvider/authProviderActionConstants.js
  27. 20
    0
      store/actions/authProvider/authProviderActions.js
  28. 0
    14
      store/actions/index.js
  29. 30
    0
      store/actions/login/loginActionConstants.js
  30. 48
    0
      store/actions/login/loginActions.js
  31. 18
    0
      store/actions/register/registerActionConstants.js
  32. 30
    0
      store/actions/register/registerActions.js
  33. 3
    0
      store/actions/user/userActionConstants.js
  34. 14
    0
      store/actions/user/userActions.js
  35. 0
    79
      store/authPersistor.js
  36. 31
    8
      store/index.js
  37. 51
    0
      store/middleware/accessTokenMiddleware.js
  38. 22
    0
      store/middleware/authenticationMiddleware.js
  39. 20
    0
      store/middleware/internalServerErrorMiddleware.js
  40. 30
    0
      store/middleware/loadingMiddleware.js
  41. 27
    0
      store/middleware/requestStatusMiddleware.js
  42. 33
    0
      store/reducers/authProvider/authProviderReducer.js
  43. 11
    9
      store/reducers/index.js
  44. 45
    0
      store/reducers/loading/loadingReducer.js
  45. 69
    0
      store/reducers/login/loginReducer.js
  46. 54
    0
      store/reducers/register/registerReducer.js
  47. 0
    14
      store/reducers/test.js
  48. 0
    36
      store/reducers/user.js
  49. 31
    0
      store/reducers/user/userReducer.js
  50. 42
    0
      store/saga/authProviderSaga.js
  51. 8
    0
      store/saga/index.js
  52. 80
    0
      store/saga/loginSaga.js
  53. 32
    0
      store/saga/registerSaga.js
  54. 8
    0
      store/selectors/authProviderSelectors.js
  55. 16
    0
      store/selectors/loadingSelectors.js
  56. 29
    0
      store/selectors/loginSelectors.js
  57. 8
    0
      store/selectors/registerSelectors.js
  58. 28
    0
      store/selectors/userSelectors.js
  59. 13
    0
      store/utils/createReducer.js
  60. 0
    36
      thunks/user.thunk.js
  61. 33
    0
      utils/hooks/useAuthHook.js
  62. 7
    0
      utils/rejectErrorMessageHelper.js
  63. 9
    0
      utils/variables.js
  64. 119
    32
      yarn.lock

+ 26
- 8
App.js Просмотреть файл

@@ -1,14 +1,34 @@
import "react-native-gesture-handler";
import React, { useContext } from "react";
import React, { useEffect } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { Provider } from "react-redux";
import { Provider, useDispatch } from "react-redux";
import store from "./store";

import { useFonts } from "expo-font";
import { AuthProvider } from "./context/AuthContext";
import RootNavigation from "./navigation/RootNavigation";
import { getData } from "./service/asyncStorage";
import { JWT_REFRESH_TOKEN, JWT_TOKEN } from "./constants/localStorage";
import { fetchUserSuccess } from "./store/actions/login/loginActions";
import { addHeaderToken } from "./request";

function App() {
const dispatch = useDispatch();
const getToken = async () => {
const token = await getData(JWT_TOKEN);
const refreshToken = await getData(JWT_REFRESH_TOKEN);

if (token) {
addHeaderToken(token);
}
if (token !== undefined && refreshToken !== undefined) {
dispatch(fetchUserSuccess({ jwt: token, refreshToken }));
}
};

useEffect(() => {
getToken();
}, []);

return (
<NavigationContainer>
<RootNavigation />
@@ -27,11 +47,9 @@ const AppWrapper = () => {
}

return (
<AuthProvider>
<Provider store={store}>
<App />
</Provider>
</AuthProvider>
<Provider store={store}>
<App />
</Provider>
);
};


+ 14
- 6
components/CustomDrawer/CustomDrawer.jsx Просмотреть файл

@@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import {
View,
Text,
@@ -12,17 +12,25 @@ import {
DrawerContentScrollView,
DrawerItemList,
} from "@react-navigation/drawer";
import { AuthContext } from "../../context/AuthContext";
import Loader from "../Loader";
import { MaterialIcons } from "@expo/vector-icons";
import { Ionicons } from "@expo/vector-icons";
import { useDispatch } from "react-redux";
import { logoutUser } from "../../store/actions/login/loginActions";
import useAuthHook from "../../utils/hooks/useAuthHook";

const CustomDrawer = (props) => {
const { logout, isLoading } = useContext(AuthContext);

const {logoutAuthProvider} = useAuthHook()

const dispatch = useDispatch();

const handleLogout = async () => {
logoutAuthProvider()
dispatch(logoutUser());
}

return (
<View style={styles.container}>
<Loader visible={isLoading} />
<DrawerContentScrollView {...props} contentContainerStyle={styles.drawer}>
<ImageBackground
source={require("../../assets/images/menu-bg.jpeg")}
@@ -48,7 +56,7 @@ const CustomDrawer = (props) => {
<Text style={styles.footerButtonText}>Contact Us</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={logout} style={styles.footerButton}>
<TouchableOpacity onPress={handleLogout} style={styles.footerButton}>
<View style={styles.buttonContent}>
<MaterialIcons name="logout" size={22} color="#333" />
<Text style={styles.footerButtonText}>Sign Out</Text>

+ 3
- 0
constants/localStorage.js Просмотреть файл

@@ -0,0 +1,3 @@
export const JWT_TOKEN = 'JwtToken';
export const JWT_REFRESH_TOKEN = 'JwtRefreshToken';
export const ACCESS_TOKEN = "AccessToken";

+ 0
- 126
context/AuthContext.js Просмотреть файл

@@ -1,126 +0,0 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Google from "expo-auth-session/providers/google";
import React, { createContext, useEffect, useState } from "react";
import { googleApi, loginApi, registerApi } from "../service/user";
import { revokeAsync } from "expo-auth-session";

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
const [userInfo, setUserInfo] = useState({});
const [isLoading, setIsLoading] = useState(false);

// Google Auth
const [request, response, promptAsync] = Google.useAuthRequest({
androidClientId:
"1032296921439-dpgvgkss3ggds0egvo6puf0r7un9em6c.apps.googleusercontent.com",
iosClientId:
"1032296921439-j7jfahhm7q3l07aj04qvdblmcns77oul.apps.googleusercontent.com",
expoClientId:
"1032296921439-vfs9k28sn7kei4nft998ck3067m8ksiq.apps.googleusercontent.com",
});

useEffect(() => {
if (response?.type === "success") {
googleApi(response.authentication.accessToken)
.then((res) => {
if (res.user) {
setUserInfo(res);
AsyncStorage.setItem("userInfo", JSON.stringify(res));
setIsLoading(false);
} else {
callback(res);
setIsLoading(false);
}
})
.catch((e) => {
console.log("error", e);
setIsLoading(false);
});
}
}, [response]);

const login = (email, password, callback) => {
setIsLoading(true);
loginApi({ identifier: email, password })
.then((res) => {
if (res.user) {
setUserInfo(res);
AsyncStorage.setItem("userInfo", JSON.stringify(res));
AsyncStorage.setItem("jwt-token", res.jwt);
setIsLoading(false);
} else {
callback(res);
setIsLoading(false);
}
})
.catch((e) => {
console.log("error", e);
setIsLoading(false);
});
};

const googleAuth = () => {
promptAsync({ useProxy: true, showInRecents: true });
};

const register = (username, email, password, callback) => {
setIsLoading(true);
registerApi({ username, email, password })
.then((res) => {
if (res.user) {
setUserInfo(res);
AsyncStorage.setItem("userInfo", JSON.stringify(res));
AsyncStorage.setItem("jwt-token", res.jwt);
setIsLoading(false);
} else {
callback(res);
setIsLoading(false);
}
})
.catch((e) => {
console.log("error", e);
setIsLoading(false);
});
};

const logout = async () => {
setIsLoading(true);
AsyncStorage.removeItem("userInfo");
AsyncStorage.removeItem("jwt-token");
setUserInfo({});

if (userInfo.user.provider === "google") {
await revokeAsync(
{ token: response.authentication.accessToken },
{ revocationEndpoint: "https://oauth2.googleapis.com/revoke" }
);
}
setIsLoading(false);
};

const isLoggedIn = async () => {
try {
let userInfo = await AsyncStorage.getItem("userInfo");
userInfo = JSON.parse(userInfo);

if (userInfo) {
setUserInfo(userInfo);
}
} catch (e) {
console.log(e);
}
};

useEffect(() => {
isLoggedIn();
}, []);

return (
<AuthContext.Provider
value={{ isLoading, login, logout, userInfo, register, googleAuth }}
>
{children}
</AuthContext.Provider>
);
};

+ 9
- 11
navigation/RootNavigation.js Просмотреть файл

@@ -1,15 +1,13 @@
import React, {useContext} from 'react'
import { AuthContext } from '../context/AuthContext'
import AppStack from './AppStack';
import AuthStack from './AuthStack';
import React from "react";
import AppStack from "./AppStack";
import AuthStack from "./AuthStack";
import { useSelector } from "react-redux";
import { selectTokens } from "../store/selectors/loginSelectors";

const RootNavigation = () => {
const {userInfo} = useContext(AuthContext);
return (
!userInfo.jwt ? <AuthStack /> : <AppStack />
)
const tokens = useSelector(selectTokens);

}
return !tokens.JwtToken ? <AuthStack /> : <AppStack />;
};

export default RootNavigation
export default RootNavigation;

+ 6
- 4
package.json Просмотреть файл

@@ -17,13 +17,15 @@
"@reduxjs/toolkit": "^1.9.1",
"axios": "^1.2.1",
"expo": "~47.0.8",
"expo-auth-session": "~3.7.3",
"expo-auth-session": "~3.8.0",
"expo-random": "~13.0.0",
"expo-splash-screen": "~0.17.5",
"expo-status-bar": "~1.4.2",
"expo-web-browser": "~12.0.0",
"formik": "^2.2.9",
"jwt-decode": "^3.1.2",
"lodash.filter": "^4.6.0",
"lodash.isempty": "^4.4.0",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-native": "0.70.5",
@@ -39,9 +41,9 @@
"react-native-vector-icons": "^9.2.0",
"react-native-web": "~0.18.7",
"react-navigation-stack": "^2.10.4",
"react-redux": "^8.0.5",
"redux-batched-actions": "^0.5.0",
"redux-thunk": "^2.4.2",
"react-redux": "^7.2.4",
"redux": "^4.1.0",
"redux-saga": "^1.1.3",
"yup": "^0.32.11"
},
"devDependencies": {

+ 11
- 0
request/apiEndpoints.js Просмотреть файл

@@ -0,0 +1,11 @@
export default {
authentications: {
login: "api/auth/local",
register: "api/auth/local/register",
refreshToken: "api/token/refresh",
authProvider: "api/auth/google/callback?access_token={accessToken}",
},
users: {
logout: "api/auth/logout",
},
};

+ 112
- 0
request/index.js Просмотреть файл

@@ -0,0 +1,112 @@
import axios from "axios";

const request = axios.create({
baseURL: "http://localhost:1337/",
headers: {
"Content-Type": "application/json",
},
});

export const getRequest = (url, params = null, options = null) =>
request.get(url, { params, ...options });

export const postRequest = (url, data, params = null, options = null) =>
request.post(url, data, { params, ...options });

export const putRequest = (url, data, params = null, options = null) =>
request.put(url, data, { params, ...options });

export const patchRequest = (url, data, params = null, options = null) =>
request.patch(url, data, { params, ...options });

export const deleteRequest = (url, params = null, options = null) =>
request.delete(url, { params, ...options });

export const downloadRequest = (url, params = null, options = null) =>
request.get(url, { params, ...options, responseType: "blob" });

export const replaceInUrl = (url, pathVariables = {}) => {
const keys = Object.keys(pathVariables);
if (!keys.length) {
return url;
}

return keys.reduce(
(acc, key) => acc.replace(`{${key}}`, pathVariables[`${key}`]),
url
);
};

export const addHeaderToken = (token) => {
request.defaults.headers.Authorization = `Bearer ${token}`;
};

export const addHeaderCookie = (key, value) => {
request.defaults.headers[`${key}`] = value;
};

export const removeHeaderToken = () => {
delete request.defaults.headers.Authorization;
};

// If you pass function to interceptor of axios, it only adds that function
// to existing array of interceptor functions. That causes that same function
// of interceptors getting called multiple times instead of just one time, as it
// is supposed to do. Thats why there is 'global' axios interceptor array, which indicates
// axios to eject previous interceptor. This approach requires that every middleware has its
// unique name from which it is being recognized. Every object in those arrays contains
// interceptor name and ID of interceptor function.
let axiosInterceptorRequests = [];
let axiosInterceptorResponses = [];

export const attachPostRequestListener = (
postRequestListener,
interceptorName
) => {
let previousAxiosInterceptor = axiosInterceptorResponses.find(
(item) => item.name === interceptorName
);
let previousAxiosInterceptorResponses = axiosInterceptorResponses;
if (previousAxiosInterceptor !== undefined) {
request.interceptors.response.eject(previousAxiosInterceptor.interceptorID);
previousAxiosInterceptorResponses = axiosInterceptorResponses.filter(
(item) => item.interceptorID !== previousAxiosInterceptor.interceptorID
);
}
let axiosInterceptorID = request.interceptors.response.use(
(response) => response,
(response) => postRequestListener(response)
);
previousAxiosInterceptorResponses.push({
name: interceptorName,
interceptorID: axiosInterceptorID,
});
axiosInterceptorResponses = [...previousAxiosInterceptorResponses];
};

export const attachBeforeRequestListener = (
beforeRequestListener,
interceptorName
) => {
let previousAxiosInterceptor = axiosInterceptorRequests.find(
(item) => item.name === interceptorName
);
let previousAxiosInterceptorRequests = axiosInterceptorRequests;
if (previousAxiosInterceptor !== undefined) {
request.interceptors.request.eject(previousAxiosInterceptor.interceptorID);
previousAxiosInterceptorRequests = axiosInterceptorRequests.filter(
(item) => item.interceptorID !== previousAxiosInterceptor.interceptorID
);
}
let axiosInterceptorID = request.interceptors.request.use(
(response) => beforeRequestListener(response),
(response) => response
);
previousAxiosInterceptorRequests.push({
name: interceptorName,
interceptorID: axiosInterceptorID,
});
axiosInterceptorRequests = [...previousAxiosInterceptorRequests];
};

export const apiDefaultUrl = request.defaults.baseURL;

+ 18
- 0
request/loginRequest.js Просмотреть файл

@@ -0,0 +1,18 @@
import { getRequest, postRequest, replaceInUrl } from "./index";
import apiEndpoints from "./apiEndpoints";

export const attemptLogin = (payload) =>
postRequest(apiEndpoints.authentications.login, payload);

export const attemptAuthProvider = (accessToken) =>
getRequest(
replaceInUrl(apiEndpoints.authentications.authProvider, { accessToken })
);

export const attemptRegister = (payload) =>
postRequest(apiEndpoints.authentications.register, payload);

export const refreshTokenRequest = (payload) =>
postRequest(apiEndpoints.authentications.refreshToken, payload);

export const logoutUserRequest = () => getRequest(apiEndpoints.users.logout);

+ 8
- 10
screens/HomeScreen.jsx Просмотреть файл

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import {
View,
Text,
@@ -10,15 +10,13 @@ import {
StyleSheet,
} from "react-native";
import Feather from "@expo/vector-icons/Feather";
import { postsApi } from "../service/post";
import { AuthContext } from "../context/AuthContext";
import ListItem from "../components/ListItem/ListItem";
import filter from "lodash.filter";
import { globalStyles } from "../styles/global";
import { getRequest } from "../request";

const HomeScreen = ({ navigation }) => {
const [posts, setPosts] = useState([]);
const { userInfo } = useContext(AuthContext);

const [query, setQuery] = useState("");
const [filteredData, setFilteredData] = useState(posts);
@@ -40,11 +38,11 @@ const HomeScreen = ({ navigation }) => {
setQuery(formatedText);
};

const fetchAll = () => {
if (userInfo.jwt) {
postsApi(userInfo.jwt)
.then((res) => setPosts(res.data))
.catch((e) => console.log(e));
const fetchAll = async () => {
const { data } = await getRequest("api/posts?populate=*");
if (data.data) {
setPosts(data.data);
}
};

@@ -56,7 +54,7 @@ const HomeScreen = ({ navigation }) => {
if (posts) {
setFilteredData(posts);
}
}, [posts])
}, [posts]);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#fff" }}>
<ScrollView style={{ padding: 20 }}>

+ 62
- 15
screens/LoginScreen.jsx Просмотреть файл

@@ -1,4 +1,4 @@
import React, { useState, useContext } from "react";
import React, { useEffect } from "react";
import {
SafeAreaView,
View,
@@ -17,20 +17,56 @@ import TwitterSVG from "../assets/images/twitter.svg";
import CustomButton from "../components/Buttons/CustomButton";
import InputField from "../components/InputField";
import { globalStyles } from "../styles/global";
import { AuthContext } from "../context/AuthContext";
import Loader from "../components/Loader";
import { Formik } from "formik";
import { loginSchema } from "../schemas/loginSchema";
import { useDispatch, useSelector } from "react-redux";
import { selectLoginError } from "../store/selectors/loginSelectors";
import {
clearLoginErrors,
fetchUser,
} from "../store/actions/login/loginActions";
import { selectIsLoadingByActionType } from "../store/selectors/loadingSelectors";
import { LOGIN_USER_SCOPE } from "../store/actions/login/loginActionConstants";
import { fetchAuthProvider } from "../store/actions/authProvider/authProviderActions";
import useAuthHook from "../utils/hooks/useAuthHook";
import { storeData } from "../service/asyncStorage";
import { ACCESS_TOKEN } from "../constants/localStorage";

const LoginScreen = ({ navigation }) => {
const [error, setError] = useState(null);
const { response, promptAsync } = useAuthHook();
const dispatch = useDispatch();
const error = useSelector(selectLoginError);

const isLoading = useSelector(selectIsLoadingByActionType(LOGIN_USER_SCOPE));

const storeToken = async (token) => {
await storeData(ACCESS_TOKEN, token);
}

const { isLoading, login, googleAuth } = useContext(AuthContext);
useEffect(() => {
if (response?.type === "success") {
const accessToken = response.authentication.accessToken;
if (accessToken) {
storeToken(accessToken);
dispatch(fetchAuthProvider({ accessToken }));
}
}
}, [response]);

const handleGoogleAuth = () => {
promptAsync({ useProxy: true, showInRecents: true });
};

const handleLogin = (values) => {
login(values.email, values.password, function (result) {
setError(result.error.message);
});
const { email, password } = values;
dispatch(clearLoginErrors());
dispatch(
fetchUser({
identifier: email,
password,
})
);
};

return (
@@ -50,16 +86,23 @@ const LoginScreen = ({ navigation }) => {
validateOnChange={false}
validateOnBlur={false}
>
{({ handleChange, handleBlur, handleSubmit, values, isValid, errors }) => (
{({
handleChange,
handleBlur,
handleSubmit,
values,
isValid,
errors,
}) => (
<>
<Text style={globalStyles.boldText}>Sign In</Text>
<InputField
name="email"
label={"Email"}
keyboardType="email-address"
onChangeText={handleChange('email')}
onChangeText={handleChange("email")}
text={values.email}
handleBlur={handleBlur('email')}
handleBlur={handleBlur("email")}
icon={
<MaterialIcons
name="alternate-email"
@@ -69,16 +112,18 @@ const LoginScreen = ({ navigation }) => {
/>
}
/>
{errors.email && <Text style={styles.errorMessage}>{errors.email}</Text>}
{errors.email && (
<Text style={styles.errorMessage}>{errors.email}</Text>
)}
<InputField
name="password"
label={"Password"}
filedButtonLabel={"Forgot?"}
fieldButtonFunction={() => {}}
inputType="password"
handleBlur={handleBlur('password')}
handleBlur={handleBlur("password")}
text={values.password}
onChangeText={handleChange('password')}
onChangeText={handleChange("password")}
icon={
<Ionicons
name="ios-lock-closed-outline"
@@ -88,7 +133,9 @@ const LoginScreen = ({ navigation }) => {
/>
}
/>
{errors.password && <Text style={styles.errorMessage}>{errors.password}</Text>}
{errors.password && (
<Text style={styles.errorMessage}>{errors.password}</Text>
)}
{error && <Text style={styles.errorMessage}>{error}</Text>}
<CustomButton label={"Login"} onPress={handleSubmit} />
</>
@@ -97,7 +144,7 @@ const LoginScreen = ({ navigation }) => {
<Text style={globalStyles.regularCenteredText}>Or login with ...</Text>
<View style={styles.providersContainer}>
<TouchableOpacity
onPress={googleAuth}
onPress={handleGoogleAuth}
style={globalStyles.iconButton}
>
<GoogleSVG height={24} width={24} />

+ 4
- 7
screens/PostDetailsScreen.jsx Просмотреть файл

@@ -1,19 +1,16 @@
import React, { useContext, useEffect, useState } from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { AuthContext } from "../context/AuthContext";
import { singlePostApi } from "../service/post";
import { getRequest } from "../request";
import { globalStyles } from "../styles/global";
import { windowWidth } from "../utils/Dimensions";

const PostDetailsScreen = ({ navigation, route }) => {
const { userInfo } = useContext(AuthContext);
const [post, setPost] = useState({});

const fetchPost = async () => {
if (userInfo.jwt && route.params.id) {
singlePostApi(userInfo.jwt, route.params?.id)
.then((res) => setPost(res.data))
.catch((e) => console.log(e));
const { data } = await getRequest(`api/posts/${route.params.id}?populate=*`);
if (data) {
setPost(data.data);
}
};


+ 61
- 35
screens/RegisterScreen.jsx Просмотреть файл

@@ -1,13 +1,12 @@
import React, { useState, useContext } from "react";
import React, { useEffect } from "react";
import {
SafeAreaView,
ScrollView,
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Platform,
Alert,
} from "react-native";

// import DateTimePicker from "@react-native-community/datetimepicker";
@@ -23,40 +22,70 @@ import TwitterSVG from "../assets/images/twitter.svg";

import CustomButton from "../components/Buttons/CustomButton";
import { globalStyles } from "../styles/global";
import { AuthContext } from "../context/AuthContext";
import Loader from "../components/Loader";
import { Formik } from "formik";
import { registerSchema } from "../schemas/registerSchema";
import { useDispatch, useSelector } from "react-redux";
import { selectRegisterError } from "../store/selectors/registerSelectors";
import { selectIsLoadingByActionType } from "../store/selectors/loadingSelectors";
import { REGISTER_USER_SCOPE } from "../store/actions/register/registerActionConstants";
import {
clearRegisterErrors,
registerUser,
} from "../store/actions/register/registerActions";
import useAuthHook from "../utils/hooks/useAuthHook";
import { fetchAuthProvider } from "../store/actions/authProvider/authProviderActions";
import { ACCESS_TOKEN } from "../constants/localStorage";
import { storeData } from "../service/asyncStorage";

const RegisterScreen = ({ navigation }) => {
const { isLoading, register, googleAuth } = useContext(AuthContext);
const { response, promptAsync } = useAuthHook();
const dispatch = useDispatch();
const error = useSelector(selectRegisterError);

const isLoading = useSelector(
selectIsLoadingByActionType(REGISTER_USER_SCOPE)
);

const [date, setDate] = useState(new Date());
const [open, setOpen] = useState(false);
// const [dateOfBirthLabel, setDateOfBirthLabel] = useState("Date of Birth");
const [error, setError] = useState(null);
const storeToken = async (token) => {
await storeData(ACCESS_TOKEN, token);
}

// BIRTHDAY DATETIME PICKER
// const onChange = (event, selectedDate) => {
// const currentDate = selectedDate || date;
// setDate(currentDate);
// let tempDate = new Date(currentDate);
// let fDate =
// tempDate.getDate() +
// "/" +
// (tempDate.getMonth() + 1) +
// "/" +
// tempDate.getFullYear();
// setDateOfBirthLabel(fDate);
// setOpen(false);
// };
const handleApiResponseSuccess = () => {
Alert.alert(
"Success",
"Successfully registered account, now you can log in",
[
{
text: "OK",
onPress: () => navigation.navigate("Login"),
},
]
);
};

const handleSignup = (values) => {
register(values.username, values.email, values.password, function (result) {
setError(result.error.message);
});
const { username, email, password } = values;
dispatch(clearRegisterErrors());
dispatch(
registerUser({ username, email, password, handleApiResponseSuccess })
);
};

const handleGoogleAuth = () => {
promptAsync({ useProxy: true, showInRecents: true });
};

useEffect(() => {
if (response?.type === "success") {
const accessToken = response.authentication.accessToken;
if (accessToken) {
storeToken(accessToken);
dispatch(fetchAuthProvider({ accessToken }));
}
}
}, [response]);

return (
<SafeAreaView style={globalStyles.safeArea}>
<Loader visible={isLoading} />
@@ -70,7 +99,7 @@ const RegisterScreen = ({ navigation }) => {
<Text style={globalStyles.boldText}>Sign Up</Text>
<View style={styles.providersContainer}>
<TouchableOpacity
onPress={googleAuth}
onPress={handleGoogleAuth}
style={globalStyles.iconButton}
>
<GoogleSVG height={24} width={24} />
@@ -178,14 +207,11 @@ const RegisterScreen = ({ navigation }) => {
}
inputType="password"
/>
{errors.confirmPassword && <Text style={styles.errorMessage}>{errors.confirmPassword}</Text>}
{/* <View style={styles.dateOfBirthContainer}>
<Ionicons name='calendar-outline' size={20} color="#666" style={{marginRight: 5}} />
<TouchableOpacity onPress={() => setOpen(true)}>
<Text style={styles.dateOfBirthLabel}>{dateOfBirthLabel}</Text>
{open && <DateTimePicker testID='dateTimePicker' value={date} mode="date" display='spinner' onChange={onChange} textColor="#000" />}
</TouchableOpacity>
</View> */}
{errors.confirmPassword && (
<Text style={styles.errorMessage}>
{errors.confirmPassword}
</Text>
)}
{error && <Text style={styles.errorMessage}>{error}</Text>}
<CustomButton label={"Sign Up"} onPress={handleSubmit} />
</>

+ 0
- 6
service/apiEndpoints.js Просмотреть файл

@@ -1,6 +0,0 @@
export default {
login: 'api/auth/local',
register: 'api/auth/local/register',
googleAuth: 'api/auth/google/callback?access_token=',
fetchPosts: 'api/posts?populate=*',
}

+ 6
- 0
service/asyncStorage.js Просмотреть файл

@@ -24,3 +24,9 @@ export const removeData = async (key) => {
// error reading value
}
};

export const clearAll = async () => {
try {
await AsyncStorage.clear();
} catch (e) {}
};

+ 0
- 1
service/endpointDef.js Просмотреть файл

@@ -1 +0,0 @@
export const API_ENDPOINT ="http://localhost:1337/";

+ 0
- 41
service/index.js Просмотреть файл

@@ -1,41 +0,0 @@
import axios from "axios";
import { API_ENDPOINT } from "./endpointDef";

const request = axios.create({
baseURL: API_ENDPOINT,
headers: {
"Content-Type": "application/json",
},
});

export const getRequest = (url, params = null, options = null) =>
request.get(url, { params, ...options });

export const postRequest = (url, data, params = null, options = null) =>
request.post(url, data, { params, ...options });

export const putRequest = (url, data, params = null, options = null) =>
request.put(url, data, { params, ...options });

export const patchRequest = (url, data, params = null, options = null) =>
request.patch(url, data, { params, ...options });

export const deleteRequest = (url, params = null, options = null) =>
request.delete(url, { params, ...options });

export const addHeaderToken = (token) => {
request.defaults.headers.Authorization = `Bearer ${token}`;
};

export const removeHeaderToken = () => {
delete request.defaults.headers.Authorization;
};

export const attachPostRequestListener = (postRequestListener) => {
request.interceptors.response.use(
(response) => response,
(response) => postRequestListener(response)
);
};

export const apiDefaultUrl = request.defaults.baseURL;

+ 0
- 30
service/post.js Просмотреть файл

@@ -1,30 +0,0 @@
import { API_ENDPOINT } from "./endpointDef";
import apiEndpoints from "./apiEndpoints";

export const postsApi = async (jwt) => {
const result = await fetch(`${API_ENDPOINT}${apiEndpoints.fetchPosts}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
'Authorization': "Bearer " + jwt,
},
});

const res = await result.json();

return res;
};

export const singlePostApi = async (jwt, postId) => {
const result = await fetch(`${API_ENDPOINT}api/posts/${postId}?populate=*`, {
method: "GET",
headers: {
"Content-Type": "application/json",
'Authorization': "Bearer " + jwt,
},
});
const res = await result.json();
return res;
};

+ 0
- 22
service/tokenService/index.js Просмотреть файл

@@ -1,22 +0,0 @@
import store from '../../store/index';
import { refreshTokens } from './tokenApiClient'
import { validateAccessToken } from './validator'

let lock;

export async function getAccessToken() {
if (lock) {
await lock;
}
else {
lock = new Promise((resolve, reject) => {

})
}
if (!validateAccessToken()) {
console.log("REFRESHING TOKEN")
await refreshTokens();

}
return store.getState().user.token
}

+ 0
- 36
service/tokenService/tokenApiClient.js Просмотреть файл

@@ -1,36 +0,0 @@
import axios from "axios";
import { batchActions } from 'redux-batched-actions';
import { logOut, setRefreshToken, setToken } from "../../store/actions";
import store from "../../store/index";
import { API_ENDPOINT } from '../endpointDef';

let refreshingPromise = null

export async function refreshTokens() {
// console.log("RefreshTokens")
if (!refreshingPromise) {
refreshingPromise = new Promise(async (resolve, reject) => {
// console.log("Sending the requst for new tokens")
const refreshToken = store.getState().user.refreshToken
const username = store.getState().user.username
try {
const { data } = await axios.post(`${API_ENDPOINT}api/token/refresh`, { Token: refreshToken, Username: username, });
// console.log("Dispatch from refresh", data.Data.AccessToken, data.Data.RefreshToken)
store.dispatch(batchActions([setToken(data.Data.AccessToken), setRefreshToken(data.Data.RefreshToken)]))
resolve()
}
catch (e) {
// console.log("Could not refresh token.")
store.dispatch(logOut())
reject(e)
}
finally {
refreshingPromise = null
}
})
}
// else {
// console.log("Someone is refreshing...")
// }
await refreshingPromise
}

+ 0
- 17
service/tokenService/validator.js Просмотреть файл

@@ -1,17 +0,0 @@
import store from "../../store/store";
import jwt_decode from "jwt-decode";

export function validateAccessToken() {
const accessToken = store.getState().user.token
if (!accessToken) {
return false;
}
const decoded = jwt_decode(accessToken);
// console.log("DECODED", decoded)
const exp = decoded.exp * 1000
const isExpired = exp - (Date.now() + 60 * 1000)
// console.log("isExpired", isExpired)
if (isExpired <= 0)
return false
return true
}

+ 0
- 40
service/user.js Просмотреть файл

@@ -1,40 +0,0 @@
import { API_ENDPOINT } from "./endpointDef";
import apiEndpoints from "./apiEndpoints";

export const loginApi = async (payload) => {
const result = await fetch(`${API_ENDPOINT}${apiEndpoints.login}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})

const res = await result.json();

return res;
};

export const registerApi = async (payload) => {
const result = await fetch(`${API_ENDPOINT}${apiEndpoints.register}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})

const res = await result.json();

return res;
};

export const googleApi = async (accessToken) => {
const result = await fetch(`${API_ENDPOINT}${apiEndpoints.googleAuth}${accessToken}`);

const res = await result.json();

return res;
};



+ 21
- 0
store/actions/actionHelpers/index.js Просмотреть файл

@@ -0,0 +1,21 @@
export const FETCH = '[FETCH]';
export const UPDATE = '[UPDATE]';
export const SUCCESS = '[SUCCESS]';
export const SET = '[SET]';
export const ERROR = '[ERROR]';
export const SUBMIT = '[SUBMIT]';
export const CLEAR = '[CLEAR]';
export const DELETE = '[DELETE]';
export const LOADING = '[LOADING]';

const createType = (typeDescription) => (type) => `${typeDescription}${type}`;

export const createFetchType = createType(FETCH);
export const createUpdateType = createType(UPDATE);
export const createSuccessType = createType(SUCCESS);
export const createSetType = createType(SET);
export const createErrorType = createType(ERROR);
export const createSubmitType = createType(SUBMIT);
export const createClearType = createType(CLEAR);
export const createDeleteType = createType(DELETE);
export const createLoadingType = createType(LOADING);

+ 5
- 0
store/actions/app/appActionConstants.js Просмотреть файл

@@ -0,0 +1,5 @@
import { createLoadingType } from "../actionHelpers";

export const APP_LOADING = createLoadingType("APP_LOADING");
export const ADD_LOADER = createLoadingType("ADD_LOADER");
export const REMOVE_LOADER = createLoadingType("REMOVE_LOADER");

+ 11
- 0
store/actions/app/appActions.js Просмотреть файл

@@ -0,0 +1,11 @@
import { ADD_LOADER, REMOVE_LOADER } from "./appActionConstants";

export const addLoader = (payload) => ({
type: ADD_LOADER,
payload,
});

export const removeLoader = (payload) => ({
type: REMOVE_LOADER,
payload,
});

+ 10
- 0
store/actions/authProvider/authProviderActionConstants.js Просмотреть файл

@@ -0,0 +1,10 @@
import {
createErrorType,
createFetchType,
createSuccessType,
} from "../actionHelpers";

export const AUTH_PROVIDER_SCOPE = "AUTH_PROVIDER";
export const AUTH_PROVIDER_FETCH = createFetchType(AUTH_PROVIDER_SCOPE);
export const AUTH_PROVIDER_SUCCESS = createSuccessType(AUTH_PROVIDER_SCOPE);
export const AUTH_PROVIDER_ERROR = createErrorType(AUTH_PROVIDER_SCOPE);

+ 20
- 0
store/actions/authProvider/authProviderActions.js Просмотреть файл

@@ -0,0 +1,20 @@
import {
AUTH_PROVIDER_ERROR,
AUTH_PROVIDER_FETCH,
AUTH_PROVIDER_SUCCESS,
} from "./authProviderActionConstants";

export const fetchAuthProvider = (payload) => ({
type: AUTH_PROVIDER_FETCH,
payload,
});

export const fetchAuthProviderSuccess = (payload) => ({
type: AUTH_PROVIDER_SUCCESS,
payload,
});

export const fetchAuthProviderError = (payload) => ({
type: AUTH_PROVIDER_ERROR,
payload,
});

+ 0
- 14
store/actions/index.js Просмотреть файл

@@ -1,14 +0,0 @@
import { createAction } from "@reduxjs/toolkit";

const createTestAction = (name) => createAction(`TEST_${name}`)
export const increment = createTestAction('INCREMENT')
export const decrement = createTestAction('DECREMENT')

const createUserAction = (name) => createAction(`USER_${name}`)
export const setToken = createUserAction('TOKEN')
export const setRefreshToken = createUserAction('REFRESHTOKEN')
export const authLoaded = createUserAction('AUTH_LOADED')
export const logOut = createUserAction('LOGOUT')
export const setUsername = createUserAction('USERNAME')
export const logInSuccess = createUserAction('LOGIN_SUCCESS')
export const setConnectionError = createUserAction('SET_CONNECTION_ERROR')

+ 30
- 0
store/actions/login/loginActionConstants.js Просмотреть файл

@@ -0,0 +1,30 @@
import {
createClearType,
createErrorType,
createFetchType,
createLoadingType,
createSuccessType,
createSubmitType,
} from '../actionHelpers';


export const LOGIN_USER_SCOPE = 'LOGIN_USER';
export const LOGIN_USER_FETCH = createFetchType(LOGIN_USER_SCOPE);
export const LOGIN_USER_SUCCESS = createSuccessType(LOGIN_USER_SCOPE);
export const LOGIN_USER_ERROR = createErrorType(LOGIN_USER_SCOPE);
export const CLEAR_LOGIN_USER_ERROR = createClearType(
`${LOGIN_USER_SCOPE}_ERROR`,
);
export const LOGIN_USER_LOADING = createLoadingType(LOGIN_USER_SCOPE);


export const UPDATE_USER_JWT_TOKEN = 'UPDATE_USER_JWT_TOKEN';
export const RESET_LOGIN_STATE = 'RESET_LOGIN_STATE';
export const AUTHENTICATE_USER = 'AUTHENTICATE_USER';
export const LOGOUT_USER = 'LOGOUT_USER';
export const REFRESH_TOKEN = 'REFRESH_TOKEN';

const GENERATE_TOKEN_SCOPE = 'GENERATE_TOKEN';
export const GENERATE_TOKEN = createSubmitType(GENERATE_TOKEN_SCOPE);
export const GENERATE_TOKEN_SUCCESS = createSuccessType(GENERATE_TOKEN_SCOPE);
export const GENERATE_TOKEN_ERROR = createErrorType(GENERATE_TOKEN_SCOPE);

+ 48
- 0
store/actions/login/loginActions.js Просмотреть файл

@@ -0,0 +1,48 @@
import {
CLEAR_LOGIN_USER_ERROR,
LOGIN_USER_ERROR,
LOGIN_USER_FETCH,
LOGIN_USER_SUCCESS,
LOGOUT_USER,
RESET_LOGIN_STATE,
UPDATE_USER_JWT_TOKEN,
REFRESH_TOKEN,
} from './loginActionConstants';


export const fetchUser = (payload) => ({
type: LOGIN_USER_FETCH,
payload,
});

export const fetchUserSuccess = (payload) => ({
type: LOGIN_USER_SUCCESS,
payload,
});

export const fetchUserError = (payload) => ({
type: LOGIN_USER_ERROR,
payload,
});

export const updateUserToken = (payload) => ({
type: UPDATE_USER_JWT_TOKEN,
payload,
});

export const resetLoginState = () => ({
type: RESET_LOGIN_STATE,
});

export const clearLoginErrors = () => ({
type: CLEAR_LOGIN_USER_ERROR,
});

export const logoutUser = () => ({
type: LOGOUT_USER,
});

export const refreshUserToken = (payload) => ({
type: REFRESH_TOKEN,
payload
});

+ 18
- 0
store/actions/register/registerActionConstants.js Просмотреть файл

@@ -0,0 +1,18 @@
import {
createClearType,
createErrorType,
createFetchType,
createLoadingType,
createSuccessType,
} from "../actionHelpers";

export const REGISTER_USER_SCOPE = "REGISTER_USER";
export const REGISTER_USER_FETCH = createFetchType(REGISTER_USER_SCOPE);
export const REGISTER_USER_SUCCESS = createSuccessType(REGISTER_USER_SCOPE);
export const REGISTER_USER_ERROR = createErrorType(REGISTER_USER_SCOPE);
export const CLEAR_REGISTER_USER_ERROR = createClearType(
`${REGISTER_USER_SCOPE}_ERROR`
);
export const REGISTER_USER_LOADING = createLoadingType(REGISTER_USER_SCOPE);

export const RESET_REGISTER_STATE = "RESET_REGISTER_STATE";

+ 30
- 0
store/actions/register/registerActions.js Просмотреть файл

@@ -0,0 +1,30 @@
import {
CLEAR_REGISTER_USER_ERROR,
REGISTER_USER_ERROR,
REGISTER_USER_FETCH,
REGISTER_USER_SUCCESS,
RESET_REGISTER_STATE,
} from "./registerActionConstants";

export const registerUser = (payload) => ({
type: REGISTER_USER_FETCH,
payload,
});

export const registerUserSuccess = (payload) => ({
type: REGISTER_USER_SUCCESS,
payload,
});

export const registerUserError = (payload) => ({
type: REGISTER_USER_ERROR,
payload,
});

export const resetRegisterState = () => ({
type: RESET_REGISTER_STATE,
});

export const clearRegisterErrors = () => ({
type: CLEAR_REGISTER_USER_ERROR,
});

+ 3
- 0
store/actions/user/userActionConstants.js Просмотреть файл

@@ -0,0 +1,3 @@

export const SET_USER = 'SET_USER';
export const SET_USER_ERROR = 'SET_USER_ERROR';

+ 14
- 0
store/actions/user/userActions.js Просмотреть файл

@@ -0,0 +1,14 @@
import {
SET_USER,
SET_USER_ERROR,
} from './userActionConstants';

export const setUser = (payload) => ({
type: SET_USER,
payload,
});

export const setUserError = (payload) => ({
type: SET_USER_ERROR,
payload,
});

+ 0
- 79
store/authPersistor.js Просмотреть файл

@@ -1,79 +0,0 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { batchActions } from 'redux-batched-actions';
import { authLoaded, setRefreshToken, setToken, setUsername } from './actions'

// AsyncStorage.getAllKeys()
// .then((keys) => AsyncStorage.multiGet(keys)
// .then((data) => console.log(data)));

async function safeSetStorage(key, value) {
if (value) {
await AsyncStorage.setItem(key, value)
}
else {
await AsyncStorage.removeItem(key)
}
}

const accessTokenKey = 'userToken'
const refreshTokenKey = 'refreshToken'
const userNameKey = 'userName'

function subscibeOnAuthChanges(store) {
let lastAccessToken = store.getState().user.token;
let lastRefreshToken = store.getState().user.refreshToken;
let lastUsername = store.getState().user.username;
// console.log("In subscribe function", lastAccessToken, lastRefreshToken, lastUsername)

store.subscribe(async () => {
const state = store.getState();
// console.log("In subscribe...")
const newAccessToken = state.user.token;
const newRefreshToken = state.user.refreshToken;
const newUsername = state.user.username;
// console.log("In subscribe", newAccessToken, newRefreshToken, newUsername)

if (lastAccessToken !== newAccessToken) {
lastAccessToken = newAccessToken
await safeSetStorage(accessTokenKey, newAccessToken)
}

if (lastRefreshToken !== newRefreshToken) {
lastRefreshToken = newRefreshToken
await safeSetStorage(refreshTokenKey, newRefreshToken)
}

if (lastUsername !== newUsername) {
lastUsername = newUsername
await safeSetStorage(userNameKey, newUsername)
}
})
}

async function loadFromStorage(store) {
try {
// leave comments this for testing
// console.log("getting Token from storage");
// await AsyncStorage.setItem(accessTokenKey, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI4MmVlNjNkMi0yZDRjLTRkZjQtYTQ0MS1hZWRkZjhiNGEwMmYiLCJzdWIiOiJpbGlqYSt1Y2lAcHJvZml0b3B0aWNzLmNvbSIsInVuaXF1ZV9uYW1lIjoiaWxpamErdWNpQHByb2ZpdG9wdGljcy5jb20iLCJVdGNPZmZzZXRNaW51dGVzIjoiLTI0MCIsIkFncmVlbWVudEFjY2VwdGVkIjoiVHJ1ZSIsIk9yZ2FuaXphdGlvbklkIjoiMTkiLCJPcmdhbml6YXRpb25Ib3N0bmFtZSI6Im15c3RyYXRhZGV2Mi5wcm9maXRvcHRpY3MuY29tIiwibmJmIjoxNjE2Njk0NTI2LCJleHAiOjE2MTY2OTYzMjYsImlzcyI6InN0cmF0YWNsZWFyLmNvbSIsImF1ZCI6IkNsaWVudCJ9.kk0TQENidNJfQpkLy6uVUZULHp1r46gA7IyhAMHkqsI");
userToken = await AsyncStorage.getItem(accessTokenKey);
// leave this for testing
// await AsyncStorage.setItem(refreshTokenKey, "IBFRYNvviwI0+C3CVy1sViQlvFDTXsTUZWa8fgzv6oLQtYTng3A+8xc4gMqpX+y1lR8WIhN4uDlEV19uu+prWEVpi8qkjp999Li0t+eiEoe7OdeK+vQwecowvvJZRt1WK/Qb3biMC76IhElZta/riy2yZ9jQRBhiEfPs+yK+GJI=");
// await AsyncStorage.setItem(refreshTokenKey, "TV+VsT+Qwb9hh1oi8mG4CBX0gnN9y9aNfXT/QUPZsPwCJ3n1svOO/KwT14Z5UZxhU+8h5ocuvipj4dIGDJa7wJHkV7j5/mBKfSgVqMvX43VdFXkp19Dg0h4Tg9Elh6GpR0N6FSCDJl3igv83W3OpC4UWXylQKW+0gr+tjOPmT78=");
refreshToken = await AsyncStorage.getItem(refreshTokenKey);
// leave this for testing
// await AsyncStorage.setItem(userNameKey, "ilija+uci@profitoptics.com");
userName = await AsyncStorage.getItem(userNameKey);

// console.log('Loaded from storage', userToken)
store.dispatch(batchActions([setToken(userToken), setRefreshToken(refreshToken), setUsername(userName), authLoaded()]))
} catch (eror) {
console.error("There was an error getting token from storage", eror);
}
}

function init(store) {
loadFromStorage(store)
.then(() => subscibeOnAuthChanges(store))
}

export { init };

+ 31
- 8
store/index.js Просмотреть файл

@@ -1,9 +1,32 @@
import { createStore, applyMiddleware } from "redux";
import { enableBatching } from 'redux-batched-actions';
import thunk from "redux-thunk";
import reducers from "./reducers";
import { init } from './authPersistor'
import { applyMiddleware, compose, createStore } from "redux";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./reducers";
import rootSaga from "./saga";
import loadingMiddleware from "./middleware/loadingMiddleware";
import requestStatusMiddleware from "./middleware/requestStatusMiddleware";
import accessTokenMiddleware from "./middleware/accessTokenMiddleware";
// import authenticationMiddleware from "./middleware/authenticationMiddleware";
// import internalServerErrorMiddleware from "./middleware/internalServerErrorMiddleware";
const composeEnhancers =
(window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
trace: true,
traceLimit: 25,
})) ||
compose;
const sagaMiddleware = createSagaMiddleware();
export default createStore(
rootReducer,
composeEnhancers(
applyMiddleware(
sagaMiddleware,
loadingMiddleware,
requestStatusMiddleware,
// internalServerErrorMiddleware,
accessTokenMiddleware,
// authenticationMiddleware
)
)
);

const store = createStore(enableBatching(reducers), applyMiddleware(thunk));
init(store)
export default store;
sagaMiddleware.run(rootSaga);

+ 51
- 0
store/middleware/accessTokenMiddleware.js Просмотреть файл

@@ -0,0 +1,51 @@
import axios from "axios";
import jwt_decode from "jwt-decode";
import { JWT_REFRESH_TOKEN, JWT_TOKEN } from "../../constants/localStorage";
import { attachBeforeRequestListener } from "../../request/index";
import { getData } from "../../service/asyncStorage";
import { logoutUser, refreshUserToken } from "../actions/login/loginActions";

export const accessTokensMiddlewareInterceptorName = "ACCESS_TOKEN_INTERCEPTOR";

export default ({ dispatch }) =>
(next) =>
(action) => {
attachBeforeRequestListener(async (response) => {
const jwtToken = await getData(JWT_TOKEN);
const refresh = await getData(JWT_REFRESH_TOKEN);
if (!jwtToken || !refresh) return Promise.resolve(response);
const jwtTokenDecoded = jwt_decode(jwtToken);
const refreshTokenDecoded = jwt_decode(refresh);
if (!response.headers?.Authorization) {
response.headers.Authorization = `Bearer ${jwtToken}`;
}

// If refresh token is expired, log out user
if (new Date() > new Date(refreshTokenDecoded?.exp * 1000)) {
dispatch(logoutUser());
return Promise.resolve(response);
}

// If access token is expired, refresh access token
if (new Date() > new Date(jwtTokenDecoded.exp * 1000)) {
const axiosResponse = await axios.post(
"http://localhost:1337/api/token/refresh",
{
refreshToken: refresh,
},
{
headers: { Authorization: `Bearer ${jwtToken}` },
}
);
const newToken = axiosResponse.data;

response.headers.Authorization = `Bearer ${newToken.jwt}`;

dispatch(refreshUserToken(newToken));
}

return Promise.resolve(response);
}, accessTokensMiddlewareInterceptorName);

next(action);
};

+ 22
- 0
store/middleware/authenticationMiddleware.js Просмотреть файл

@@ -0,0 +1,22 @@
import { attachPostRequestListener } from "../../request";
import { logoutUser } from "../actions/login/loginActions";

export const authenticationMiddlewareInterceptorName =
"AUTHENTICATION_MIDDLEWARE";

export default ({ dispatch }) =>
(next) =>
(action) => {
attachPostRequestListener((error) => {
if (!error.response) {
return Promise.reject(error);
}
if (error.response.status === 401) {
dispatch(logoutUser());
return Promise.reject(error);
}
return Promise.resolve();
}, authenticationMiddlewareInterceptorName);

next(action);
};

+ 20
- 0
store/middleware/internalServerErrorMiddleware.js Просмотреть файл

@@ -0,0 +1,20 @@
import { attachPostRequestListener } from "../../request";
import { makeErrorToastMessage } from "../../util/helpers/toastMessage";
import i18next from "i18next";

export const serverErrorMiddlewareInterceptorName =
"INTERNAL_SERVER_ERROR_MIDDLEWARE_INTERCEPTOR";

export default () => (next) => (action) => {
attachPostRequestListener((error) => {
if (!error.response) {
return makeErrorToastMessage(i18next.t("apiErrors.SomethingWentWrong"));
}
if (error.response.status === 500) {
return makeErrorToastMessage(i18next.t("apiErrors.SomethingWentWrong"));
}
return Promise.reject(error);
}, serverErrorMiddlewareInterceptorName);

next(action);
};

+ 30
- 0
store/middleware/loadingMiddleware.js Просмотреть файл

@@ -0,0 +1,30 @@
import {
DELETE,
ERROR,
FETCH,
SUCCESS,
UPDATE,
SUBMIT,
} from '../actions/actionHelpers';
import { addLoader, removeLoader } from '../actions/app/appActions';

const promiseTypes = [FETCH, UPDATE, DELETE, SUBMIT];
export default ({ dispatch }) => (next) => (action) => {
const promiseType = promiseTypes.find((promiseType) =>
action.type.includes(promiseType),
);
if (promiseType) {
dispatch(addLoader(action.type));
return next(action);
}

if (action.type.includes(SUCCESS) || action.type.includes(ERROR)) {
// const actionType = action.type.includes(SUCCESS)
// ? action.type.replace(SUCCESS, '[LOADING]')
// : action.type.replace(ERROR, '[LOADING]');

dispatch(removeLoader(action.type));
return next(action);
}
next(action);
};

+ 27
- 0
store/middleware/requestStatusMiddleware.js Просмотреть файл

@@ -0,0 +1,27 @@
import { attachPostRequestListener } from "../../request";
import apiEndpoints from "../../request/apiEndpoints";
import { logoutUser } from "../actions/login/loginActions";

export const requestStatusMiddlewareInterceptorName =
"REQUEST_STATUS_MIDDLEWARE_INTERCEPTOR";

export default ({ dispatch }) =>
(next) =>
(action) => {
attachPostRequestListener((error) => {
if (!error.response) {
return Promise.reject(error);
}
if (
error.response.config.url !== apiEndpoints.authentications.login &&
error.response.config.url !==
apiEndpoints.authentications.confirmSecurityQuestion &&
error.response.status === 401
) {
return dispatch(logoutUser());
}
return Promise.reject(error);
}, requestStatusMiddlewareInterceptorName);

next(action);
};

+ 33
- 0
store/reducers/authProvider/authProviderReducer.js Просмотреть файл

@@ -0,0 +1,33 @@
import createReducer from "../../utils/createReducer";

import {
AUTH_PROVIDER_ERROR,
AUTH_PROVIDER_SUCCESS,
} from "../../actions/authProvider/authProviderActionConstants";

const initialState = {
success: "",
errorMessage: "",
};

export default createReducer(
{
[AUTH_PROVIDER_SUCCESS]: setProvider,
[AUTH_PROVIDER_ERROR]: setProviderError,
},
initialState
);

function setProvider(state, action) {
return {
...state,
success: action.payload,
};
}

function setProviderError(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

+ 11
- 9
store/reducers/index.js Просмотреть файл

@@ -1,12 +1,14 @@
import { combineReducers } from "redux";
// import { persistReducer } from "redux-persist";
// import immutableTransform from "redux-persist-transform-immutable";
import test from "./test";
import user from "./user";
import loginReducer from "./login/loginReducer";
import loadingReducer from "./loading/loadingReducer";
import userReducer from "./user/userReducer";
import registerReducer from "./register/registerReducer";
import authProviderReducer from "./authProvider/authProviderReducer";

const rootReducer = combineReducers({
test,
user
export default combineReducers({
login: loginReducer,
user: userReducer,
loading: loadingReducer,
register: registerReducer,
authProvider: authProviderReducer,
});

export default rootReducer;

+ 45
- 0
store/reducers/loading/loadingReducer.js Просмотреть файл

@@ -0,0 +1,45 @@
import createReducer from "../../utils/createReducer";
import {
ADD_LOADER,
REMOVE_LOADER,
} from "../../actions/app/appActionConstants";

const initialState = {
loaderCount: 0,
};
export default createReducer(
{
[ADD_LOADER]: addLoader,
[REMOVE_LOADER]: removeLoader,
},
initialState
);

function addLoader(state, action) {
let loaderCount = state.loaderCount;
let actionType = action.payload.replace("[FETCH]", "");
if (!state[actionType]) {
loaderCount++;
}
return {
...state,
[actionType]: true,
loaderCount,
};
}

function removeLoader(state, action) {
let actionType = action.payload
.replace("[SUCCESS]", "")
.replace("[ERROR]", "");
let loaderCount = state.loaderCount;
if (state[actionType] === true) {
loaderCount--;
}

return {
...state,
[actionType]: false,
loaderCount,
};
}

+ 69
- 0
store/reducers/login/loginReducer.js Просмотреть файл

@@ -0,0 +1,69 @@
import createReducer from '../../utils/createReducer';
import {
CLEAR_LOGIN_USER_ERROR,
LOGIN_USER_ERROR,
LOGIN_USER_SUCCESS,
RESET_LOGIN_STATE,
UPDATE_USER_JWT_TOKEN,
} from '../../actions/login/loginActionConstants';

const initialState = {
email: '',
token: {
JwtRefreshToken: '',
JwtToken: '',
},
errorMessage: '',
};

export default createReducer(
{

[LOGIN_USER_SUCCESS]: setUser,
[UPDATE_USER_JWT_TOKEN]: setUserJwtToken,
[RESET_LOGIN_STATE]: resetLoginState,
[LOGIN_USER_ERROR]: setError,
[CLEAR_LOGIN_USER_ERROR]: clearLoginErrors,
},
initialState,
);


function setUser(state, action) {
return {
...state,
token: {
...state.token,
JwtToken: action.payload.jwt,
JwtRefreshToken: action.payload.refreshToken
},
};
}

function setUserJwtToken(state, action) {
return {
...state,
token: {
...state.token,
JwtToken: action.payload,
},
};
}

function setError(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

function resetLoginState() {
return initialState;
}

function clearLoginErrors(state) {
return {
...state,
errorMessage: '',
};
}

+ 54
- 0
store/reducers/register/registerReducer.js Просмотреть файл

@@ -0,0 +1,54 @@
import createReducer from '../../utils/createReducer';
import {
CLEAR_REGISTER_USER_ERROR,
REGISTER_USER_ERROR,
REGISTER_USER_SUCCESS,
RESET_REGISTER_STATE,
} from '../../actions/register/registerActionConstants';

const initialState = {
token: {
JwtToken: '',
},
errorMessage: '',
};

export default createReducer(
{

[REGISTER_USER_SUCCESS]: setUser,
[RESET_REGISTER_STATE]: resetRegisterState,
[REGISTER_USER_ERROR]: setError,
[CLEAR_REGISTER_USER_ERROR]: clearRegisterErrors,
},
initialState,
);


function setUser(state, action) {
return {
...state,
token: {
...state.token,
JwtToken: action.payload.jwt,
},
};
}

function setError(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

function resetRegisterState() {
return initialState;
}

function clearRegisterErrors(state) {
return {
...state,
errorMessage: '',
};
}

+ 0
- 14
store/reducers/test.js Просмотреть файл

@@ -1,14 +0,0 @@
import { createReducer } from '@reduxjs/toolkit'
import { increment, decrement } from '../actions/index'

export default createReducer({
value: 0
}, builder => {
builder
.addCase(increment, (state) => {
state.value += 1
})
.addCase(decrement, (state) => {
state.value -= 1
})
})

+ 0
- 36
store/reducers/user.js Просмотреть файл

@@ -1,36 +0,0 @@
import {createReducer} from '@reduxjs/toolkit'
import { setToken, setRefreshToken, authLoaded, logOut, setUsername, logInSuccess, setConnectionError } from "../actions/index";

export default createReducer({
token: null,
refreshToken: null,
username: "",
logInSuccess: "",
connectionError: "",
isAuthLoadedFromStorage: false
}, builder => {
builder
.addCase(setToken, (state, { payload }) => {
state.token = payload
})
.addCase(setRefreshToken, (state, { payload }) => {
state.refreshToken = payload
})
.addCase(authLoaded, (state) => {
state.isAuthLoadedFromStorage = true
})
.addCase(logOut, (state) => {
state.username = null;
state.token = null;
state.refreshToken = null;
})
.addCase(setUsername, (state, { payload }) => {
state.username = payload
})
.addCase(logInSuccess, (state, { payload }) => {
state.logInSuccess = payload
})
.addCase(setConnectionError, (state) => {
state.connectionError = payload
})
})

+ 31
- 0
store/reducers/user/userReducer.js Просмотреть файл

@@ -0,0 +1,31 @@
import createReducer from '../../utils/createReducer';
import {
SET_USER,
SET_USER_ERROR,
} from '../../actions/user/userActionConstants';

const initialState = {
user: {},
};

export default createReducer(
{
[SET_USER]: setUser,
[SET_USER_ERROR]: setUserError,
},
initialState,
);

function setUser(state, action) {
return {
...state,
user: action.payload,
};
}

function setUserError(state, action) {
return {
...state,
errorMessage: action.payload,
};
}

+ 42
- 0
store/saga/authProviderSaga.js Просмотреть файл

@@ -0,0 +1,42 @@
import { all, call, put, takeLatest } from "@redux-saga/core/effects";
import { attemptAuthProvider } from "../../request/loginRequest";
import {
fetchUserError,
fetchUserSuccess,
} from "../actions/login/loginActions";
import { setUser } from "../actions/user/userActions";
import { addHeaderToken } from "../../request";
import { ACCESS_TOKEN, JWT_REFRESH_TOKEN, JWT_TOKEN } from "../../constants/localStorage";
import { storeData } from "../../service/asyncStorage";
import { rejectErrorCodeHelper } from "../../utils/rejectErrorMessageHelper";
import { AUTH_PROVIDER_FETCH } from "../actions/authProvider/authProviderActionConstants";
import {
fetchAuthProviderError,
fetchAuthProviderSuccess,
} from "../actions/authProvider/authProviderActions";

function* fetchAuthProvider({ payload }) {
try {
const { data } = yield call(attemptAuthProvider, payload.accessToken);
if (data?.jwt) {
const user = data?.user;
yield call(storeData, JWT_TOKEN, data.jwt);
yield call(storeData, JWT_REFRESH_TOKEN, data?.refreshToken);
yield call(storeData, ACCESS_TOKEN, payload.accessToken);
yield call(addHeaderToken, data?.jwt);
yield put(setUser(user));
}
yield put(fetchUserSuccess(data));
yield put(fetchAuthProviderSuccess("Success"));
} catch (e) {
if (e.response && e.response.data) {
const errorMessage = yield call(rejectErrorCodeHelper, e);
yield put(fetchUserError(errorMessage));
yield put(fetchAuthProviderError('Error'));
}
}
}

export default function* authProviderSaga() {
yield all([takeLatest(AUTH_PROVIDER_FETCH, fetchAuthProvider)]);
}

+ 8
- 0
store/saga/index.js Просмотреть файл

@@ -0,0 +1,8 @@
import { all } from "redux-saga/effects";
import loginSaga from "./loginSaga";
import registerSaga from "./registerSaga";
import authProviderSaga from "./authProviderSaga";

export default function* rootSaga() {
yield all([loginSaga(), registerSaga(), authProviderSaga()]);
}

+ 80
- 0
store/saga/loginSaga.js Просмотреть файл

@@ -0,0 +1,80 @@
import { all, call, put, takeLatest } from "@redux-saga/core/effects";
import jwt_decode from "jwt-decode";
import {
LOGIN_USER_FETCH,
LOGOUT_USER,
REFRESH_TOKEN,
} from "../actions/login/loginActionConstants";
import { attemptLogin, logoutUserRequest } from "../../request/loginRequest";
import {
fetchUserError,
fetchUserSuccess,
resetLoginState,
} from "../actions/login/loginActions";
import { setUser } from "../actions/user/userActions";
import { addHeaderToken, removeHeaderToken } from "../../request";
import {
JWT_REFRESH_TOKEN,
JWT_TOKEN,
} from "../../constants/localStorage";
import { storeData, getData, clearAll } from "../../service/asyncStorage";
import { rejectErrorCodeHelper } from "../../utils/rejectErrorMessageHelper";

function* fetchUser({ payload }) {
try {
const { data } = yield call(attemptLogin, payload);
if (data?.jwt) {
const user = data?.user;
yield call(storeData, JWT_TOKEN, data.jwt);
yield call(storeData, JWT_REFRESH_TOKEN, data?.refreshToken);
yield call(addHeaderToken, data?.jwt);
yield put(setUser(user));
}
yield put(fetchUserSuccess(data));
} catch (e) {
if (e.response && e.response.data) {
const errorMessage = yield call(rejectErrorCodeHelper, e);
yield put(fetchUserError(errorMessage));
}
}
}

function* logoutUser() {
try {
const token = yield call(getData, JWT_REFRESH_TOKEN);
const user = yield call(jwt_decode, token);
if (user) {
yield call(logoutUserRequest);
}
} catch (error) {
console.log(error); // eslint-disable-line
} finally {
yield call(removeHeaderToken);
yield call(clearAll);
yield put(resetLoginState());
}
}

export function* refreshToken({ payload }) {
try {
const newTokenDecoded = jwt_decode(payload.jwt);

yield call(storeData, JWT_TOKEN, payload.jwt);
yield call(storeData, JWT_REFRESH_TOKEN, payload.refreshToken);
addHeaderToken(payload.jwt);
yield put(setUser(newTokenDecoded));
yield put(fetchUserSuccess(payload));
return true;
} catch (error) {
console.log(error); // eslint-disable-line
return false;
}
}

export default function* loginSaga() {
yield all([
takeLatest(LOGIN_USER_FETCH, fetchUser),
takeLatest(LOGOUT_USER, logoutUser),
takeLatest(REFRESH_TOKEN, refreshToken),
]);
}

+ 32
- 0
store/saga/registerSaga.js Просмотреть файл

@@ -0,0 +1,32 @@
import { all, call, put, takeLatest } from "@redux-saga/core/effects";
import { REGISTER_USER_FETCH } from "../actions/register/registerActionConstants";
import { attemptRegister } from "../../request/loginRequest";
import {
registerUserError,
registerUserSuccess,
} from "../actions/register/registerActions";
import { JWT_TOKEN } from "../../constants/localStorage";
import { storeData } from "../../service/asyncStorage";
import { rejectErrorCodeHelper } from "../../utils/rejectErrorMessageHelper";

function* registerUser({ payload }) {
try {
const { data } = yield call(attemptRegister, payload);
if (data?.jwt) {
yield call(storeData, JWT_TOKEN, data.jwt);
}
yield put(registerUserSuccess(data.jwt));
if (payload.handleApiResponseSuccess) {
yield call(payload.handleApiResponseSuccess);
}
} catch (e) {
if (e.response && e.response.data) {
const errorMessage = yield call(rejectErrorCodeHelper, e);
yield put(registerUserError(errorMessage));
}
}
}

export default function* registerSaga() {
yield all([takeLatest(REGISTER_USER_FETCH, registerUser)]);
}

+ 8
- 0
store/selectors/authProviderSelectors.js Просмотреть файл

@@ -0,0 +1,8 @@
import createSelector from "reselect";

const authProviderSelector = (state) => state.authProvider;

export const selectLoginError = createSelector(
authProviderSelector,
(state) => state.errorMessage
);

+ 16
- 0
store/selectors/loadingSelectors.js Просмотреть файл

@@ -0,0 +1,16 @@
import { createSelector } from "reselect";

const loadingSelector = (state) => state.loading;

export const selectIsLoadingByActionType = (loadingActionType) =>
createSelector(loadingSelector, (state) => state[`${loadingActionType}`]);

export const selectIsLoadingByActionTypes = (actionTypes) =>
createSelector(loadingSelector, (state) =>
actionTypes.some((actionType) => state[`${actionType}`])
);

export const selectLoaderCount = createSelector(
loadingSelector,
(state) => state.loaderCount
);

+ 29
- 0
store/selectors/loginSelectors.js Просмотреть файл

@@ -0,0 +1,29 @@
import { createSelector } from 'reselect';

const loginSelector = (state) => state.login;

export const selectLoginEmail = createSelector(
loginSelector,
(state) => state.email,
);

export const selectUsernames = createSelector(
loginSelector,
(state) => state.usernames,
);

export const selectTokens = createSelector(
loginSelector,
(state) => state.token,
);

export const selectJWTToken = createSelector(
loginSelector,
(state) => state.token.JwtToken,
);


export const selectLoginError = createSelector(
loginSelector,
(state) => state.errorMessage,
);

+ 8
- 0
store/selectors/registerSelectors.js Просмотреть файл

@@ -0,0 +1,8 @@
import { createSelector } from 'reselect';

const registerSelector = (state) => state.register;

export const selectRegisterError = createSelector(
registerSelector,
(state) => state.errorMessage,
);

+ 28
- 0
store/selectors/userSelectors.js Просмотреть файл

@@ -0,0 +1,28 @@
import { createSelector } from 'reselect';

export const userSelector = (state) => state.user;

export const selectAuthUser = createSelector(
userSelector,
(state) => state.user,
);

export const getUserSecurityQuestion = createSelector(
userSelector,
(state) => state.securityQuestion,
);

export const getForgotPasswordRequest = createSelector(
userSelector,
(state) => state.user,
);

export const selectForgotPasswordError = createSelector(
userSelector,
(state) => state.errorMessage,
);

export const getResetPasswordRequest = createSelector(
userSelector,
(state) => state.user,
);

+ 13
- 0
store/utils/createReducer.js Просмотреть файл

@@ -0,0 +1,13 @@
const createReducer = (handlers, initialState) => {
return (state = initialState, action) => {
const reducer = handlers[action.type];

if (!reducer) {
return state;
}

return reducer(state, action);
};
};

export default createReducer;

+ 0
- 36
thunks/user.thunk.js Просмотреть файл

@@ -1,36 +0,0 @@
import { batchActions } from "redux-batched-actions";
import { removeData, storeData } from "../service/asyncStorage";
import { loginApi } from "../service/user";
import { logInSuccess, setToken, setUsername } from "../store/actions";

export const login = (email, password, callback) => {
return async (dispatch) => {
const result = await loginApi({ identifier: email, password });
if (result.data === null) {
callback(result);
}else {
await removeData("TOKEN");
const token = await storeData('TOKEN', result.jwt);
console.log(result.user);
if (token) {
dispatch(batchActions([
setToken(result.jwt),
setUsername(result.user.username),
logInSuccess(true)
]))
}
}
};
};

export const logout = () => {
return async (dispatch) => {
await removeData('TOKEN');
dispatch(batchActions([
setToken(null),
setUsername(null),
logInSuccess(false)
]))
};
};

+ 33
- 0
utils/hooks/useAuthHook.js Просмотреть файл

@@ -0,0 +1,33 @@
import React from "react";
import * as Google from "expo-auth-session/providers/google";
import { getData } from "../../service/asyncStorage";
import { ACCESS_TOKEN } from "../../constants/localStorage";
import { revokeAsync } from "expo-auth-session";
import variables from "../variables";

const useAuthHook = () => {
const [request, response, promptAsync] = Google.useAuthRequest({
androidClientId: variables.androidClientId,
iosClientId: variables.iosClientId,
expoClientId: variables.expoClientId,
});

const logoutAuthProvider = async () => {
const token = await getData(ACCESS_TOKEN);
if (token !== null) {
await revokeAsync(
{ token: token },
{ revocationEndpoint: variables.revocationEndpoint }
);
}
};

return {
request,
response,
promptAsync,
logoutAuthProvider,
};
};

export default useAuthHook;

+ 7
- 0
utils/rejectErrorMessageHelper.js Просмотреть файл

@@ -0,0 +1,7 @@
export const rejectErrorCodeHelper = (error) => {
if (error?.response?.data?.error) {
const errorMessage = error?.response?.data?.error.message;

return errorMessage;
}
};

+ 9
- 0
utils/variables.js Просмотреть файл

@@ -0,0 +1,9 @@
export default {
androidClientId:
"1032296921439-dpgvgkss3ggds0egvo6puf0r7un9em6c.apps.googleusercontent.com",
iosClientId:
"1032296921439-j7jfahhm7q3l07aj04qvdblmcns77oul.apps.googleusercontent.com",
expoClientId:
"1032296921439-vfs9k28sn7kei4nft998ck3067m8ksiq.apps.googleusercontent.com",
revocationEndpoint: "https://oauth2.googleapis.com/revoke",
};

+ 119
- 32
yarn.lock Просмотреть файл

@@ -1017,13 +1017,20 @@
pirates "^4.0.5"
source-map-support "^0.5.16"

"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.20.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3"
integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==
dependencies:
regenerator-runtime "^0.13.11"

"@babel/runtime@^7.6.3":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
dependencies:
regenerator-runtime "^0.13.11"

"@babel/template@^7.0.0", "@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@@ -1820,6 +1827,50 @@
dependencies:
nanoid "^3.1.23"

"@redux-saga/core@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.2.2.tgz#99b1daac93a42feecd9bab449f452f56f3155fea"
integrity sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==
dependencies:
"@babel/runtime" "^7.6.3"
"@redux-saga/deferred" "^1.2.1"
"@redux-saga/delay-p" "^1.2.1"
"@redux-saga/is" "^1.1.3"
"@redux-saga/symbols" "^1.1.3"
"@redux-saga/types" "^1.2.1"
redux "^4.0.4"
typescript-tuple "^2.2.1"

"@redux-saga/deferred@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@redux-saga/deferred/-/deferred-1.2.1.tgz#aca373a08ccafd6f3481037f2f7ee97f2c87c3ec"
integrity sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==

"@redux-saga/delay-p@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@redux-saga/delay-p/-/delay-p-1.2.1.tgz#e72ac4731c5080a21f75b61bedc31cb639d9e446"
integrity sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==
dependencies:
"@redux-saga/symbols" "^1.1.3"

"@redux-saga/is@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/is/-/is-1.1.3.tgz#b333f31967e87e32b4e6b02c75b78d609dd4ad73"
integrity sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==
dependencies:
"@redux-saga/symbols" "^1.1.3"
"@redux-saga/types" "^1.2.1"

"@redux-saga/symbols@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/symbols/-/symbols-1.1.3.tgz#b731d56201719e96dc887dc3ae9016e761654367"
integrity sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==

"@redux-saga/types@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.2.1.tgz#9403f51c17cae37edf870c6bc0c81c1ece5ccef8"
integrity sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==

"@reduxjs/toolkit@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.1.tgz#4c34dc4ddcec161535288c60da5c19c3ef15180e"
@@ -1962,7 +2013,7 @@
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa"
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==

"@types/hoist-non-react-statics@^3.3.1":
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@@ -2019,6 +2070,16 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==

"@types/react-redux@^7.1.20":
version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"

"@types/react@*":
version "18.0.26"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917"
@@ -2033,11 +2094,6 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==

"@types/use-sync-external-store@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==

"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@@ -3453,14 +3509,14 @@ expo-asset@~8.6.2:
path-browserify "^1.0.0"
url-parse "^1.5.9"

expo-auth-session@~3.7.3:
version "3.7.3"
resolved "https://registry.yarnpkg.com/expo-auth-session/-/expo-auth-session-3.7.3.tgz#ded1afbbf88e0c6e4098e59920b4f13927fc1f3d"
integrity sha512-0mX47j6WdpEoaFVxU36VBRkEjJKwAkqnRyNdd5g+Ab1fHXQJJK3nzjOjxuBzgE8mKSS9NGHr/At9IGY6MLrg2g==
expo-auth-session@~3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/expo-auth-session/-/expo-auth-session-3.8.0.tgz#ad929f1bcc2e1dc9d0b265f9555d3f967bda6bcc"
integrity sha512-pQ8GryTTZL/JKHvifUGD4GGlZWo7RrcoQlvQ0O5m5edYfoa7fMHCg20MBX4Da4P3eVgJlqWZWCHfBC2fZxcRfA==
dependencies:
expo-constants "~14.0.0"
expo-crypto "~12.0.0"
expo-linking "~3.2.0"
expo-linking "~3.3.0"
expo-web-browser "~12.0.0"
invariant "^2.2.4"
qs "6.9.1"
@@ -3502,10 +3558,10 @@ expo-keep-awake@~11.0.1:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==

expo-linking@~3.2.0:
version "3.2.3"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.2.3.tgz#7b493a7fea2aadafc88a42e2fc6a5a4ba6d47df9"
integrity sha512-PgiWCao9TecLOPdtWyiNSY+UQGAwdjFx4KbHd1YsF0KnM1CJ2idcaHpDRlQPWSNmDebUZYN461/dVtJi9b2krg==
expo-linking@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.0.tgz#3860af9da374f0187db75036f353ca6fe9231a15"
integrity sha512-wXPzI2kijnql2L2F6i8zP1zINTkYlcRXyh1iV3P6Bt57v6yZiiniZBnb6grJVj19LOmluNs0PYrbX1ZsHBChCg==
dependencies:
"@types/qs" "^6.5.3"
expo-constants "~14.0.0"
@@ -4692,6 +4748,11 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"

jwt-decode@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==

kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -4775,6 +4836,11 @@ lodash.filter@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
integrity sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==

lodash.isempty@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==

lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -6032,7 +6098,7 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d"
integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==

"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0:
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0":
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
@@ -6042,7 +6108,7 @@ react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==

react-is@^17.0.1:
react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@@ -6205,17 +6271,17 @@ react-navigation-stack@^2.10.4:
color "^3.1.3"
react-native-iphone-x-helper "^1.3.0"

react-redux@^8.0.5:
version "8.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
integrity sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==
react-redux@^7.2.4:
version "7.2.9"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d"
integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==
dependencies:
"@babel/runtime" "^7.12.1"
"@types/hoist-non-react-statics" "^3.3.1"
"@types/use-sync-external-store" "^0.0.3"
"@babel/runtime" "^7.15.4"
"@types/react-redux" "^7.1.20"
hoist-non-react-statics "^3.3.2"
react-is "^18.0.0"
use-sync-external-store "^1.0.0"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^17.0.2"

react-refresh@^0.4.0:
version "0.4.3"
@@ -6274,17 +6340,19 @@ recast@^0.20.4:
source-map "~0.6.1"
tslib "^2.0.1"

redux-batched-actions@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/redux-batched-actions/-/redux-batched-actions-0.5.0.tgz#d3f0e359b2a95c7d80bab442df450bfafd57d122"
integrity sha512-6orZWyCnIQXMGY4DUGM0oj0L7oYnwTACsfsru/J7r94RM3P9eS7SORGpr3LCeRCMoIMQcpfKZ7X4NdyFHBS8Eg==
redux-saga@^1.1.3:
version "1.2.2"
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.2.2.tgz#4b9b30e022cf94ed1450605e9afd45998c3e8ac1"
integrity sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==
dependencies:
"@redux-saga/core" "^1.2.2"

redux-thunk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==

redux@^4.2.0:
redux@^4.0.0, redux@^4.0.4, redux@^4.1.0, redux@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
@@ -7247,6 +7315,25 @@ type-is@~1.6.17:
media-typer "0.3.0"
mime-types "~2.1.24"

typescript-compare@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425"
integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==
dependencies:
typescript-logic "^0.0.0"

typescript-logic@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196"
integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==

typescript-tuple@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2"
integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==
dependencies:
typescript-compare "^0.0.2"

ua-parser-js@^0.7.30:
version "0.7.32"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211"

Загрузка…
Отмена
Сохранить