Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wave9] Require name + surname #69

Merged
merged 25 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
872bee9
Merge branch 'wave9/create-onboarding-navigator' into wave9/require-n…
MaciejSWM Feb 21, 2024
6e76f5d
Fix centering of progress bar when back button present
MaciejSWM Feb 21, 2024
5500bf4
Show progress bar
MaciejSWM Feb 21, 2024
d663bc5
Open personal details modal when onboarding not finished
MaciejSWM Feb 21, 2024
c70e89e
Name + surname form
MaciejSWM Feb 21, 2024
ef9c1cd
Keyboard avoiding view for form
MaciejSWM Feb 21, 2024
dc57c1c
Add auto focus
MaciejSWM Feb 23, 2024
abd15f8
Navigate to purpose screen on Continue press
MaciejSWM Feb 23, 2024
6165511
Form validation
MaciejSWM Feb 23, 2024
87cb85f
Merge branch 'wave9/onboarding-flow' into wave9/require-name-surname
MaciejSWM Feb 23, 2024
14fc343
Fix conflicting style names
MaciejSWM Feb 23, 2024
8609295
Also add header prefix to container style
MaciejSWM Feb 23, 2024
8cc6030
Require name and surname
MaciejSWM Feb 23, 2024
41c0eff
Drop bottom margin
MaciejSWM Feb 23, 2024
eb9bcab
Drop close button
MaciejSWM Feb 23, 2024
0b7894c
Custom error message
MaciejSWM Feb 23, 2024
c6c16a4
Control trimming values before validation and submit
MaciejSWM Feb 26, 2024
d61da75
Do not trim values and check string length to decide if it's empty
MaciejSWM Feb 26, 2024
abab618
Custom fixErrorAlert message
MaciejSWM Feb 27, 2024
308cf29
Drop unused code
MaciejSWM Feb 27, 2024
f90adc8
Revert custom error messages
MaciejSWM Feb 27, 2024
89df038
Spanish translations
MaciejSWM Feb 27, 2024
28ea31f
Drop useless variable
MaciejSWM Feb 28, 2024
459c677
Comment usage of undefined
MaciejSWM Feb 28, 2024
6d72726
Calculated lineHeight
MaciejSWM Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProvider
/** Should validate function be called when the value of the input is changed */
shouldValidateOnChange?: boolean;

/** Whether to remove invisible characters from strings before validation and submission */
shouldTrimValues?: boolean;

/** Styles that will be applied to the submit button only */
submitButtonStyles?: StyleProp<ViewStyle>;

Expand All @@ -89,6 +92,7 @@ function FormProvider(
enabledWhenOffline = false,
draftValues,
onSubmit,
shouldTrimValues = true,
...rest
}: FormProviderProps,
forwardedRef: ForwardedRef<FormRef>,
Expand All @@ -102,7 +106,7 @@ function FormProvider(

const onValidate = useCallback(
(values: FormOnyxValues, shouldClearServerError = true) => {
const trimmedStringValues = ValidationUtils.prepareValues(values);
const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(values) : values;

if (shouldClearServerError) {
FormActions.clearErrors(formID);
Expand Down Expand Up @@ -158,7 +162,7 @@ function FormProvider(

return touchedInputErrors;
},
[errors, formID, validate],
[errors, formID, validate, shouldTrimValues],
);

// When locales change from another session of the same account,
Expand All @@ -170,7 +174,7 @@ function FormProvider(
}

// Prepare validation values
const trimmedStringValues = ValidationUtils.prepareValues(inputValues);
const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues;

// Validate in order to make sure the correct error translations are displayed,
// making sure to not clear server errors if they exist
Expand All @@ -195,7 +199,7 @@ function FormProvider(
}

// Prepare values before submitting
const trimmedStringValues = ValidationUtils.prepareValues(inputValues);
const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues;

// Touches all form inputs, so we can validate the entire form
Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true));
Expand All @@ -211,7 +215,7 @@ function FormProvider(
}

onSubmit(trimmedStringValues);
}, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]);
}, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate, shouldTrimValues]);

const resetForm = useCallback(
(optionalValue: FormOnyxValues) => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Form/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function FormWrapper({
isSubmitActionDangerous = false,
formID,
scrollContextEnabled = false,
fixErrorsAlert,
shouldHideFixErrorsAlert = false,
disablePressOnEnter = true,
}: FormWrapperProps) {
Expand Down Expand Up @@ -108,7 +109,7 @@ function FormWrapper({
buttonText={submitButtonText}
isAlertVisible={((!isEmptyObject(errors) || !isEmptyObject(formState?.errorFields)) && !shouldHideFixErrorsAlert) || !!errorMessage}
isLoading={!!formState?.isLoading}
message={isEmptyObject(formState?.errorFields) ? errorMessage : undefined}
message={fixErrorsAlert ?? (isEmptyObject(formState?.errorFields) ? errorMessage : undefined)}
onSubmit={onSubmit}
footerContent={footerContent}
onFixTheErrorsLinkPressed={onFixTheErrorsLinkPressed}
Expand Down Expand Up @@ -139,6 +140,7 @@ function FormWrapper({
submitButtonStyles,
submitFlexEnabled,
submitButtonText,
fixErrorsAlert,
shouldHideFixErrorsAlert,
onFixTheErrorsLinkPressed,
disablePressOnEnter,
Expand Down
3 changes: 3 additions & 0 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
/** Whether the form submit action is dangerous */
isSubmitActionDangerous?: boolean;

/** Custom error message to show when there is an error in the form */
fixErrorsAlert?: string;

/** Should fix the errors alert be displayed when there is an error in the form */
shouldHideFixErrorsAlert?: boolean;

Expand Down
31 changes: 12 additions & 19 deletions src/components/HeaderWithBackButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,17 @@ function HeaderWithBackButton({
const middleContent = useMemo(() => {
if (progressBarPercentage) {
return (
<View>
<View style={styles.progressBarWrapper}>
<View style={[{width: `${progressBarPercentage}%`}, styles.progressBar]} />
<>
{/* Reserves as much space for the middleContent as possible */}
<View style={styles.flexGrow1} />
{/* Uses absolute positioning so that it's always centered instead of being affected by the
presence or absence of back/close buttons to the left/right of it */}
<View style={styles.headerProgressBarContainer}>
<View style={styles.headerProgressBar}>
<View style={[{width: `${progressBarPercentage}%`}, styles.headerProgressBarFill]} />
</View>
</View>
</View>
</>
);
}

Expand All @@ -99,21 +105,7 @@ function HeaderWithBackButton({
textStyles={titleColor ? [StyleUtils.getTextColorStyle(titleColor)] : []}
/>
);
}, [
StyleUtils,
policy,
progressBarPercentage,
report,
shouldEnableDetailPageNavigation,
shouldShowAvatarWithDisplay,
stepCounter,
styles.progressBar,
styles.progressBarWrapper,
subtitle,
title,
titleColor,
translate,
]);
}, [StyleUtils, policy, progressBarPercentage, report, shouldEnableDetailPageNavigation, shouldShowAvatarWithDisplay, stepCounter, styles, subtitle, title, titleColor, translate]);

return (
<View
Expand All @@ -125,6 +117,7 @@ function HeaderWithBackButton({
isCentralPaneSettings && styles.headerBarDesktopHeight,
shouldShowBorderBottom && styles.borderBottom,
shouldShowBackButton && styles.pl2,
progressBarPercentage !== undefined && styles.pl2,
MaciejSWM marked this conversation as resolved.
Show resolved Hide resolved
shouldOverlay && StyleSheet.absoluteFillObject,
]}
>
Expand Down
8 changes: 8 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,14 @@ export default {
loginForm: 'Login form',
notYou: ({user}: NotYouParams) => `Not ${user}?`,
},
onboarding: {
welcome: 'Welcome!',
whatsYourName: "What's your name?",
error: {
requiredFirstName: 'Please input your first name to continue',
requiredLasttName: 'Please input your last name to continue',
},
},
personalDetails: {
error: {
containsReservedWord: 'Name cannot contain the words Expensify or Concierge',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,14 @@ export default {
loginForm: 'Formulario de inicio de sesión',
notYou: ({user}: NotYouParams) => `¿No eres ${user}?`,
},
onboarding: {
welcome: '¡Bienvenido!',
whatsYourName: '¿Cómo te llamas?',
error: {
requiredFirstName: 'Por favor, ingresa tu primer nombre para continuar',
requiredLasttName: 'Por favor, ingresa tu apellido para continuar',
},
},
personalDetails: {
error: {
containsReservedWord: 'El nombre no puede contener las palabras Expensify o Concierge',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
return;
}

Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARD)});
Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS)});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingApp]);

Expand Down
2 changes: 1 addition & 1 deletion src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ function isValidPersonName(value: string) {
/**
* Checks if the provided string includes any of the provided reserved words
*/
function doesContainReservedWord(value: string, reservedWords: string[]): boolean {
function doesContainReservedWord(value: string, reservedWords: readonly string[]): boolean {
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
const valueToCheck = value.trim().toLowerCase();
return reservedWords.some((reservedWord) => valueToCheck.includes(reservedWord.toLowerCase()));
}
Expand Down
10 changes: 8 additions & 2 deletions src/libs/actions/PersonalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from
import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
import * as Session from './Session';

type OptionsProps = {
preventGoBack?: boolean;
};

let currentUserEmail = '';
let currentUserAccountID = -1;
Onyx.connect({
Expand Down Expand Up @@ -70,7 +74,7 @@ function updatePronouns(pronouns: string) {
Navigation.goBack();
}

function updateDisplayName(firstName: string, lastName: string) {
function updateDisplayName(firstName: string, lastName: string, options: OptionsProps = {}) {
if (currentUserAccountID) {
const parameters: UpdateDisplayNameParams = {firstName, lastName};

Expand All @@ -94,7 +98,9 @@ function updateDisplayName(firstName: string, lastName: string) {
});
}

Navigation.goBack();
if (!options.preventGoBack) {
Navigation.goBack();
}
}

function updateLegalName(legalFirstName: string, legalLastName: string) {
Expand Down
130 changes: 111 additions & 19 deletions src/pages/OnboardingPersonalDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,130 @@
import React, {useCallback} from 'react';
import {View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as Report from '@userActions/Report';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as PersonalDetails from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/DisplayNameForm';

function OnboardingPersonalDetails() {
type OnboardingPersonalDetailsProps = WithCurrentUserPersonalDetailsProps;

function OnboardingPersonalDetails({currentUserPersonalDetails}: OnboardingPersonalDetailsProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {windowHeight} = useWindowDimensions();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useOnboardingLayout();
const theme = useTheme();
const currentUserDetails = currentUserPersonalDetails || {};
MaciejSWM marked this conversation as resolved.
Show resolved Hide resolved

const closeModal = useCallback(() => {
Report.dismissEngagementModal();
Navigation.goBack();
const saveAndNavigate = useCallback((values: FormOnyxValues<'displayNameForm'>) => {
PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim(), {preventGoBack: true});

Navigation.navigate(ROUTES.ONBOARDING_PURPOSE);
}, []);

const validate = (values: FormOnyxValues<'displayNameForm'>) => {
const errors = {};

// First we validate the first name field
if (values.firstName.length === 0) {
ErrorUtils.addErrorMessage(errors, 'firstName', 'onboarding.error.requiredFirstName');
}
if (!ValidationUtils.isValidDisplayName(values.firstName)) {
ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter');
}
if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord');
}

// Then we validate the last name field
if (values.lastName.length === 0) {
ErrorUtils.addErrorMessage(errors, 'lastName', 'onboarding.error.requiredLasttName');
}
if (!ValidationUtils.isValidDisplayName(values.lastName)) {
ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.hasInvalidCharacter');
}
if (ValidationUtils.doesContainReservedWord(values.lastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.containsReservedWord');
}

return errors;
};

return (
<View style={[styles.defaultModalContainer, styles.w100, styles.h100, !shouldUseNarrowLayout && styles.pt8]}>
<View style={{maxHeight: windowHeight}}>
<HeaderWithBackButton
shouldShowCloseButton
shouldShowBackButton={false}
onCloseButtonPress={closeModal}
shouldOverlay
iconFill={theme.iconColorfulBackground}
/>
</View>
<View style={[styles.h100, styles.defaultModalContainer, !shouldUseNarrowLayout && styles.pt8]}>
<HeaderWithBackButton
shouldShowBackButton={false}
iconFill={theme.iconColorfulBackground}
progressBarPercentage={33.3}
/>
<KeyboardAvoidingView
style={[styles.flex1, styles.dFlex]}
behavior="padding"
>
<FormProvider
style={[styles.flexGrow1, styles.mt5, shouldUseNarrowLayout ? styles.mh8 : styles.mh5]}
formID={ONYXKEYS.FORMS.DISPLAY_NAME_FORM}
validate={validate}
onSubmit={saveAndNavigate}
submitButtonText={translate('common.continue')}
enabledWhenOffline
submitFlexEnabled
shouldValidateOnBlur
shouldValidateOnChange
shouldTrimValues={false}
>
<View style={[shouldUseNarrowLayout ? styles.flexRow : styles.flexColumn, styles.mb5]}>
<Text style={[styles.textHeroSmall]}>{translate('onboarding.welcome')} </Text>
<Text style={[styles.textHeroSmall]}>{translate('onboarding.whatsYourName')}</Text>
</View>
<View style={styles.mb4}>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.FIRST_NAME}
name="fname"
label={translate('common.firstName')}
aria-label={translate('common.firstName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={currentUserDetails?.firstName}
maxLength={CONST.DISPLAY_NAME.MAX_LENGTH}
spellCheck={false}
autoFocus
/>
</View>
<View>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.LAST_NAME}
name="lname"
label={translate('common.lastName')}
aria-label={translate('common.lastName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={currentUserDetails?.lastName}
maxLength={CONST.DISPLAY_NAME.MAX_LENGTH}
spellCheck={false}
/>
</View>
</FormProvider>
</KeyboardAvoidingView>
</View>
);
}

OnboardingPersonalDetails.displayName = 'OnboardingPersonalDetails';
export default OnboardingPersonalDetails;

export default withCurrentUserPersonalDetails(OnboardingPersonalDetails);
3 changes: 1 addition & 2 deletions src/stories/HeaderWithBackButton.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ Attachment.args = {
};
Profile.args = {
title: 'Profile',
shouldShowBackButton: true,
};
ProgressBar.args = {
title: 'ProgressBar',
shouldShowBackButton: true,
progressBarPercentage: 33,
shouldShowBackButton: false,
};

export default story;
Expand Down
Loading
Loading