feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+46 -47
View File
@@ -1,18 +1,18 @@
import { useRouter } from "expo-router";
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { useRouter } from 'expo-router'
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import AppleSignInButton from "../../components/buttons/AppleSignInButton";
import Button from "../../components/buttons/Button";
import GoogleSignInButton from "../../components/buttons/GoogleSignInButton";
import AppInput from "../../components/inputs/Input";
import PublicScreenLayout from "../../components/layout/PublicScreenLayout";
import useRegisterForm from "../../hooks/useRegisterForm";
import Fonts from "../../styles/Fonts";
import Palette from "../../styles/Palette";
import AppleSignInButton from '../../components/buttons/AppleSignInButton'
import Button from '../../components/buttons/Button'
import GoogleSignInButton from '../../components/buttons/GoogleSignInButton'
import AppInput from '../../components/inputs/Input'
import PublicScreenLayout from '../../components/layout/PublicScreenLayout'
import useRegisterForm from '../../hooks/useRegisterForm'
import Fonts from '../../styles/Fonts'
import Palette from '../../styles/Palette'
export default function RegisterScreen() {
const router = useRouter();
const router = useRouter()
const {
email,
setEmail,
@@ -25,22 +25,21 @@ export default function RegisterScreen() {
signInWithApple,
isAppleSignInAvailable,
isLoading,
} = useRegisterForm();
} = useRegisterForm()
const handleGoBack = () => {
if (router.canGoBack?.()) {
router.back();
return;
router.back()
return
}
router.replace("/(public)/login");
};
router.replace('/(public)/login')
}
return (
<PublicScreenLayout title="Inscription" bodyStyle={styles.body}>
<Text style={styles.heading}>Rejoignez Minuit</Text>
<Text style={styles.subheading}>
Créez votre compte en quelques secondes et profitez de l&apos;expérience
complète.
Créez votre compte en quelques secondes et profitez de l&apos;expérience complète.
</Text>
<View style={styles.form}>
@@ -52,10 +51,10 @@ export default function RegisterScreen() {
type="email"
containerStyle={styles.input}
textInputProps={{
autoCapitalize: "none",
autoComplete: "email",
textContentType: "emailAddress",
name: "email",
autoCapitalize: 'none',
autoComplete: 'email',
textContentType: 'emailAddress',
name: 'email',
}}
/>
@@ -67,9 +66,9 @@ export default function RegisterScreen() {
type="password"
containerStyle={styles.input}
textInputProps={{
autoComplete: "new-password",
textContentType: "newPassword",
name: "new-password",
autoComplete: 'new-password',
textContentType: 'newPassword',
name: 'new-password',
}}
/>
@@ -81,9 +80,9 @@ export default function RegisterScreen() {
type="password"
containerStyle={styles.input}
textInputProps={{
autoComplete: "new-password",
textContentType: "newPassword",
name: "confirm-password",
autoComplete: 'new-password',
textContentType: 'newPassword',
name: 'confirm-password',
}}
/>
@@ -123,34 +122,34 @@ export default function RegisterScreen() {
/>
</View>
</PublicScreenLayout>
);
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: "center",
justifyContent: 'center',
gap: 24,
alignItems: "center",
alignItems: 'center',
},
heading: {
fontSize: 28,
fontWeight: "700",
fontWeight: '700',
color: Palette.darkPurple,
textAlign: "center",
textAlign: 'center',
},
subheading: {
...Fonts.bodySmall,
color: "rgba(15, 12, 20, 0.6)",
textAlign: "center",
color: 'rgba(15, 12, 20, 0.6)',
textAlign: 'center',
},
form: {
width: "100%",
width: '100%',
maxWidth: 420,
gap: 16,
},
input: {
width: "100%",
width: '100%',
},
primaryButton: {
marginTop: 8,
@@ -160,30 +159,30 @@ const styles = StyleSheet.create({
gap: 12,
},
socialDivider: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
dividerLine: {
flex: 1,
height: StyleSheet.hairlineWidth,
backgroundColor: "rgba(15, 12, 20, 0.15)",
backgroundColor: 'rgba(15, 12, 20, 0.15)',
},
dividerText: {
...Fonts.tinyBold,
textTransform: "uppercase",
color: "rgba(15, 12, 20, 0.45)",
textTransform: 'uppercase',
color: 'rgba(15, 12, 20, 0.45)',
},
socialButton: {
width: "100%",
width: '100%',
},
hint: {
marginTop: 12,
...Fonts.bodySmall,
textAlign: "center",
color: "rgba(15, 12, 20, 0.6)",
textAlign: 'center',
color: 'rgba(15, 12, 20, 0.6)',
},
secondaryButton: {
marginTop: 12,
},
});
})
+50 -50
View File
@@ -1,83 +1,83 @@
import { useRouter, usePathname } from 'expo-router';
import { useCallback, useMemo, useState } from 'react';
import { Platform } from 'react-native';
import { useRouter, usePathname } from 'expo-router'
import { useCallback, useMemo, useState } from 'react'
import { Platform } from 'react-native'
import useSocialAuth from './useSocialAuth';
import { useAuth } from '../providers/auth-context';
import { useLoading } from '../providers/loading-context';
import { useTooltip } from '../providers/tooltip-context';
import useSocialAuth from './useSocialAuth'
import { useAuth } from '../providers/auth-context'
import { useLoading } from '../providers/loading-context'
import { useTooltip } from '../providers/tooltip-context'
const normalizeEmail = rawEmail =>
typeof rawEmail === 'string' ? rawEmail.trim().toLowerCase() : '';
const normalizeEmail = (rawEmail) =>
typeof rawEmail === 'string' ? rawEmail.trim().toLowerCase() : ''
const normalizePassword = rawPassword =>
typeof rawPassword === 'string' ? rawPassword.trim() : '';
const normalizePassword = (rawPassword) =>
typeof rawPassword === 'string' ? rawPassword.trim() : ''
const MIN_PASSWORD_LENGTH = 6;
const MIN_PASSWORD_LENGTH = 6
const useRegisterForm = () => {
const router = useRouter();
const pathname = usePathname();
const { registerWithEmail, redirectUser } = useAuth();
const { withLoading, isLoading } = useLoading();
const { showTooltip } = useTooltip();
const defaultEmail = __DEV__ ? `test@${Platform.OS}.com` : '';
const defaultPassword = __DEV__ ? 'Minuit33' : '';
const [email, setEmail] = useState(defaultEmail);
const [password, setPassword] = useState(defaultPassword);
const [confirmPassword, setConfirmPassword] = useState(defaultPassword);
const router = useRouter()
const pathname = usePathname()
const { registerWithEmail, redirectUser } = useAuth()
const { withLoading, isLoading } = useLoading()
const { showTooltip } = useTooltip()
const defaultEmail = __DEV__ ? `test@${Platform.OS}.com` : ''
const defaultPassword = __DEV__ ? 'Minuit33' : ''
const [email, setEmail] = useState(defaultEmail)
const [password, setPassword] = useState(defaultPassword)
const [confirmPassword, setConfirmPassword] = useState(defaultPassword)
const { signInWithGoogle, signInWithApple, isAppleSignInAvailable } =
useSocialAuth({ defaultAction: 'register' });
const { signInWithGoogle, signInWithApple, isAppleSignInAvailable } = useSocialAuth({
defaultAction: 'register',
})
const handleSubmit = useCallback(async () => {
if (isLoading) {
return;
return
}
const trimmedEmail = normalizeEmail(email);
const trimmedPassword = normalizePassword(password);
const trimmedConfirm = normalizePassword(confirmPassword);
const trimmedEmail = normalizeEmail(email)
const trimmedPassword = normalizePassword(password)
const trimmedConfirm = normalizePassword(confirmPassword)
if (!trimmedEmail || !trimmedPassword || !trimmedConfirm) {
showTooltip({
message: 'Merci de renseigner tous les champs requis.',
type: 'error',
});
return;
})
return
}
if (trimmedPassword.length < MIN_PASSWORD_LENGTH) {
showTooltip({
message: `Le mot de passe doit contenir au moins ${MIN_PASSWORD_LENGTH} caractères.`,
type: 'error',
});
return;
})
return
}
if (trimmedPassword !== trimmedConfirm) {
showTooltip({
message: 'Les mots de passe ne correspondent pas.',
type: 'error',
});
return;
})
return
}
try {
await withLoading(async () => {
await registerWithEmail(trimmedEmail, trimmedPassword);
});
await redirectUser({ router, pathname, isInitial: false });
await registerWithEmail(trimmedEmail, trimmedPassword)
})
await redirectUser({ router, pathname, isInitial: false })
showTooltip({
message: 'Compte créé ! Bienvenue chez Minuit.',
type: 'success',
});
})
} catch (error) {
showTooltip({
message:
error?.message || 'Impossible de créer votre compte pour le moment.',
message: error?.message || 'Impossible de créer votre compte pour le moment.',
type: 'error',
});
})
}
}, [
confirmPassword,
@@ -90,20 +90,20 @@ const useRegisterForm = () => {
pathname,
showTooltip,
withLoading,
]);
])
const submitDisabled = useMemo(() => {
const trimmedEmail = normalizeEmail(email);
const trimmedPassword = normalizePassword(password);
const trimmedConfirm = normalizePassword(confirmPassword);
const trimmedEmail = normalizeEmail(email)
const trimmedPassword = normalizePassword(password)
const trimmedConfirm = normalizePassword(confirmPassword)
return (
!trimmedEmail ||
!trimmedPassword ||
!trimmedConfirm ||
trimmedPassword.length < MIN_PASSWORD_LENGTH ||
isLoading
);
}, [confirmPassword, email, isLoading, password]);
)
}, [confirmPassword, email, isLoading, password])
return {
email,
@@ -118,7 +118,7 @@ const useRegisterForm = () => {
signInWithGoogle,
signInWithApple,
isAppleSignInAvailable,
};
};
}
}
export default useRegisterForm;
export default useRegisterForm
+123 -131
View File
@@ -1,150 +1,146 @@
import {
GoogleSignin,
statusCodes,
} from '@react-native-google-signin/google-signin';
import * as AppleAuthentication from 'expo-apple-authentication';
import { useRouter, usePathname } from 'expo-router';
import * as Google from 'expo-auth-session/providers/google';
import Constants from 'expo-constants';
import * as WebBrowser from 'expo-web-browser';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Platform } from 'react-native';
import { GoogleSignin, statusCodes } from '@react-native-google-signin/google-signin'
import * as AppleAuthentication from 'expo-apple-authentication'
import { useRouter, usePathname } from 'expo-router'
import * as Google from 'expo-auth-session/providers/google'
import Constants from 'expo-constants'
import * as WebBrowser from 'expo-web-browser'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Platform } from 'react-native'
import { GOOGLE_CLIENT_IDS } from '../config/keys';
import { useAuth } from '../providers/auth-context';
import { useLoading } from '../providers/loading-context';
import { useTooltip } from '../providers/tooltip-context';
import { GOOGLE_CLIENT_IDS } from '../config/keys'
import { useAuth } from '../providers/auth-context'
import { useLoading } from '../providers/loading-context'
import { useTooltip } from '../providers/tooltip-context'
WebBrowser.maybeCompleteAuthSession();
WebBrowser.maybeCompleteAuthSession()
const PLACEHOLDER_CLIENT_IDS = {
android: 'placeholder-android-client-id',
ios: 'placeholder-ios-client-id',
web: 'placeholder-web-client-id',
expo: 'placeholder-expo-client-id',
};
}
const GOOGLE_CLIENT_ID_ENV_KEYS = {
androidClientId: 'EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID',
iosClientId: 'EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID',
webClientId: 'EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID',
expoClientId: 'EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID',
};
}
const isMeaningfulValue = value => {
const isMeaningfulValue = (value) => {
if (!value) {
return false;
return false
}
const normalized = value.toLowerCase();
const normalized = value.toLowerCase()
if (
normalized.startsWith('your-') ||
normalized.includes('example') ||
normalized.includes('placeholder')
) {
return false;
return false
}
return true;
};
return true
}
const readGoogleClientIds = () => ({
androidClientId: GOOGLE_CLIENT_IDS.android,
iosClientId: GOOGLE_CLIENT_IDS.ios,
webClientId: GOOGLE_CLIENT_IDS.web,
expoClientId: GOOGLE_CLIENT_IDS.expo,
});
})
const buildAuthSessionConfig = ids => ({
const buildAuthSessionConfig = (ids) => ({
androidClientId: ids.androidClientId || PLACEHOLDER_CLIENT_IDS.android,
iosClientId: ids.iosClientId || PLACEHOLDER_CLIENT_IDS.ios,
webClientId: ids.webClientId || PLACEHOLDER_CLIENT_IDS.web,
expoClientId: ids.expoClientId || PLACEHOLDER_CLIENT_IDS.expo,
});
})
const getRequiredGoogleKeys = ({ isNativePlatform, isExpoGo }) => {
if (Platform.OS === 'web') {
return ['webClientId'];
return ['webClientId']
}
if (isExpoGo) {
if (Platform.OS === 'android') {
return ['expoClientId', 'androidClientId'];
return ['expoClientId', 'androidClientId']
}
if (Platform.OS === 'ios') {
return ['expoClientId', 'iosClientId'];
return ['expoClientId', 'iosClientId']
}
return ['expoClientId'];
return ['expoClientId']
}
if (isNativePlatform) {
const keys = ['webClientId'];
const keys = ['webClientId']
if (Platform.OS === 'android') {
keys.push('androidClientId');
keys.push('androidClientId')
}
if (Platform.OS === 'ios') {
keys.push('iosClientId');
keys.push('iosClientId')
}
return keys;
return keys
}
return ['webClientId'];
};
return ['webClientId']
}
const findMissingGoogleKeys = (ids, options) =>
getRequiredGoogleKeys(options).filter(key => !isMeaningfulValue(ids?.[key]));
getRequiredGoogleKeys(options).filter((key) => !isMeaningfulValue(ids?.[key]))
const formatMissingGoogleKeys = keys =>
keys.map(key => GOOGLE_CLIENT_ID_ENV_KEYS[key] || key).join(', ');
const formatMissingGoogleKeys = (keys) =>
keys.map((key) => GOOGLE_CLIENT_ID_ENV_KEYS[key] || key).join(', ')
const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
const router = useRouter();
const pathname = usePathname();
const { authenticateWithGoogle, authenticateWithApple, redirectUser } =
useAuth();
const { withLoading, isLoading } = useLoading();
const { showTooltip } = useTooltip();
const router = useRouter()
const pathname = usePathname()
const { authenticateWithGoogle, authenticateWithApple, redirectUser } = useAuth()
const { withLoading, isLoading } = useLoading()
const { showTooltip } = useTooltip()
const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false);
const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false)
useEffect(() => {
let mounted = true;
let mounted = true
AppleAuthentication.isAvailableAsync()
.then(isAvailable => {
.then((isAvailable) => {
if (mounted) {
setIsAppleSignInAvailable(isAvailable);
setIsAppleSignInAvailable(isAvailable)
}
})
.catch(() => {
if (mounted) {
setIsAppleSignInAvailable(false);
setIsAppleSignInAvailable(false)
}
});
})
return () => {
mounted = false;
};
}, []);
mounted = false
}
}, [])
const googleClientIds = useMemo(() => readGoogleClientIds(), []);
const googleClientIds = useMemo(() => readGoogleClientIds(), [])
const authSessionConfig = useMemo(
() => buildAuthSessionConfig(googleClientIds),
[googleClientIds],
);
[googleClientIds]
)
const isNativePlatform = Platform.OS === 'ios' || Platform.OS === 'android';
const appOwnership = Constants?.appOwnership || null;
const isExpoGo = appOwnership === 'expo';
const shouldUseAuthSession = !isNativePlatform || isExpoGo;
const isNativePlatform = Platform.OS === 'ios' || Platform.OS === 'android'
const appOwnership = Constants?.appOwnership || null
const isExpoGo = appOwnership === 'expo'
const shouldUseAuthSession = !isNativePlatform || isExpoGo
useEffect(() => {
if (!isNativePlatform || shouldUseAuthSession) {
return;
return
}
if (!isMeaningfulValue(googleClientIds.webClientId)) {
return;
return
}
try {
@@ -152,44 +148,44 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
webClientId: googleClientIds.webClientId,
iosClientId: googleClientIds.iosClientId,
offlineAccess: true,
});
})
} catch (error) {
console.warn('Failed to configure Google Sign-In', error);
console.warn('Failed to configure Google Sign-In', error)
}
}, [googleClientIds, isNativePlatform, shouldUseAuthSession]);
}, [googleClientIds, isNativePlatform, shouldUseAuthSession])
const [, , promptAsync] = Google.useAuthRequest(authSessionConfig);
const [, , promptAsync] = Google.useAuthRequest(authSessionConfig)
const handleMissingGoogleConfig = useCallback(
missingKeys => {
(missingKeys) => {
if (!missingKeys.length) {
return false;
return false
}
showTooltip({
message: `La configuration Google est incomplète. Variables manquantes : ${formatMissingGoogleKeys(
missingKeys,
missingKeys
)}.`,
type: 'error',
});
})
return true;
return true
},
[showTooltip],
);
[showTooltip]
)
const signInWithGoogle = useCallback(
async (actionOverride = undefined) => {
if (isLoading) {
return;
return
}
const missingKeys = findMissingGoogleKeys(googleClientIds, {
isNativePlatform,
isExpoGo,
});
})
if (handleMissingGoogleConfig(missingKeys)) {
return;
return
}
if (isNativePlatform && !shouldUseAuthSession) {
@@ -197,38 +193,38 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
await withLoading(async () => {
await GoogleSignin.hasPlayServices({
showPlayServicesUpdateDialog: true,
});
})
const account = await GoogleSignin.signIn();
let idToken = account?.idToken;
let accessToken = account?.accessToken;
const account = await GoogleSignin.signIn()
let idToken = account?.idToken
let accessToken = account?.accessToken
try {
const tokens = await GoogleSignin.getTokens();
idToken = idToken || tokens?.idToken;
accessToken = accessToken || tokens?.accessToken;
const tokens = await GoogleSignin.getTokens()
idToken = idToken || tokens?.idToken
accessToken = accessToken || tokens?.accessToken
} catch (_) {
// no-op: continue with whatever tokens we already have
}
if (!idToken) {
throw new Error('Impossible de récupérer le jeton Google.');
throw new Error('Impossible de récupérer le jeton Google.')
}
await authenticateWithGoogle({
idToken,
accessToken,
action: actionOverride || defaultAction,
});
await redirectUser({ router, pathname, isInitial: false });
});
})
await redirectUser({ router, pathname, isInitial: false })
})
} catch (error) {
if (error?.code === statusCodes.SIGN_IN_CANCELLED) {
return;
return
}
if (error?.code === statusCodes.IN_PROGRESS) {
return;
return
}
if (error?.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
@@ -236,20 +232,19 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
message:
'Les services Google Play ne sont pas disponibles ou nécessitent une mise à jour.',
type: 'error',
});
return;
})
return
}
const message =
error?.message ||
'Impossible de terminer la connexion Google pour le moment.';
error?.message || 'Impossible de terminer la connexion Google pour le moment.'
showTooltip({
message,
type: 'error',
});
})
}
return;
return
}
if (!promptAsync) {
@@ -257,8 +252,8 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
message:
'La configuration Google est incomplète. Merci de vérifier vos identifiants client.',
type: 'error',
});
return;
})
return
}
try {
@@ -266,38 +261,37 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
const response = await promptAsync({
useProxy: Platform.OS !== 'web',
showInRecents: true,
});
})
if (!response || response.type !== 'success') {
if (response?.type && response.type !== 'dismiss') {
showTooltip({
message: 'Connexion Google annulée.',
type: 'warning',
});
})
}
return;
return
}
const { authentication } = response;
const { authentication } = response
if (!authentication?.idToken) {
throw new Error('Jeton Google introuvable.');
throw new Error('Jeton Google introuvable.')
}
await authenticateWithGoogle({
idToken: authentication.idToken,
accessToken: authentication.accessToken,
action: actionOverride || defaultAction,
});
await redirectUser({ router, pathname, isInitial: false });
});
})
await redirectUser({ router, pathname, isInitial: false })
})
} catch (error) {
const message =
error?.message ||
'Impossible de terminer la connexion Google pour le moment.';
error?.message || 'Impossible de terminer la connexion Google pour le moment.'
showTooltip({
message,
type: 'error',
});
})
}
},
[
@@ -315,22 +309,21 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
shouldUseAuthSession,
showTooltip,
withLoading,
],
);
]
)
const signInWithApple = useCallback(
async (actionOverride = undefined) => {
if (isLoading) {
return;
return
}
if (!isAppleSignInAvailable) {
showTooltip({
message:
'La connexion avec Apple nest pas disponible sur cet appareil.',
message: 'La connexion avec Apple nest pas disponible sur cet appareil.',
type: 'warning',
});
return;
})
return
}
try {
@@ -340,30 +333,29 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
});
})
if (!appleCredential?.identityToken) {
throw new Error('Impossible de récupérer le jeton Apple.');
throw new Error('Impossible de récupérer le jeton Apple.')
}
await authenticateWithApple({
idToken: appleCredential.identityToken,
rawNonce: appleCredential.nonce,
action: actionOverride || defaultAction,
});
await redirectUser({ router, pathname, isInitial: false });
});
})
await redirectUser({ router, pathname, isInitial: false })
})
} catch (error) {
if (error?.code === 'ERR_CANCELED') {
return;
return
}
const message =
error?.message ||
'Impossible de terminer la connexion avec Apple pour le moment.';
error?.message || 'Impossible de terminer la connexion avec Apple pour le moment.'
showTooltip({
message,
type: 'error',
});
})
}
},
[
@@ -376,14 +368,14 @@ const useSocialAuth = ({ defaultAction = 'login' } = {}) => {
router,
showTooltip,
withLoading,
],
);
]
)
return {
isAppleSignInAvailable,
signInWithGoogle,
signInWithApple,
};
};
}
}
export default useSocialAuth;
export default useSocialAuth