From 5bec61950dbe8aff491930e4d1f70296144fb369 Mon Sep 17 00:00:00 2001 From: 0hee0 Date: Sun, 13 Mar 2022 00:46:04 +0900 Subject: [PATCH] [#1] Feat: Implement authentication flow --- react-native/App.tsx | 98 +++++++++++---------- react-native/components/LogoutButton.tsx | 6 +- react-native/contexts/Auth.tsx | 99 +++++++++++++++++++++ react-native/core/utils.ts | 6 +- react-native/package.json | 10 +-- react-native/screens/JoinScreen.tsx | 105 +++++++++++++++-------- react-native/screens/LoginScreen.tsx | 35 ++++---- react-native/services/authService.ts | 38 ++++++++ react-native/types.ts | 28 +++++- react-native/yarn.lock | 81 ++++++++--------- 10 files changed, 345 insertions(+), 161 deletions(-) create mode 100644 react-native/contexts/Auth.tsx create mode 100644 react-native/services/authService.ts diff --git a/react-native/App.tsx b/react-native/App.tsx index 1331499..2344a93 100644 --- a/react-native/App.tsx +++ b/react-native/App.tsx @@ -8,6 +8,8 @@ import AppLoading from 'expo-app-loading'; import useFonts from './hooks/useFonts'; import { theme } from './core/theme'; +import { AuthProvider } from './contexts/Auth'; + import LoginScreen from './screens/LoginScreen'; import JoinScreen from './screens/JoinScreen'; import HomeScreen from './screens/HomeScreen'; @@ -36,52 +38,54 @@ export default function App() { } return ( - - - - - - , - - headerTitle: (props) => ( // App Logo - - ), - }} - /> - - - - - - + + + + + + + , + + headerTitle: (props) => ( // App Logo + + ), + }} + /> + + + + + + + ); } diff --git a/react-native/components/LogoutButton.tsx b/react-native/components/LogoutButton.tsx index ad5cc25..13213aa 100644 --- a/react-native/components/LogoutButton.tsx +++ b/react-native/components/LogoutButton.tsx @@ -2,9 +2,11 @@ import React, { useState } from 'react'; import { TouchableOpacity, Alert } from 'react-native'; import { useNavigation, StackActions } from '@react-navigation/native'; import { AntDesign } from '@expo/vector-icons'; +import { useAuth } from '../contexts/Auth'; const LogoutButton = () => { const navigation = useNavigation(); + const auth = useAuth(); const [showBox, setShowBox] = useState(true); const LogoutConfirm = () => { @@ -15,9 +17,7 @@ const LogoutButton = () => { text: "Yes", onPress: () => { setShowBox(false); - // await fetch('https://fetch.url/logout').then( - // response => { - // console.log(response); + auth.signOut(); navigation.dispatch(StackActions.popToTop()) }, }, { diff --git a/react-native/contexts/Auth.tsx b/react-native/contexts/Auth.tsx new file mode 100644 index 0000000..2a8e546 --- /dev/null +++ b/react-native/contexts/Auth.tsx @@ -0,0 +1,99 @@ +import React, { createContext, useState, useContext, useEffect } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +import { authService } from '../services/authService'; +import { AuthData, JoinData } from '../types'; + +interface AuthContextData { + authData?: AuthData; + loading: boolean; + signUp(data: JoinData): Promise; + signIn(accessToken: string): Promise; + signOut(): void; +}; + +// Create the Auth Context with the data type specified +// and a empty object +const AuthContext = createContext({} as AuthContextData); + +const AuthProvider: React.FC = ({ children }) => { + const [authData, setAuthData] = useState(); + + // the AuthContext start with loading equals true + // and stay like this, until the data be load from Async Storage + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Every time the App is opened, this provider is rendered + // and call de loadStorage function. + loadStorageData(); + }, []); + + async function loadStorageData(): Promise { + try { + //Try get the data from Async Storage + const authDataSerialized = await AsyncStorage.getItem('@AuthData'); + if (authDataSerialized) { + //If there are data, it's converted to an Object and the state is updated. + const _authData: AuthData = JSON.parse(authDataSerialized); + setAuthData(_authData); + } + } catch (error) { + } finally { + // loading finished + setLoading(false); + } + } + + const signUp = async (data: JoinData) => { + const _authData = await authService.signUp(data); + + setAuthData(_authData); + + AsyncStorage.setItem('@AuthData', JSON.stringify(_authData)); + }; + + const signIn = async (accessToken: string) => { + const _authData = await authService.signIn(accessToken); + + // Set the data in the context, so the App can be notified + // and send the user to the AuthStack + setAuthData(_authData); + + // Persist the data in the Async Storage + // to be recovered in the next user session. + AsyncStorage.setItem('@AuthData', JSON.stringify(_authData)); + }; + + const signOut = async () => { + // Remove data from context, so the App can be notified + // and send the user to the AuthStack + setAuthData(undefined); + + // Remove the data from Async Storage + // to NOT be recoverede in next session. + await AsyncStorage.removeItem('@AuthData'); + }; + + return ( + // This component will be used to encapsulate the whole App, + // so all components will have access to the Context + + {children} + + ); +}; + +// A simple hooks to facilitate the access to the AuthContext +// and permit components to subscribe to AuthContext updates +function useAuth(): AuthContextData { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +} + +export {AuthContext, AuthProvider, useAuth}; \ No newline at end of file diff --git a/react-native/core/utils.ts b/react-native/core/utils.ts index 3f94b2e..ea4bcde 100644 --- a/react-native/core/utils.ts +++ b/react-native/core/utils.ts @@ -1,4 +1,4 @@ -export const emailValidator = (email: string) => { +export const emailValidator = (email: string | undefined) => { const re = /\S+@\S+\.\S+/; if (!email || email.length <= 0) return 'Email cannot be empty.'; @@ -7,13 +7,13 @@ export const emailValidator = (email: string) => { return ''; }; -export const passwordValidator = (password: string) => { +export const passwordValidator = (password: string | undefined) => { if (!password || password.length <= 0) return 'Password cannot be empty.'; return ''; }; -export const nameValidator = (name: string) => { +export const nameValidator = (name: string | undefined) => { if (!name || name.length <= 0) return 'Name cannot be empty.'; return ''; diff --git a/react-native/package.json b/react-native/package.json index e858114..2d09f4c 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -11,16 +11,18 @@ }, "dependencies": { "@expo-google-fonts/lora": "^0.2.2", + "@react-native-async-storage/async-storage": "~1.15.0", "@react-navigation/native": "^6.0.8", "@react-navigation/native-stack": "^6.5.0", + "axios": "^0.26.1", "expo": "~44.0.0", "expo-app-loading": "~1.3.0", "expo-application": "^4.0.2", "expo-auth-session": "~3.5.0", "expo-camera": "^12.1.2", "expo-font": "~10.0.4", - "expo-splash-screen": "~0.14.1", "expo-random": "~12.1.1", + "expo-splash-screen": "~0.14.1", "expo-status-bar": "~1.2.0", "expo-web-browser": "~10.1.0", "metro-react-native-babel-preset": "^0.69.0", @@ -39,17 +41,13 @@ "react-native-vector-icons": "^9.1.0", "react-native-web": "0.17.1", "react-navigation": "^4.4.4", - "react-navigation-stack": "^2.10.4", - "react-redux": "^7.2.6", - "redux": "^4.1.2" + "react-navigation-stack": "^2.10.4" }, "devDependencies": { "@babel/core": "^7.12.9", "@types/react": "~17.0.21", "@types/react-native": "~0.64.12", "@types/react-native-dotenv": "^0.2.0", - "@types/react-redux": "^7.1.22", - "@types/redux": "^3.6.0", "typescript": "~4.3.5" }, "private": true diff --git a/react-native/screens/JoinScreen.tsx b/react-native/screens/JoinScreen.tsx index 1c61817..411622e 100644 --- a/react-native/screens/JoinScreen.tsx +++ b/react-native/screens/JoinScreen.tsx @@ -1,54 +1,83 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { StyleSheet, View, KeyboardAvoidingView, Alert, Platform, ScrollView, Image, GestureResponderEvent } from 'react-native'; import { FormControl, Input, Button, VStack, Select, CheckIcon } from 'native-base'; import { nameValidator } from '../core/utils'; -import type { Navigation } from '../types'; +import type { Navigation, AuthData, JoinData } from '../types'; import { theme } from '../core/theme'; +import { useAuth } from '../contexts/Auth'; export default function JoinScreen({ navigation }: Navigation) { - const [profileImg, setProfileImg] = useState(1); - const [username, setUsername] = useState(''); - const [language, setLanguage] = useState(''); const [childrenNumber, setChildrenNumber] = useState('1'); - const [childrenName, setChildrenName] = useState([]); const imgSource = [require(`../assets/images/profile-images/profile-1.png`), require(`../assets/images/profile-images/profile-2.png`), require(`../assets/images/profile-images/profile-3.png`), require(`../assets/images/profile-images/profile-4.png`), require(`../assets/images/profile-images/profile-5.png`), require(`../assets/images/profile-images/profile-6.png`), require(`../assets/images/profile-images/profile-7.png`)]; - const childrenColor = [theme.colors.primary, theme.colors.secondary, theme.colors.skyblue, theme.colors.coral, theme.colors.gray, '#000'] - + const colors = [theme.colors.primary, theme.colors.secondary, theme.colors.skyblue, theme.colors.coral, theme.colors.gray, '#000'] + const [joinForm, setJoinForm] = useState({ + uid: undefined, + uprofileImg: 1, + username: '', + ulanguage: '', + uchildren: colors.map(color => ({'cname': '', 'color': color})) + }) + + const [user, setUser] = useState(); + const auth = useAuth(); + + useEffect(() => { + if (auth?.authData?.uroleType==='USER') { + Alert.alert( + "Success", + "Congratulations, your account has been successfully created." + ) + navigation.navigate('Home'); + } + else if (auth?.authData?.uroleType==='GUEST') { + setUser(auth?.authData); + } + }, [auth]); + + useEffect(() => { + if (user?.username) { + setJoinForm({ ...joinForm, ['username']: user.username }); + } + }, [user]); + const errorAlert = (error: string) => Alert.alert( - "Join Failed", - error, - [ - { text: "OK", onPress: () => console.log("OK Pressed") } - ] + "Join Failed", + error, + [ + { text: "OK", onPress: () => console.log("OK Pressed") } + ] ); const handleProfileImg = (profileType: number) => (event: GestureResponderEvent) => { - setProfileImg(profileType); + setJoinForm({ ...joinForm, ['uprofileImg']: profileType }); } - const handleChildName = (childNum: number, text: string) => { - let array = [...childrenName]; - array[childNum] = text; - setChildrenName(array); + const handleChildren = (childNum: number, text: string) => { + let array = joinForm?.uchildren; + if (array) array[childNum].cname = text; + setJoinForm({ ...joinForm, ['uchildren']: array }); } const onJoinPressed = () => { - const usernameError = nameValidator(username); - const childrenNameError = childrenName.length !== Number(childrenNumber); + if (user && joinForm) { + joinForm.uid = user?.uid; - if (usernameError || childrenNameError || !language) { - console.log(usernameError); - errorAlert("Please fill in all the blanks!"); - return; - } + let childrenArr = joinForm?.uchildren; + childrenArr = childrenArr?.slice(0, Number(childrenNumber)); + joinForm.uchildren = childrenArr; - Alert.alert( - "Success", - "Congratulations, your account has been successfully created." - ) - navigation.navigate('Home'); + const usernameError = nameValidator(joinForm.username); + const childrenNameError = joinForm.uchildren?.some(child => child.cname === ''); + + if (usernameError || childrenNameError || !joinForm.ulanguage) { + errorAlert("Please fill in all the blanks!"); + return; + } + + auth.signUp(joinForm); + } }; return ( @@ -60,7 +89,7 @@ export default function JoinScreen({ navigation }: Navigation) { {Array(7).fill(1).map((num, index) => )} @@ -69,8 +98,8 @@ export default function JoinScreen({ navigation }: Navigation) { Username setUsername(text)} + value={joinForm.username} + onChangeText={(text) => setJoinForm({ ...joinForm, ['username']: text })} autoFocus autoCapitalize="none" returnKeyType={"next"} @@ -78,8 +107,8 @@ export default function JoinScreen({ navigation }: Navigation) { Select Your Language - { + setJoinForm({ ...joinForm, ['ulanguage']: itemValue }) }} _selectedItem={{ bg: "skyblue.500", endIcon: @@ -121,12 +150,12 @@ export default function JoinScreen({ navigation }: Navigation) { key={'i_'+index} size="md" variant="underlined" - value={childrenName[index]} - onChangeText={(text) => handleChildName(index, text)} + value={joinForm?.uchildren && joinForm.uchildren[index]?.cname} + onChangeText={(text) => handleChildren(index, text)} autoCapitalize="none" mb={2} InputRightElement={ - } diff --git a/react-native/screens/LoginScreen.tsx b/react-native/screens/LoginScreen.tsx index 216dce4..e7f7d13 100644 --- a/react-native/screens/LoginScreen.tsx +++ b/react-native/screens/LoginScreen.tsx @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; -import { StyleSheet, View, KeyboardAvoidingView, Image, Platform, TouchableOpacity } from 'react-native'; +import { StyleSheet, View, KeyboardAvoidingView, Image, Platform, TouchableOpacity, Alert } from 'react-native'; import { Button, Text } from 'native-base'; import { theme } from '../core/theme'; import type { Navigation } from '../types'; import * as WebBrowser from 'expo-web-browser'; import * as Google from 'expo-auth-session/providers/google'; import { GOOGLE_CLIENT_ID_WEB } from '@env'; +import { useAuth } from '../contexts/Auth'; WebBrowser.maybeCompleteAuthSession(); @@ -16,30 +17,34 @@ export default function LoginScreen({ navigation }: Navigation) { webClientId: GOOGLE_CLIENT_ID_WEB, // responseType: 'id_token' }) + const auth = useAuth(); useEffect(() => { // WebBrowser.dismissAuthSession(); if (response?.type === 'success') { - const { authentication } = response; - // console.log('success!') - console.log(authentication); - // console.log(response.params); - // TODO: fetch API - // TEST - let status = 'join'; - switch(status) { - case 'login': // if account exists - navigation.navigate('Home'); - case 'join': // if no account - navigation.navigate('Join'); - } + const { authentication } = response; + + if (authentication) { + auth.signIn(authentication?.accessToken); + } + else { + Alert.alert("Authentication failed. Please try again."); + } } else { - console.log('fail') console.log(response) } }, [response]); + useEffect(() => { + console.log('auth',auth?.authData) + if (auth?.authData?.uroleType === 'GUEST') { + navigation.navigate('Join'); + } else if (auth?.authData?.uroleType === 'USER') { + navigation.navigate('Home'); + } + }, [auth?.authData]); + const onLoginPressed = () => { navigation.navigate('Home'); }; diff --git a/react-native/services/authService.ts b/react-native/services/authService.ts new file mode 100644 index 0000000..8f6735a --- /dev/null +++ b/react-native/services/authService.ts @@ -0,0 +1,38 @@ +import axios from 'axios'; +import type { AuthData, JoinData } from '../types'; + + +const signIn = (accessToken: string): Promise => { + return new Promise((resolve, reject) => { + axios.get('http://localhost:8080/login/oauth2', { + headers: { + "Authorization": accessToken + } + }) + .then(response => { + console.log(response.data); + resolve(response.data) + }) + .catch(err => { + reject(err) + }) + }); +}; + +const signUp = (data: JoinData): Promise => { + return new Promise((resolve, reject) => { + axios.post('http://localhost:8080/join', data) + .then(response => { + console.log(response.data); + resolve(response.data); + }) + .catch(err => { + console.log(err); + reject(err) + }) + }) +} + +export const authService = { + signIn, signUp +}; diff --git a/react-native/types.ts b/react-native/types.ts index 9df87d5..6d7eceb 100644 --- a/react-native/types.ts +++ b/react-native/types.ts @@ -18,6 +18,29 @@ export type TextInput = { description: string; } +interface Children { + cid?: number, + cname?: string, + color?: string, +} + +interface JoinData { + uid?: number, + uprofileImg?: number, + username?: string, + ulanguage?: string, + uchildren?: Children[] +} + +interface AuthData extends JoinData { + uemail?: string | undefined, + uproviderType?: string | undefined, + uroleType?: string | undefined, + + jwt_token?: string, + refresh_token?: string, +}; + interface Result { id: number, summary: {id: number, content: string, highlight: boolean, registered: boolean}[], @@ -41,7 +64,7 @@ interface Notice { } } -export interface UserProfile { +interface UserProfile { userId: number; username: string; gmail: string; @@ -62,5 +85,6 @@ interface BottomDrawerProps { } export type { - Result, Notice, BottomDrawerProps + AuthData, JoinData, Children, + Result, Notice, UserProfile, BottomDrawerProps } diff --git a/react-native/yarn.lock b/react-native/yarn.lock index e16b59a..7783571 100644 --- a/react-native/yarn.lock +++ b/react-native/yarn.lock @@ -1034,7 +1034,7 @@ pirates "^4.0.5" source-map-support "^0.5.16" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.14.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.17.2" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1812,6 +1812,13 @@ "@react-aria/ssr" "^3.0.1" "@react-aria/utils" "^3.3.0" +"@react-native-async-storage/async-storage@~1.15.0": + version "1.15.17" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.15.17.tgz#0dae263a52e476ffce871086f1fef5b8e44708eb" + integrity sha512-NQCFs47aFEch9kya/bqwdpvSdZaVRtzU7YB02L8VrmLSLpKgQH/1VwzFUBPcc1/JI1s3GU4yOLVrEbwxq+Fqcw== + dependencies: + merge-options "^3.0.4" + "@react-native-community/cli-debugger-ui@^5.0.1": version "5.0.1" resolved "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-5.0.1.tgz" @@ -2343,14 +2350,6 @@ dependencies: "@types/node" "*" -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.4" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" @@ -2419,16 +2418,6 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.20", "@types/react-redux@^7.1.22": - version "7.1.22" - resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.22.tgz" - integrity sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - "@types/react@*", "@types/react@~17.0.21": version "17.0.39" resolved "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz" @@ -2438,13 +2427,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/redux@^3.6.0": - version "3.6.0" - resolved "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz" - integrity sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo= - dependencies: - redux "*" - "@types/scheduler@*": version "0.16.2" resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" @@ -2642,6 +2624,13 @@ atob@^2.1.2: resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz" @@ -3902,6 +3891,11 @@ flow-parser@0.*, flow-parser@^0.121.0: resolved "https://registry.npmjs.org/flow-parser/-/flow-parser-0.121.0.tgz" integrity sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg== +follow-redirects@^1.14.8: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + fontfaceobserver@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.1.0.tgz" @@ -4112,7 +4106,7 @@ hoist-non-react-statics@^2.3.1: resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4338,6 +4332,11 @@ is-plain-obj@^1.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" @@ -4830,6 +4829,13 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -5838,7 +5844,7 @@ react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1, react-is@^17.0.2: +react-is@^17.0.1: version "17.0.2" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -6018,18 +6024,6 @@ react-navigation@^4.4.4: "@react-navigation/core" "^3.7.9" "@react-navigation/native" "^3.8.4" -react-redux@^7.2.6: - version "7.2.6" - resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz" - integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== - dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" - react-refresh@^0.4.0: version "0.4.3" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz" @@ -6073,13 +6067,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redux@*, redux@^4.0.0, redux@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz" - integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== - dependencies: - "@babel/runtime" "^7.9.2" - regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz"