diff --git a/_templates/generators/page/componentFile.ejs.t b/_templates/generators/page/componentFile.ejs.t index 2b21599f..c34531f6 100644 --- a/_templates/generators/page/componentFile.ejs.t +++ b/_templates/generators/page/componentFile.ejs.t @@ -1,8 +1,8 @@ --- to: src/screens/<%= h.changeCase.pascalCase(componentName) %>.tsx --- -import { Box, Text } from '$shared/ui/primitives'; -import { Screen } from '$shared/ui/Screen'; +import { Box, Text } from '$shared/uiKit/primitives'; +import { Screen } from '$shared/uiKit/Screen'; type <%= h.changeCase.pascalCase(componentName) %>Props = { someProp?: string; diff --git a/_templates/generators/page/navigationInjection.ejs.t b/_templates/generators/page/navigationInjection.ejs.t index f4f6b897..3b460d5d 100644 --- a/_templates/generators/page/navigationInjection.ejs.t +++ b/_templates/generators/page/navigationInjection.ejs.t @@ -1,7 +1,7 @@ --- inject: true before: inject screens before this -to: src/core/navigation/navigation.tsx +to: src/core/navigation/RootStack.tsx --- } diff --git a/package.json b/package.json index 3f87c5bf..3766c00c 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "postpack": "pinst --enable" }, "dependencies": { + "@hookform/resolvers": "3.3.4", "@react-native-masked-view/masked-view": "0.3.0", "@react-navigation/native": "6.1.17", "@react-navigation/native-stack": "6.9.26", @@ -106,11 +107,12 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", + "react-hook-form": "7.51.3", "react-i18next": "14.1.0", "react-native": "0.73.6", "react-native-device-info": "10.13.1", "react-native-gesture-handler": "~2.14.0", - "react-native-keyboard-aware-scroll-view": "0.9.5", + "react-native-keyboard-controller": "1.11.6", "react-native-mmkv": "2.11.0", "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", diff --git a/src/App.tsx b/src/App.tsx index 433a5fd5..7b6f831c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import type { ErrorInfo } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { StatusBar, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { KeyboardProvider } from 'react-native-keyboard-controller'; import { SafeAreaProvider, initialWindowMetrics, @@ -63,17 +64,19 @@ const App = () => { > - - <> - + + + <> + - + - + - - - + + + + diff --git a/src/core/i18n/resources/en/homeScreen.json b/src/core/i18n/resources/en/homeScreen.json index 2bf6ca0c..7ae82728 100644 --- a/src/core/i18n/resources/en/homeScreen.json +++ b/src/core/i18n/resources/en/homeScreen.json @@ -5,6 +5,7 @@ }, "navigation": { "content": "Press to navigate", + "screenTitle": "Home", "title": "Navigate to another page" }, "sandbox": { diff --git a/src/core/i18n/resources/en/miscScreens.json b/src/core/i18n/resources/en/miscScreens.json index 98c4c9e7..9a0ce317 100644 --- a/src/core/i18n/resources/en/miscScreens.json +++ b/src/core/i18n/resources/en/miscScreens.json @@ -11,6 +11,34 @@ "description": "An app update is mandatory to be able to use the application.", "title": "Update is required" }, + "dummyForm": { + "form": { + "email": { + "label": "Email", + "placeholder": "Enter your email", + "validation": { + "email": "Please enter a valid email" + } + }, + "firstName": { + "label": "First name", + "placeholder": "John", + "validation": { + "maxLength": "First name must be at most 20 characters long", + "minLength": "First name must be at least 2 characters long" + } + }, + "lastName": { + "label": "Last name", + "placeholder": "Doe", + "validation": { + "maxLength": "Last name must be at most 30 characters long", + "minLength": "Last name must be at least 2 characters long" + } + } + }, + "screenTitle": "Dummy form" + }, "errorBoundary": { "cta": "Relaunch the app", "description": "An unknown error occured. If the error persist, contact an administrator.", diff --git a/src/core/i18n/resources/en/otherScreen.json b/src/core/i18n/resources/en/otherScreen.json index a7b43e49..af1c0a20 100644 --- a/src/core/i18n/resources/en/otherScreen.json +++ b/src/core/i18n/resources/en/otherScreen.json @@ -1,7 +1,13 @@ { + "form": { + "cta": "Navigate", + "title": "Form example" + }, + "graphql": { + "cta": "Navigate", + "title": "API call example" + }, "navigation": { - "backCta": "Back", - "tabBar": "Another page", - "title": "Another page" + "title": "Other screen" } } diff --git a/src/core/i18n/resources/fr/homeScreen.json b/src/core/i18n/resources/fr/homeScreen.json index 158d4484..68d7e2fd 100644 --- a/src/core/i18n/resources/fr/homeScreen.json +++ b/src/core/i18n/resources/fr/homeScreen.json @@ -5,6 +5,7 @@ }, "navigation": { "content": "Touchez pour naviguer", + "screenTitle": "Accueil", "title": "Naviguer vers une autre page" }, "sandbox": { diff --git a/src/core/i18n/resources/fr/miscScreens.json b/src/core/i18n/resources/fr/miscScreens.json index 64422d02..950c3af5 100644 --- a/src/core/i18n/resources/fr/miscScreens.json +++ b/src/core/i18n/resources/fr/miscScreens.json @@ -11,6 +11,34 @@ "description": "Une mise à jour de l'application est requise pour fonctionner.", "title": "Mise à jour requise" }, + "dummyForm": { + "form": { + "email": { + "label": "Email", + "placeholder": "Saisir un email", + "validation": { + "email": "Il faut un email valide" + } + }, + "firstName": { + "label": "Prénom", + "placeholder": "Martin", + "validation": { + "maxLength": "Le prénom doit faire au plus 20 caractères", + "minLength": "Le prénom doit faire au moins 2 caractères" + } + }, + "lastName": { + "label": "Nom", + "placeholder": "Dupont", + "validation": { + "maxLength": "Le nom doit faire au plus 30 caractères", + "minLength": "Le nom doit faire au moins 2 caractères" + } + } + }, + "screenTitle": "Dummy form" + }, "errorBoundary": { "cta": "Relancer l'app", "description": "Une erreur est survenue. Si l'erreur persiste, contacter un administrateur.", diff --git a/src/core/i18n/resources/fr/otherScreen.json b/src/core/i18n/resources/fr/otherScreen.json index 6c4108dc..b65ccca1 100644 --- a/src/core/i18n/resources/fr/otherScreen.json +++ b/src/core/i18n/resources/fr/otherScreen.json @@ -1,7 +1,13 @@ { + "form": { + "cta": "Naviguer", + "title": "Formulaire" + }, + "graphql": { + "cta": "Naviguer", + "title": "Example d'appel API" + }, "navigation": { - "backCta": "Retour en arrière", - "tabBar": "Une autre page", - "title": "Une autre page" + "title": "Autre écran" } } diff --git a/src/core/monitoring/errorMonitoring.ts b/src/core/monitoring/errorMonitoring.ts index 6d51cabe..8765f0f5 100644 --- a/src/core/monitoring/errorMonitoring.ts +++ b/src/core/monitoring/errorMonitoring.ts @@ -31,7 +31,7 @@ class ErrorMonitoringClass { Sentry.init({ dsn: config.sentryDsn, - debug: config.env === 'development', + debug: false, tracesSampleRate: sampleRate, enabled: isEnabled, environment: config.env, diff --git a/src/core/navigation/RootStack.tsx b/src/core/navigation/RootStack.tsx index 1f697889..bf0fc2d7 100644 --- a/src/core/navigation/RootStack.tsx +++ b/src/core/navigation/RootStack.tsx @@ -15,13 +15,6 @@ import { screens } from './screens'; const Stack = createNativeStackNavigator(); -const navigatorScreenOptions: NativeStackNavigationOptions = { - gestureEnabled: true, -}; -const screenOptions: NativeStackNavigationOptions = { - headerShown: false, -}; - export const RootStack = () => { const routeNameRef = useRef(); const navigationRef = useNavigationContainerRef(); @@ -58,7 +51,10 @@ export const RootStack = () => { { options={{ title: t('navigation.title') }} /> + + + + {/* inject screens before this */} ); }; + +const navigatorScreenOptions: NativeStackNavigationOptions = { + gestureEnabled: true, +}; +const screenOptions: NativeStackNavigationOptions = { + headerShown: false, +}; diff --git a/src/core/navigation/navigation.types.ts b/src/core/navigation/navigation.types.ts index 557513ee..7bca29ea 100644 --- a/src/core/navigation/navigation.types.ts +++ b/src/core/navigation/navigation.types.ts @@ -5,6 +5,8 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; export type RootStackParamList = { HomeScreen: undefined; OtherScreen: { someProps: string } | undefined; + BlogPost: undefined; + DummyForm: undefined; // inject stack types before this }; @@ -18,4 +20,12 @@ export type OtherScreenNavigationProp = NativeStackNavigationProp< RootStackParamList, 'OtherScreen' >; +export type BlogPostScreenNavigationProp = NativeStackNavigationProp< + RootStackParamList, + 'BlogPost' +>; +export type DummyFormScreenNavigationProp = NativeStackNavigationProp< + RootStackParamList, + 'DummyForm' +>; // inject page types before this diff --git a/src/core/navigation/screens.ts b/src/core/navigation/screens.ts index 0f410cd5..38772c45 100644 --- a/src/core/navigation/screens.ts +++ b/src/core/navigation/screens.ts @@ -1,3 +1,5 @@ +import { BlogPost } from '$screens/BlogPost'; +import { DummyForm } from '$screens/DummyForm'; import { HomeScreen } from '$screens/HomeScreen'; import { OtherScreen } from '$screens/OtherScreen'; // inject pages before this @@ -5,5 +7,7 @@ import { OtherScreen } from '$screens/OtherScreen'; export const screens = { HomeScreen, OtherScreen, + BlogPost, + DummyForm, // inject exports before this }; diff --git a/src/features/dummyForm/DummyFormExample.tsx b/src/features/dummyForm/DummyFormExample.tsx new file mode 100644 index 00000000..4e61a6d7 --- /dev/null +++ b/src/features/dummyForm/DummyFormExample.tsx @@ -0,0 +1,111 @@ +/* eslint-disable react/jsx-props-no-spreading */ + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useRef } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { TextInput } from 'react-native'; + +import { + DummyFormSchema, + type DummyFormSchemaType, +} from '$features/dummyForm/utils/dummyForm.schema'; +import { Button } from '$shared/uiKit/button'; +import { Input } from '$shared/uiKit/input'; +import { Box } from '$shared/uiKit/primitives'; + +export const DummyFormExample = () => { + const firstNameInputRef = useRef(null); + const lastNameInputRef = useRef(null); + + const { t } = useTranslation('miscScreens'); + + const { control, handleSubmit } = useForm({ + resolver: zodResolver(DummyFormSchema), + }); + + const onSubmit: SubmitHandler = (data) => { + // Do your form submission stuff here + console.log('data', data); + }; + + return ( + <> + + ( + { + firstNameInputRef.current?.focus(); + }} + /> + )} + /> + + + + ( + { + lastNameInputRef.current?.focus(); + }} + /> + )} + /> + + + + ( + + )} + /> + + + Promise} + > + Submit + + + ); +}; diff --git a/src/features/dummyForm/index.ts b/src/features/dummyForm/index.ts new file mode 100644 index 00000000..3ffd4c18 --- /dev/null +++ b/src/features/dummyForm/index.ts @@ -0,0 +1 @@ +export { DummyFormExample as DummyForm } from './DummyFormExample'; diff --git a/src/features/dummyForm/utils/dummyForm.schema.ts b/src/features/dummyForm/utils/dummyForm.schema.ts new file mode 100644 index 00000000..66073da6 --- /dev/null +++ b/src/features/dummyForm/utils/dummyForm.schema.ts @@ -0,0 +1,39 @@ +import i18next from 'i18next'; +import { z } from 'zod'; + +const FIRST_NAME_MIN_LENGTH = 2; +const FIRST_NAME_MAX_LENGTH = 20; +const LAST_NAME_MIN_LENGTH = 2; +const LAST_NAME_MAX_LENGTH = 30; + +export const DummyFormSchema = z.object({ + email: z.string().email({ + message: i18next.t('miscScreens:dummyForm.form.email.validation.email'), + }), + firstName: z + .string() + .min(FIRST_NAME_MIN_LENGTH, { + message: i18next.t( + 'miscScreens:dummyForm.form.firstName.validation.minLength', + ), + }) + .max(FIRST_NAME_MAX_LENGTH, { + message: i18next.t( + 'miscScreens:dummyForm.form.firstName.validation.maxLength', + ), + }), + lastName: z + .string() + .min(LAST_NAME_MIN_LENGTH, { + message: i18next.t( + 'miscScreens:dummyForm.form.lastName.validation.minLength', + ), + }) + .max(LAST_NAME_MAX_LENGTH, { + message: i18next.t( + 'miscScreens:dummyForm.form.lastName.validation.minLength', + ), + }), +}); + +export type DummyFormSchemaType = z.infer; diff --git a/src/screens/BlogPost.tsx b/src/screens/BlogPost.tsx new file mode 100644 index 00000000..36d58f98 --- /dev/null +++ b/src/screens/BlogPost.tsx @@ -0,0 +1,11 @@ +import { BlogPost as BlogPostComponent } from '$features/blogPost'; +import { Box } from '$shared/uiKit/primitives'; +import { Screen } from '$shared/uiKit/Screen'; + +export const BlogPost = () => ( + + + + + +); diff --git a/src/screens/DummyForm.tsx b/src/screens/DummyForm.tsx new file mode 100644 index 00000000..8d36e300 --- /dev/null +++ b/src/screens/DummyForm.tsx @@ -0,0 +1,17 @@ +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; + +import { DummyForm as DummyFormComponent } from '$features/dummyForm'; +import { Box } from '$shared/uiKit/primitives'; +import { Screen } from '$shared/uiKit/Screen'; + +export const DummyForm = () => { + return ( + + + + + + + + ); +}; diff --git a/src/screens/OtherScreen.tsx b/src/screens/OtherScreen.tsx index f4486734..fd954c51 100644 --- a/src/screens/OtherScreen.tsx +++ b/src/screens/OtherScreen.tsx @@ -1,7 +1,6 @@ import { useTranslation } from 'react-i18next'; import type { OtherScreenNavigationProp } from '$core/navigation/navigation.types'; -import { BlogPost } from '$features/blogPost'; import { Button } from '$shared/uiKit/button'; import { Box, Text } from '$shared/uiKit/primitives'; import { Screen } from '$shared/uiKit/Screen'; @@ -13,25 +12,36 @@ type OtherScreenProps = { export const OtherScreen = ({ navigation }: OtherScreenProps) => { const { t } = useTranslation('otherScreen'); - const goBack = () => { - navigation.goBack(); + const goToBlogPost = () => { + navigation.navigate('BlogPost'); + }; + + const goToDummyForm = () => { + navigation.navigate('DummyForm'); }; return ( - - - {t('navigation.title')} + + + {t('graphql.title')} - - - {t('navigation.backCta')} + + + {t('graphql.cta')} + + + + {t('form.title')} - - + + {t('form.cta')} diff --git a/src/screens/__tests__/OtherScreen.test.tsx b/src/screens/__tests__/OtherScreen.test.tsx index 53de960b..b30b50fb 100644 --- a/src/screens/__tests__/OtherScreen.test.tsx +++ b/src/screens/__tests__/OtherScreen.test.tsx @@ -16,7 +16,7 @@ describe('OtherPage component', () => { const { getByTestId } = render(); // Then - expect(getByTestId('otherPage_title')).toBeDefined(); + expect(getByTestId('apiExample_title')).toBeDefined(); }); it('should trigger the goBack method when the button is pressed', () => { @@ -24,9 +24,9 @@ describe('OtherPage component', () => { const { getByTestId } = render(); // When - fireEvent.press(getByTestId('back_button')); + fireEvent.press(getByTestId('apiExample_navigate_button')); // Then - expect(props.navigation.goBack).toHaveBeenCalled(); + expect(props.navigation.navigate).toHaveBeenCalled(); }); }); diff --git a/src/shared/uiKit/Screen.tsx b/src/shared/uiKit/Screen.tsx index 9aa1ff59..2acc44e0 100644 --- a/src/shared/uiKit/Screen.tsx +++ b/src/shared/uiKit/Screen.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import { ScrollView } from 'react-native'; import type { Edge } from 'react-native-safe-area-context'; import { SafeView } from './SafeView'; @@ -7,12 +7,17 @@ import { SafeView } from './SafeView'; type ScreenProps = { edges?: Edge[]; children: ReactNode; + isScrollable?: boolean; }; -export const Screen = ({ children, edges = [] }: ScreenProps) => { +export const Screen = ({ + children, + edges = [], + isScrollable = true, +}: ScreenProps) => { return ( - {children} + {isScrollable ? {children} : children} ); }; diff --git a/src/shared/uiKit/button/types/buttonTypes.ts b/src/shared/uiKit/button/types/buttonTypes.ts index 0200edc0..5d014759 100644 --- a/src/shared/uiKit/button/types/buttonTypes.ts +++ b/src/shared/uiKit/button/types/buttonTypes.ts @@ -6,7 +6,7 @@ export interface ButtonProps extends VariantProps { testID?: string; onPress: | ((arg: unknown) => Promise) - | ((arg: unknown) => void) + | ((arg?: unknown) => void) | undefined; isDisabled?: boolean; isLoading?: boolean; diff --git a/src/shared/uiKit/input/Input.tsx b/src/shared/uiKit/input/Input.tsx index 17deb838..abf0a56b 100644 --- a/src/shared/uiKit/input/Input.tsx +++ b/src/shared/uiKit/input/Input.tsx @@ -8,27 +8,12 @@ import { makeAppStyles, theme, fontSizes } from '$core/theme'; import { Box, Text } from '../primitives'; -interface InputProps extends TextInputProps { +export interface InputProps extends TextInputProps { label?: string; error?: string; isEditable?: boolean; } -const useStyles = makeAppStyles(({ colors }) => ({ - input: { - fontSize: fontSizes.regular, - borderBottomWidth: 1, - padding: 0, - paddingBottom: 5, - }, - defaultState: { - borderBottomColor: colors.secondary_80, - }, - errorState: { - borderBottomColor: colors.red, - }, -})); - export const Input = forwardRef( ({ label, error, isEditable = true, ...props }, ref) => { const styles = useStyles(); @@ -50,8 +35,8 @@ export const Input = forwardRef( style={[styles.input, errorStyles]} testID="inputID" underlineColorAndroid="transparent" - {...props} onChangeText={props.onChangeText} + {...props} /> {!!error && ( @@ -66,4 +51,19 @@ export const Input = forwardRef( }, ); +const useStyles = makeAppStyles(({ colors }) => ({ + input: { + fontSize: fontSizes.regular, + borderBottomWidth: 1, + padding: 0, + paddingBottom: 5, + }, + defaultState: { + borderBottomColor: colors.secondary_80, + }, + errorState: { + borderBottomColor: colors.red, + }, +})); + Input.displayName = 'Input'; diff --git a/yarn.lock b/yarn.lock index d7caf27a..1c8c2007 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4068,6 +4068,15 @@ __metadata: languageName: node linkType: hard +"@hookform/resolvers@npm:3.3.4": + version: 3.3.4 + resolution: "@hookform/resolvers@npm:3.3.4" + peerDependencies: + react-hook-form: ^7.0.0 + checksum: 7761e3340d23acd092dec4023344678e62b0c0ea0ed5aa3687cd315fc339b3b965e11124d643811292e6962683357423e6fe646fffabf9b96f8322f41903924d + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -18052,7 +18061,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -18293,6 +18302,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:7.51.3": + version: 7.51.3 + resolution: "react-hook-form@npm:7.51.3" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + checksum: 4ac71033b66ae0b7b9d75a1bc2053a6747fb37f68c6895ee85a72cabb890168d82990c8ca7bddd271e80fdbf58f471d14d6a0e0714400d017590d4f56b3d241f + languageName: node + linkType: hard + "react-i18next@npm:14.1.0": version: 14.1.0 resolution: "react-i18next@npm:14.1.0" @@ -18373,24 +18391,14 @@ __metadata: languageName: node linkType: hard -"react-native-iphone-x-helper@npm:^1.0.3": - version: 1.3.1 - resolution: "react-native-iphone-x-helper@npm:1.3.1" - peerDependencies: - react-native: ">=0.42.0" - checksum: 024376646009a966e33e12fc2358751830818b0fb73b1c601a64eb5e490d2dc43eec23668991b985a8c412a84d087f20eb45bb9b593567c08b66e741b7bddda5 - languageName: node - linkType: hard - -"react-native-keyboard-aware-scroll-view@npm:0.9.5": - version: 0.9.5 - resolution: "react-native-keyboard-aware-scroll-view@npm:0.9.5" - dependencies: - prop-types: "npm:^15.6.2" - react-native-iphone-x-helper: "npm:^1.0.3" +"react-native-keyboard-controller@npm:1.11.6": + version: 1.11.6 + resolution: "react-native-keyboard-controller@npm:1.11.6" peerDependencies: - react-native: ">=0.48.4" - checksum: 5eec2aedb886213dfed4c8428a443b565dce80ba9fcf5897171e4c8829d4dd64c5011ac004d93f29adfc83ad6f88c2838d1a5b45255dc4a49bd1c0a169245800 + react: "*" + react-native: "*" + react-native-reanimated: ">=2.3.0" + checksum: 7dc970b2063f4ca01853fd67d2e3287695edff077f76c2d12cd87024807dcffdb5f655d63afdcae20ee48f119cd6d405c990bb86e630d6076a122063b489e8c7 languageName: node linkType: hard @@ -19155,6 +19163,7 @@ __metadata: "@graphql-codegen/typescript": 4.0.6 "@graphql-codegen/typescript-operations": 4.2.0 "@graphql-codegen/typescript-react-query": 6.1.0 + "@hookform/resolvers": 3.3.4 "@react-native-masked-view/masked-view": 0.3.0 "@react-navigation/native": 6.1.17 "@react-navigation/native-stack": 6.9.26 @@ -19224,11 +19233,12 @@ __metadata: react: 18.2.0 react-dom: 18.2.0 react-error-boundary: 4.0.13 + react-hook-form: 7.51.3 react-i18next: 14.1.0 react-native: 0.73.6 react-native-device-info: 10.13.1 react-native-gesture-handler: ~2.14.0 - react-native-keyboard-aware-scroll-view: 0.9.5 + react-native-keyboard-controller: 1.11.6 react-native-mmkv: 2.11.0 react-native-reanimated: ~3.6.2 react-native-safe-area-context: 4.8.2