3 Commits

Author SHA1 Message Date
mathysPaul
6e71aff8a9 Setting base setup 2023-09-18 15:27:37 +02:00
mathysPaul
a927d9783e Interactive component setup 2023-09-17 23:35:19 +02:00
mathysPaul
4de28337a3 redesign: signin & signup for the follow up 2023-07-30 02:37:07 +09:00
19 changed files with 1404 additions and 36 deletions

View File

@@ -65,7 +65,7 @@ export default class API {
public static readonly baseUrl = public static readonly baseUrl =
process.env.NODE_ENV != 'development' && Platform.OS === 'web' process.env.NODE_ENV != 'development' && Platform.OS === 'web'
? '/api' ? '/api'
: Constants.manifest?.extra?.apiUrl; : "https://nightly.chroma.octohub.app/api";
public static async fetch( public static async fetch(
params: FetchParams, params: FetchParams,
handle: Pick<Required<HandleParams>, 'raw'> handle: Pick<Required<HandleParams>, 'raw'>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { useEffect } from 'react';
import store, { persistor } from './state/Store'; import store, { persistor } from './state/Store';
import { Router } from './Navigation'; import { Router } from './Navigation';
import './i18n/i18n'; import './i18n/i18n';
@@ -10,12 +11,25 @@ import LanguageGate from './i18n/LanguageGate';
import ThemeProvider, { ColorSchemeProvider } from './Theme'; import ThemeProvider, { ColorSchemeProvider } from './Theme';
import 'react-native-url-polyfill/auto'; import 'react-native-url-polyfill/auto';
import { QueryRules } from './Queries'; import { QueryRules } from './Queries';
import { useFonts } from 'expo-font';
const queryClient = new QueryClient(QueryRules); const queryClient = new QueryClient(QueryRules);
export default function App() { export default function App() {
// SplashScreen.preventAutoHideAsync();
// setTimeout(SplashScreen.hideAsync, 500);
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
setTimeout(SplashScreen.hideAsync, 500);
const [fontsLoaded] = useFonts({
'Lexend': require('./assets/fonts/Lexend-VariableFont_wght.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
return ( return (
<Provider store={store}> <Provider store={store}>

View File

@@ -12,6 +12,8 @@ import { useDispatch } from 'react-redux';
import { Translate, translate } from './i18n/i18n'; import { Translate, translate } from './i18n/i18n';
import SongLobbyView from './views/SongLobbyView'; import SongLobbyView from './views/SongLobbyView';
import AuthenticationView from './views/AuthenticationView'; import AuthenticationView from './views/AuthenticationView';
import SigninView from './views/SigninView';
import SignupView from './views/SignupView';
import StartPageView from './views/StartPageView'; import StartPageView from './views/StartPageView';
import HomeView from './views/HomeView'; import HomeView from './views/HomeView';
import SearchView from './views/SearchView'; import SearchView from './views/SearchView';
@@ -84,17 +86,27 @@ const publicRoutes = () =>
link: '/', link: '/',
}, },
Login: { Login: {
component: (params: RouteProps<{}>) => component: SigninView,
AuthenticationView({ isSignup: false, ...params }), options: { title: translate('signInBtn'), headerShown: false },
options: { title: translate('signInBtn') },
link: '/login', link: '/login',
}, },
Signup: { Signup: {
component: (params: RouteProps<{}>) => component: SignupView,
AuthenticationView({ isSignup: true, ...params }), options: { title: translate('signUpBtn'), headerShown: false },
options: { title: translate('signUpBtn') },
link: '/signup', link: '/signup',
}, },
// Login: {
// component: (params: RouteProps<{}>) =>
// AuthenticationView({ isSignup: false, ...params }),
// options: { title: translate('signInBtn') },
// link: '/login',
// },
// Signup: {
// component: (params: RouteProps<{}>) =>
// AuthenticationView({ isSignup: true, ...params }),
// options: { title: translate('signUpBtn') },
// link: '/signup',
// },
Oops: { Oops: {
component: ProfileErrorView, component: ProfileErrorView,
options: { title: 'Oops', headerShown: false }, options: { title: 'Oops', headerShown: false },

View File

@@ -12,6 +12,11 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
useSystemColorMode: false, useSystemColorMode: false,
initialColorMode: colorScheme, initialColorMode: colorScheme,
}, },
fonts: {
heading: "Lexend",
body: "Lexend",
mono: "Lexend",
},
colors: { colors: {
primary: { primary: {
50: '#e6faea', 50: '#e6faea',

BIN
front/assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

View File

@@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { StyleSheet, ActivityIndicator, View, Image } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
import InteractiveBase from './InteractiveBase';
import { Text, useTheme } from 'native-base';
// import { BlurView } from 'expo-blur';
interface ButtonProps {
title?: string;
onPress?: () => Promise<any>;
isDisabled?: boolean;
icon?: string;
iconImage?: string;
isCollapsed?: boolean;
type: 'filled' | 'outlined' | 'menu' | 'submenu';
}
const ButtonBase: React.FC<ButtonProps> = ({ title, onPress, isDisabled, icon, iconImage, isCollapsed, type = 'filled'}) => {
const { colors } = useTheme();
const [loading, setLoading] = useState(false);
const styleButton = StyleSheet.create({
Default: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: colors.primary[400],
},
onHover: {
scale: 1.02,
shadowOpacity: 0.37,
shadowRadius: 7.49,
elevation: 12,
backgroundColor: colors.primary[500],
},
onPressed: {
scale: 0.98,
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
backgroundColor: colors.primary[600],
},
Disabled: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: colors.primary[400],
}
});
const styleMenu = StyleSheet.create({
Default: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: '#ff0000',
},
onHover: {
scale: 1.01,
shadowOpacity: 0.37,
shadowRadius: 7.49,
elevation: 12,
backgroundColor: '#0ff000',
},
onPressed: {
scale: 0.99,
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
backgroundColor: '#000',
},
Disabled: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: '#0000',
}
});
const typeToStyleAnimator = {'filled': styleButton,'outlined': styleButton,'menu': styleMenu,'submenu': styleButton};
return (
<InteractiveBase
style={styles.container}
styleAnimate={typeToStyleAnimator[type]}
onPress={async () => {
if (onPress && !isDisabled) {
setLoading(true);
await onPress();
setLoading(false);
}
}}
isDisabled={isDisabled}
isOutlined={type === 'outlined'}
>
{loading ? (
<ActivityIndicator size="small" color={type === 'outlined' ? '#6075F9' : '#FFFFFF'} />
) : (
<View style={styles.content}>
{icon && <Ionicons name={icon} size={18} color={type === 'outlined' ? '#ff0000' : '#FFFFFF'} />}
{iconImage && <Image source={{uri: iconImage}} style={styles.icon} />}
{title && <Text style={styles.text}>{title}</Text>}
</View>
)}
</InteractiveBase>
);
};
const styleAnimate = StyleSheet.create({
Default: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: '#00ff00',
},
onHover: {
scale: 1.01,
shadowOpacity: 0.37,
shadowRadius: 7.49,
elevation: 12,
backgroundColor: '#0000ff',
},
onPressed: {
scale: 0.99,
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
backgroundColor: '#ff0000',
},
Disabled: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: '#000000',
}
});
const styles = StyleSheet.create({
container: {
borderRadius: 8,
},
content: {
padding: 10,
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
},
icon: {
width: 18,
height: 18,
// marginRight: 8,
},
text: {
color: '#fff',
marginHorizontal: 8
},
});
export default ButtonBase;

View File

@@ -0,0 +1,242 @@
import React, { useRef, useState } from 'react';
import { Animated, StyleSheet, TouchableOpacity, ActivityIndicator, View, Image, StyleProp, ViewStyle } from 'react-native';
// import Ionicons from '@expo/vector-icons/Ionicons';
// import { Text, useTheme } from 'native-base'
// import { BlurView } from '@react-native-community/blur';
interface InteractiveBaseProps {
children?: React.ReactNode;
onPress?: () => Promise<any>;
isDisabled?: boolean;
isOutlined?: boolean;
style?: StyleProp<ViewStyle>,
styleAnimate: {
Default: {
scale: number,
shadowOpacity: number,
shadowRadius: number,
elevation: number,
backgroundColor: string,
},
onHover: {
scale: number,
shadowOpacity: number,
shadowRadius: number,
elevation: number,
backgroundColor: string,
},
onPressed: {
scale: number,
shadowOpacity: number,
shadowRadius: number,
elevation: number,
backgroundColor: string,
},
Disabled: {
scale: number,
shadowOpacity: number,
shadowRadius: number,
elevation: number,
backgroundColor: string,
}
}
}
const InteractiveBase: React.FC<InteractiveBaseProps> = ({ children, onPress, style, styleAnimate, isDisabled = false, isOutlined = false }) => {
const scaleAnimator = useRef(new Animated.Value(1)).current;
const scaleValue = scaleAnimator.interpolate({
inputRange: [0, 1, 2],
outputRange: [styleAnimate.Default.scale, styleAnimate.onHover.scale, styleAnimate.onPressed.scale],
});
const shadowOpacityAnimator = useRef(new Animated.Value(0)).current;
const shadowOpacityValue = shadowOpacityAnimator.interpolate({
inputRange: [0, 1, 2],
outputRange: [styleAnimate.Default.shadowOpacity, styleAnimate.onHover.shadowOpacity, styleAnimate.onPressed.shadowOpacity],
});
const shadowRadiusAnimator = useRef(new Animated.Value(0)).current;
const shadowRadiusValue = shadowRadiusAnimator.interpolate({
inputRange: [0, 1, 2],
outputRange: [styleAnimate.Default.shadowRadius, styleAnimate.onHover.shadowRadius, styleAnimate.onPressed.shadowRadius],
});
const elevationAnimator = useRef(new Animated.Value(0)).current;
const elevationValue = elevationAnimator.interpolate({
inputRange: [0, 1, 2],
outputRange: [styleAnimate.Default.elevation, styleAnimate.onHover.elevation, styleAnimate.onPressed.elevation],
});
const backgroundColorAnimator = useRef(new Animated.Value(0)).current;
const backgroundColorValue = backgroundColorAnimator.interpolate({
inputRange: [0, 1, 2],
outputRange: [styleAnimate.Default.backgroundColor, styleAnimate.onHover.backgroundColor, styleAnimate.onPressed.backgroundColor],
});
// Mouse Enter
const handleMouseEnter = () => {
Animated.parallel([
Animated.spring(scaleAnimator, {
toValue: 1,
useNativeDriver: true,
}),
Animated.timing(backgroundColorAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowRadiusAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowOpacityAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(elevationAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
]).start();
}
// Mouse Down
const handlePressIn = () => {
Animated.parallel([
Animated.spring(scaleAnimator, {
toValue: 2,
useNativeDriver: true,
}),
Animated.timing(backgroundColorAnimator, {
toValue: 2,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowRadiusAnimator, {
toValue: 2,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowOpacityAnimator, {
toValue: 2,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(elevationAnimator, {
toValue: 2,
duration: 250,
useNativeDriver: false,
}),
]).start();
};
// Mouse Up
const handlePressOut = async () => {
Animated.parallel([
Animated.spring(scaleAnimator, {
toValue: 1,
useNativeDriver: true,
}),
Animated.timing(backgroundColorAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowRadiusAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowOpacityAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(elevationAnimator, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}),
]).start();
if (onPress && !isDisabled) {
await onPress();
}
}
// Mouse Leave
const handleMouseLeave = () => {
Animated.parallel([
Animated.spring(scaleAnimator, {
toValue: 0,
useNativeDriver: true,
}),
Animated.timing(backgroundColorAnimator, {
toValue: 0,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowRadiusAnimator, {
toValue: 0,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(shadowOpacityAnimator, {
toValue: 0,
duration: 250,
useNativeDriver: false,
}),
Animated.timing(elevationAnimator, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
]).start();
}
return (
<Animated.View
style={[
style,
isDisabled ? styleAnimate.Disabled : {
backgroundColor: isOutlined ? 'rgba(0,0,0,0.3)' : backgroundColorValue,
borderColor: isOutlined ? backgroundColorValue : 'transparent',
borderWidth: 2,
transform: [{ scale: scaleValue }],
shadowOpacity: shadowOpacityValue,
shadowRadius: shadowRadiusValue,
elevation: elevationValue,
},
]}
>
<TouchableOpacity
activeOpacity={1}
disabled={isDisabled}
onMouseEnter={handleMouseEnter}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onMouseLeave={handleMouseLeave}
style={styles.container}
>
{children}
{/* <BlurView
style={{
width: '420px',
height: '50px',
borderRadius: 20,
borderWidth: 1,
}}
blurType="light"
blurAmount={20}
blurRadius={5}
/> */}
</TouchableOpacity>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
height: '100%',
},
});
export default InteractiveBase;

View File

@@ -0,0 +1,23 @@
import React, { ReactNode, FunctionComponent } from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
linkText: {
textDecorationLine: 'underline',
color: '#A3AFFC',
fontWeight: '700',
},
});
interface LinkBaseProps {
children: ReactNode;
onPress: () => void;
}
const LinkBase: FunctionComponent<LinkBaseProps> = ({ children, onPress }) => (
<TouchableOpacity onPress={onPress}>
<Text style={styles.linkText}>{children}</Text>
</TouchableOpacity>
);
export default LinkBase;

View File

@@ -0,0 +1,34 @@
import React, { FunctionComponent, ReactNode } from 'react';
import { View, Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
line: {
flex: 1,
height: 2,
backgroundColor: 'white',
},
container: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
},
text: {
color: 'white',
paddingHorizontal: 16,
},
});
interface SeparatorBaseProps {
children: ReactNode;
}
const SeparatorBase: FunctionComponent<SeparatorBaseProps> = ({children}) => (
<View style={styles.container}>
<View style={styles.line} />
<Text style={styles.text}>{children}</Text>
<View style={styles.line} />
</View>
);
export default SeparatorBase;

View File

@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { StyleSheet, ActivityIndicator, View, Image } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
import InteractiveBase from './InteractiveBase';
import { Text, useTheme } from 'native-base';
// import { BlurView } from 'expo-blur';
interface SettingProps {
icon: string;
title: string;
description?: string;
onPress?: () => Promise<any>;
children?: React.ReactNode;
}
const SettingBase: React.FC<SettingProps> = ({ title, description, onPress, icon, children}) => {
const styleSetting = StyleSheet.create({
Default: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: 'rgba(16, 16, 20, 0.50)',
},
onHover: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: 'rgba(32, 32, 40, 0.50)',
},
onPressed: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: 'rgba(16, 16, 20, 0.50)',
},
Disabled: {
scale: 1,
shadowOpacity: 0.30,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: 'rgba(16, 16, 20, 0.50)',
}
});
return (
<InteractiveBase
style={[styles.container, {width: '100%'}]}
styleAnimate={styleSetting}
onPress={async () => {
if (onPress) {
await onPress();
}
}}
>
<View style={styles.content}>
<Ionicons name={icon} size={24} color="#fff"/>
<View style={styles.info}>
<Text style={styles.text}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
{children}
</View>
</InteractiveBase>
);
};
const styles = StyleSheet.create({
container: {
borderRadius: 8,
backgroundColor: 'rgba(16, 16, 20, 0.50)',
},
content: {
paddingVertical: 10,
paddingHorizontal: 20,
justifyContent: 'space-between',
alignItems: 'center',
alignSelf: 'stretch',
flexDirection: 'row',
},
info: {
flexDirection: 'column',
marginHorizontal: 16,
flex: 1,
},
text: {
color: '#fff',
fontSize: 16,
},
description: {
color: '#fff',
fontSize: 10,
},
});
export default SettingBase;

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; // Supposons que nous utilisons la bibliothèque Ionicons pour les icônes
export interface TextFieldBaseProps {
value?: string;
icon?: string;
iconColor?: string;
placeholder?: string;
autoComplete?:
| 'birthdate-day'
| 'birthdate-full'
| 'birthdate-month'
| 'birthdate-year'
| 'cc-csc'
| 'cc-exp'
| 'cc-exp-day'
| 'cc-exp-month'
| 'cc-exp-year'
| 'cc-number'
| 'email'
| 'gender'
| 'name'
| 'name-family'
| 'name-given'
| 'name-middle'
| 'name-middle-initial'
| 'name-prefix'
| 'name-suffix'
| 'password'
| 'password-new'
| 'postal-address'
| 'postal-address-country'
| 'postal-address-extended'
| 'postal-address-extended-postal-code'
| 'postal-address-locality'
| 'postal-address-region'
| 'postal-code'
| 'street-address'
| 'sms-otp'
| 'tel'
| 'tel-country-code'
| 'tel-national'
| 'tel-device'
| 'username'
| 'username-new'
| 'off'
| undefined;
isSecret?: boolean;
isRequired?: boolean;
onChangeText?: ((text: string) => void) | undefined;
}
const TextFieldBase: React.FC<TextFieldBaseProps> = ({ placeholder = '', icon, iconColor, autoComplete = 'off', isSecret = false, isRequired = false, ...props }) => {
const [isPasswordVisible, setPasswordVisible] = useState(!isSecret);
const [isFocused, setFocused] = useState(false);
return (
<View style={styles.container}>
<View style={styles.iconContainerLeft}>
{icon && <Icon name={icon} size={24} color={iconColor ? iconColor : (isFocused ? '#6075F9' : '#454562')} />}
</View>
<TextInput
style={styles.input}
autoComplete={autoComplete}
placeholder={placeholder + (isRequired ? '*' : '')}
placeholderTextColor='#454562'
secureTextEntry={isSecret ? !isPasswordVisible : false}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
{...props}
/>
{isSecret && (
<TouchableOpacity style={styles.iconContainerRight} onPress={() => setPasswordVisible(prevState => !prevState)}>
<Icon name={isPasswordVisible ? 'eye-off' : 'eye'} size={24} color='#454562' />
</TouchableOpacity>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#22222D',
borderRadius: 16,
},
input: {
flex: 1,
color: '#ffffff',
paddingHorizontal: 16 + 24 + 16,
paddingVertical: 16,
outlineStyle: 'none',
},
iconContainerLeft: {
position: 'absolute',
left: 16,
zIndex: 1,
},
iconContainerRight: {
position: 'absolute',
outlineStyle: 'none',
right: 16,
zIndex: 1,
},
});
export default TextFieldBase;

View File

@@ -0,0 +1,183 @@
import React, { useState, useEffect } from 'react';
import { View, TextInput, TouchableOpacity, StyleSheet, Text, Animated } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; // Supposons que nous utilisons la bibliothèque Ionicons pour les icônes
interface TextFormFieldProps {
value?: string;
icon?: string;
placeholder?: string;
autoComplete?:
| 'birthdate-day'
| 'birthdate-full'
| 'birthdate-month'
| 'birthdate-year'
| 'cc-csc'
| 'cc-exp'
| 'cc-exp-day'
| 'cc-exp-month'
| 'cc-exp-year'
| 'cc-number'
| 'email'
| 'gender'
| 'name'
| 'name-family'
| 'name-given'
| 'name-middle'
| 'name-middle-initial'
| 'name-prefix'
| 'name-suffix'
| 'password'
| 'password-new'
| 'postal-address'
| 'postal-address-country'
| 'postal-address-extended'
| 'postal-address-extended-postal-code'
| 'postal-address-locality'
| 'postal-address-region'
| 'postal-code'
| 'street-address'
| 'sms-otp'
| 'tel'
| 'tel-country-code'
| 'tel-national'
| 'tel-device'
| 'username'
| 'username-new'
| 'off'
| undefined;
isSecret?: boolean;
isRequired?: boolean;
error?: string;
onChangeText?: ((text: string) => void) | undefined;
}
const ERROR_HEIGHT = 20;
const ERROR_PADDING_TOP = 8;
const TextFormField: React.FC<TextFormFieldProps> = ({ value = '', placeholder = '', icon, autoComplete = 'off', isSecret = false, isRequired = false, error, ...props }) => {
const [isPasswordVisible, setPasswordVisible] = useState(!isSecret);
const [isFocused, setFocused] = useState(false);
const [fieldValue, setFieldValue] = useState(value);
const fadeAnim = React.useRef(new Animated.Value(0)).current; // Initial value for opacity: 0
const heightAnim = React.useRef(new Animated.Value(0)).current; // Initial value for height: 0
const paddingTopAnim = React.useRef(new Animated.Value(0)).current; // Initial value for paddingTop: 0
// Update fieldValue whenever value changes
useEffect(() => {
setFieldValue(value);
}, [value]);
// Animate the error message
useEffect(() => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: error ? 1 : 0,
duration: 500,
useNativeDriver: true
}
),
Animated.timing(
heightAnim,
{
toValue: error ? ERROR_HEIGHT : 0,
duration: 250,
useNativeDriver: false // height cannot be animated using native driver
}
),
Animated.timing(
paddingTopAnim,
{
toValue: error ? ERROR_PADDING_TOP : 0,
duration: 150,
useNativeDriver: false // paddingTop cannot be animated using native driver
}
),
]).start();
}, [error]);
const handleTextChange = (text: string) => {
setFieldValue(text);
if (props.onChangeText) {
props.onChangeText(text);
}
};
return (
<View style={styles.wrapper}>
<View style={[styles.container, error && styles.error, isFocused && styles.containerFocused]}>
<View style={styles.iconContainerLeft}>
{icon && <Icon name={icon} size={24} color={error ? 'red' : (isFocused ? '#6075F9' : '#454562')} />}
</View>
<TextInput
value={fieldValue}
style={styles.input}
autoComplete={autoComplete}
placeholder={placeholder + (isRequired ? '*' : '')}
placeholderTextColor='#454562'
secureTextEntry={isSecret ? !isPasswordVisible : false}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onChangeText={handleTextChange}
{...props}
/>
{isSecret && (
<TouchableOpacity style={styles.iconContainerRight} onPress={() => setPasswordVisible(prevState => !prevState)}>
<Icon name={isPasswordVisible ? 'eye-off' : 'eye'} size={24} color='#454562' />
</TouchableOpacity>
)}
</View>
<Animated.View style={{...styles.errorContainer, opacity: fadeAnim, height: heightAnim, paddingTop: paddingTopAnim}}>
<Icon name="warning" size={16} color='red' />
<Text style={styles.errorText}>{error}</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: '100%',
},
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#22222D',
borderRadius: 16,
},
error: {
},
containerFocused: {
},
input: {
flex: 1,
color: '#ffffff',
paddingHorizontal: 16 + 24 + 16,
paddingVertical: 16,
outlineStyle: 'none',
},
iconContainerLeft: {
position: 'absolute',
left: 16,
zIndex: 1,
},
iconContainerRight: {
position: 'absolute',
outlineStyle: 'none',
right: 16,
zIndex: 1,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 40,
},
errorText: {
color: 'red',
fontSize: 12,
marginLeft: 8,
},
});
export default TextFormField;

View File

@@ -0,0 +1,77 @@
import React, { useEffect } from 'react';
import { View, Text, Animated, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import TextFieldBase, { TextFieldBaseProps } from './TextFieldBase';
interface TextFormFieldProps extends TextFieldBaseProps {
error: string | null;
}
const ERROR_HEIGHT = 20;
const ERROR_PADDING_TOP = 8;
const TextFormField: React.FC<TextFormFieldProps> = ({ error, ...textFieldBaseProps }) => {
const fadeAnim = React.useRef(new Animated.Value(0)).current;
const heightAnim = React.useRef(new Animated.Value(0)).current;
const paddingTopAnim = React.useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: error ? 1 : 0,
duration: 150,
useNativeDriver: true
}
),
Animated.timing(
heightAnim,
{
toValue: error ? ERROR_HEIGHT : 0,
duration: 250,
useNativeDriver: false
}
),
Animated.timing(
paddingTopAnim,
{
toValue: error ? ERROR_PADDING_TOP : 0,
duration: 250,
useNativeDriver: false
}
),
]).start();
}, [error]);
return (
<View style={styles.wrapper}>
<TextFieldBase
iconColor={error ? '#f7253d' : undefined}
{...textFieldBaseProps}
/>
<Animated.View style={{...styles.errorContainer, opacity: fadeAnim, height: heightAnim, paddingTop: paddingTopAnim}}>
<Icon name="alert-circle" size={16} color='#f7253d' />
<Text style={styles.errorText}>{error}</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: '100%',
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 16,
},
errorText: {
color: '#f7253d',
fontSize: 12,
marginLeft: 8,
},
});
export default TextFormField;

View File

@@ -5,6 +5,8 @@ import { Translate, translate } from '../../i18n/i18n';
import { string } from 'yup'; import { string } from 'yup';
import { FormControl, Input, Stack, WarningOutlineIcon, Box, useToast } from 'native-base'; import { FormControl, Input, Stack, WarningOutlineIcon, Box, useToast } from 'native-base';
import TextButton from '../TextButton'; import TextButton from '../TextButton';
import TextFormField from '../UI/TextFormField';
import ButtonBase from '../UI/ButtonBase';
interface SigninFormProps { interface SigninFormProps {
onSubmit: (username: string, password: string) => Promise<string>; onSubmit: (username: string, password: string) => Promise<string>;
@@ -21,8 +23,6 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
error: null as string | null, error: null as string | null,
}, },
}); });
const [submittingForm, setSubmittingForm] = React.useState(false);
const validationSchemas = { const validationSchemas = {
username: string() username: string()
.min(3, translate('usernameTooShort')) .min(3, translate('usernameTooShort'))
@@ -35,18 +35,15 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
}; };
const toast = useToast(); const toast = useToast();
return ( return (
<Box alignItems="center" style={{ width: '100%' }}> <Box alignItems="center" style={{ width: '100%', backgroundColor: "#101014" }}>
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}> <Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl <FormControl
isRequired isRequired
isInvalid={formData.username.error !== null || formData.password.error !== null} isInvalid={formData.username.error !== null || formData.password.error !== null}
> >
<FormControl.Label> <TextFormField
<Translate translationKey="username" /> error={formData.username.error}
</FormControl.Label> icon='person'
<Input
isRequired
type="text"
placeholder="Username" placeholder="Username"
autoComplete="username" autoComplete="username"
value={formData.username.value} value={formData.username.value}
@@ -59,16 +56,14 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
setFormData({ ...formData, username: { value: t, error } }); setFormData({ ...formData, username: { value: t, error } });
}); });
}} }}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey="password" />
</FormControl.Label>
<Input
isRequired isRequired
type="password" />
<TextFormField
isRequired
isSecret
error={formData.password.error}
icon='lock-closed'
placeholder="Password"
autoComplete="password" autoComplete="password"
value={formData.password.value} value={formData.password.value}
onChangeText={(t) => { onChangeText={(t) => {
@@ -81,13 +76,8 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
}); });
}} }}
/> />
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}> <ButtonBase
{formData.password.error} title="Signin"
</FormControl.ErrorMessage>
<TextButton
translate={{ translationKey: 'signInBtn' }}
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={ isDisabled={
formData.password.error !== null || formData.password.error !== null ||
formData.username.error !== null || formData.username.error !== null ||
@@ -95,24 +85,27 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
formData.password.value === '' formData.password.value === ''
} }
onPress={async () => { onPress={async () => {
setSubmittingForm(true);
try { try {
const resp = await onSubmit( const resp = await onSubmit(
formData.username.value, formData.username.value,
formData.password.value formData.password.value
); );
toast.show({ description: resp, colorScheme: 'secondary' }); toast.show({ description: resp, colorScheme: 'secondary' });
setSubmittingForm(false);
} catch (e) { } catch (e) {
toast.show({ toast.show({
description: e as string, description: e as string,
colorScheme: 'red', colorScheme: 'red',
avoidKeyboard: true, avoidKeyboard: true,
}); });
setSubmittingForm(false);
} }
}} }}
/> />
<ButtonBase
// icon='logo-google'
isOutlined
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title="Signin with google"
/>
</FormControl> </FormControl>
</Stack> </Stack>
</Box> </Box>

View File

@@ -22,6 +22,7 @@
"@expo/vector-icons": "^13.0.0", "@expo/vector-icons": "^13.0.0",
"@motiz88/react-native-midi": "^0.0.5", "@motiz88/react-native-midi": "^0.0.5",
"@react-native-async-storage/async-storage": "~1.17.3", "@react-native-async-storage/async-storage": "~1.17.3",
"@react-native-community/blur": "^4.3.2",
"@react-navigation/native": "^6.0.11", "@react-navigation/native": "^6.0.11",
"@react-navigation/native-stack": "^6.7.0", "@react-navigation/native-stack": "^6.7.0",
"@reduxjs/toolkit": "^1.8.3", "@reduxjs/toolkit": "^1.8.3",
@@ -34,6 +35,7 @@
"expo": "^47.0.8", "expo": "^47.0.8",
"expo-asset": "~8.7.0", "expo-asset": "~8.7.0",
"expo-dev-client": "~2.0.1", "expo-dev-client": "~2.0.1",
"expo-linear-gradient": "~12.0.1",
"expo-linking": "~3.3.1", "expo-linking": "~3.3.1",
"expo-screen-orientation": "~5.0.1", "expo-screen-orientation": "~5.0.1",
"expo-secure-store": "~12.0.0", "expo-secure-store": "~12.0.0",
@@ -52,6 +54,7 @@
"react-dom": "18.1.0", "react-dom": "18.1.0",
"react-i18next": "^11.18.3", "react-i18next": "^11.18.3",
"react-native": "0.70.5", "react-native": "0.70.5",
"react-native-ionicons": "^4.6.5",
"react-native-paper": "^4.12.5", "react-native-paper": "^4.12.5",
"react-native-reanimated": "~2.12.0", "react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1", "react-native-safe-area-context": "4.4.1",

181
front/views/SigninView.tsx Normal file
View File

@@ -0,0 +1,181 @@
import React from 'react';
import { useDispatch } from '../state/Store';
import { Translate, translate } from '../i18n/i18n';
import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { } from 'native-base';
import SigninForm from '../components/forms/signinform';
import TextButton from '../components/TextButton';
import { useNavigation } from '../Navigation';
import { string } from 'yup';
import { FormControl, Input, Stack, Center, Button, Text, Box, useToast } from 'native-base';
import { TouchableOpacity, Linking, View, StyleSheet } from 'react-native'
import TextFormField from '../components/UI/TextFormField';
import LinkBase from '../components/UI/LinkBase';
import SeparatorBase from '../components/UI/SeparatorBase';
import ButtonBase from '../components/UI/ButtonBase';
import { Image, Flex } from 'native-base';
import ImageBanner from '../assets/banner.jpg';
import TMPBase from '../components/UI/TMPBase';
import { LinearGradient } from 'expo-linear-gradient';
import SettingBase from '../components/UI/SettingsBase';
const hanldeSignin = async (
username: string,
password: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try {
const apiAccess = await API.authenticate({ username, password });
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate('unknownError');
}
};
const SigninView = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const [formData, setFormData] = React.useState({
username: {
value: '',
error: null as string | null,
},
password: {
value: '',
error: null as string | null,
},
});
const validationSchemas = {
username: string()
.min(3, translate('usernameTooShort'))
.max(20, translate('usernameTooLong'))
.required('Username is required'),
password: string()
.min(4, translate('passwordTooShort'))
.max(100, translate('passwordTooLong'))
.required('Password is required'),
};
const toast = useToast();
const onSubmit= (username: string, password: string) => {
return hanldeSignin(username, password, (accessToken) =>
dispatch(setAccessToken(accessToken))
);
}
return (
<Flex direction='row' justifyContent="space-between" style={{ flex: 1, backgroundColor: '#101014'}}>
<Center style={{ flex: 1}}>
<Stack space={4} justifyContent="center" alignContent="center" alignItems="center" style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Text fontSize="4xl" textAlign="center">
Bienvenue !
</Text>
<Text fontSize="xl" textAlign="center">
Continuez avec Google ou entrez vos coordonnées.
</Text>
<ButtonBase
type='outlined'
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title="Signin with google"
/>
<ButtonBase
type='menu'
icon='person'
title="Menu"
/>
<SettingBase icon='person' title='title' description='description'>coucou</SettingBase>
<SeparatorBase>or</SeparatorBase>
<TextFormField
error={formData.username.error}
icon='person'
placeholder="Username"
autoComplete="username"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
isRequired
/>
<TextFormField
isRequired
isSecret
error={formData.password.error}
icon='lock-closed'
placeholder="Password"
autoComplete="password"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
/>
<LinkBase onPress={() => console.log('Link clicked!')}>
{translate('forgottenPassword')}
</LinkBase>
<ButtonBase
type='outlined'
icon='alert'
title="Signin"
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
formData.username.value === '' ||
formData.password.value === ''
}
onPress={async () => {
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value
);
toast.show({ description: resp, colorScheme: 'secondary' });
} catch (e) {
toast.show({
description: e as string,
colorScheme: 'red',
avoidKeyboard: true,
});
}
}}
/>
<Text>Vous n'avez pas de compte ?</Text>
<LinkBase onPress={() => navigation.navigate('Signup', {})}>
Inscrivez-vous gratuitement
</LinkBase>
</Stack>
</Center>
<View style={{width: '50%', height: '100%', padding: 16}}>
<Image
source={ImageBanner}
alt="banner page"
style={{width: '100%', height: '100%', borderRadius: 8}}
/>
</View>
<LinearGradient
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
colors={['#101014', '#6075F9']}
style={{top: 0, bottom: 0, right: 0, left: 0, width: '100%', height: '100%', position: 'absolute', zIndex: -2}}
/>
</Flex>
);
};
export default SigninView;

214
front/views/SignupView.tsx Normal file
View File

@@ -0,0 +1,214 @@
import React from 'react';
import { useDispatch } from '../state/Store';
import { Translate, translate } from '../i18n/i18n';
import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { } from 'native-base';
import SigninForm from '../components/forms/signinform';
import TextButton from '../components/TextButton';
import { useNavigation } from '../Navigation';
import { string } from 'yup';
import { FormControl, Input, Stack, Center, Button, Text, Box, useToast } from 'native-base';
import { TouchableOpacity, Linking, View } from 'react-native'
import TextFormField from '../components/UI/TextFormField';
import LinkBase from '../components/UI/LinkBase';
import SeparatorBase from '../components/UI/SeparatorBase';
import ButtonBase from '../components/UI/ButtonBase';
import { Image, Flex } from 'native-base';
import ImageBanner from '../assets/banner.jpg';
const handleSignup = async (
username: string,
password: string,
email: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try {
const apiAccess = await API.createAccount({ username, password, email });
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate('unknownError');
}
};
const SigninView = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const [formData, setFormData] = React.useState({
username: {
value: '',
error: null as string | null,
},
password: {
value: '',
error: null as string | null,
},
repeatPassword: {
value: '',
error: null as string | null,
},
email: {
value: '',
error: null as string | null,
},
});
const validationSchemas = {
username: string()
.min(3, translate('usernameTooShort'))
.max(20, translate('usernameTooLong'))
.required('Username is required'),
email: string().email('Invalid email').required('Email is required'),
password: string()
.min(4, translate('passwordTooShort'))
.max(100, translate('passwordTooLong'))
// .matches(
// /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$-_%\^&\*])(?=.{8,})/,
// translate(
// "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character"
// )
// )
.required('Password is required'),
};
const toast = useToast();
const onSubmit= (username: string, email: string, password: string) => {
return handleSignup(username, password, email, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
return (
<Flex direction='row' justifyContent="space-between" style={{ flex: 1, backgroundColor: '#101014'}}>
<Center style={{ flex: 1}}>
<Stack space={4} justifyContent="center" alignContent="center" alignItems="center" mx="4" style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Text fontSize="4xl" textAlign="center">Créer un compte</Text>
<Text fontSize="xl" textAlign="center">Apprendre le piano gratuitement et de manière ludique</Text>
<ButtonBase
isOutlined
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title="Signup with google"
/>
<SeparatorBase>or</SeparatorBase>
<TextFormField
error={formData.username.error}
icon='person'
placeholder="Username"
autoComplete="username"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
isRequired
/>
<TextFormField
error={formData.email.error}
icon='mail'
placeholder="Email"
autoComplete="email"
value={formData.email.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.email
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, email: { value: t, error } });
});
}}
isRequired
/>
<TextFormField
isRequired
isSecret
error={formData.password.error}
icon='lock-closed'
placeholder="Password"
autoComplete="password-new"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
/>
<TextFormField
isRequired
isSecret
error={formData.repeatPassword.error}
icon='lock-closed'
placeholder="Repeat password"
autoComplete="password-new"
value={formData.repeatPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
if (!error && t !== formData.password.value) {
error = translate('passwordsDontMatch');
}
setFormData({
...formData,
repeatPassword: { value: t, error },
});
});
}}
/>
<ButtonBase
title="Signup"
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
formData.repeatPassword.error !== null ||
formData.email.error !== null ||
formData.username.value === '' ||
formData.password.value === '' ||
formData.repeatPassword.value === '' ||
formData.repeatPassword.value === ''
}
onPress={async () => {
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value,
formData.email.value
);
toast.show({ description: resp });
} catch (e) {
toast.show({ description: e as string });
}
}}
/>
<Text>Vous avez déjà un compte ?</Text>
<LinkBase onPress={() => navigation.navigate('Login', {})}>
S'identifier
</LinkBase>
</Stack>
</Center>
<View style={{width: '50%', height: '100%', padding: 16}}>
<Image
source={ImageBanner}
alt="banner page"
style={{width: '100%', height: '100%', borderRadius: 8}}
/>
</View>
</Flex>
);
};
export default SigninView;

View File

@@ -2884,6 +2884,11 @@
dependencies: dependencies:
merge-options "^3.0.4" merge-options "^3.0.4"
"@react-native-community/blur@^4.3.2":
version "4.3.2"
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.2.tgz#185a2c7dd03ba168cc95069bc4742e9505fd6c6c"
integrity sha512-0ID+pyZKdC4RdgC7HePxUQ6JmsbNrgz03u+6SgqYpmBoK/rE+7JffqIw7IEsfoKitLEcRNLGekIBsfwCqiEkew==
"@react-native-community/cli-clean@^9.2.1": "@react-native-community/cli-clean@^9.2.1":
version "9.2.1" version "9.2.1"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-9.2.1.tgz#198c5dd39c432efb5374582073065ff75d67d018" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-9.2.1.tgz#198c5dd39c432efb5374582073065ff75d67d018"
@@ -9126,6 +9131,11 @@ expo-keep-awake@~11.0.1:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3" resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A== integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==
expo-linear-gradient@~12.0.1:
version "12.0.1"
resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.0.1.tgz#452f793b0463ddf313aad431552f23acc85f5d64"
integrity sha512-TMl/wBTVQOliL4S3DS5Aa3UFfVySr0mdJEHLG6kfBdMCLkr+tfLI2rGyJ+scS7xgMsvhTIaurhf1+Z0sL3aLCg==
expo-linking@~3.3.1: expo-linking@~3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357" resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"
@@ -15762,6 +15772,11 @@ react-native-gradle-plugin@^0.70.3:
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8"
integrity sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A== integrity sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A==
react-native-ionicons@^4.6.5:
version "4.6.5"
resolved "https://registry.yarnpkg.com/react-native-ionicons/-/react-native-ionicons-4.6.5.tgz#b792ec8896381e67ff237eba955e38afe7ceb7a4"
integrity sha512-s2Ia7M5t609LE9LWygMj3ALVPUlKhK7R9XcMb67fP4EYJv0oLcwg5pc+8ftv9XXaUuTW/WgL3zJlBYxAvtvMJg==
react-native-iphone-x-helper@^1.3.1: react-native-iphone-x-helper@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"