Skip to content

Commit e0f7cc0

Browse files
authored
Merge pull request #54186 from allgandalf/privateDomainScreen
2 parents 17704a9 + 109b1c2 commit e0f7cc0

37 files changed

+1394
-30
lines changed

src/CONST.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5615,6 +5615,9 @@ const CONST = {
56155615
DEBUG: 'DEBUG',
56165616
},
56175617
},
5618+
5619+
// We need to store this server side error in order to not show the blocking screen when the error is for invalid code
5620+
MERGE_ACCOUNT_INVALID_CODE_ERROR: '401 Not authorized - Invalid validateCode',
56185621
REIMBURSEMENT_ACCOUNT: {
56195622
DEFAULT_DATA: {
56205623
achData: {

src/ONYXKEYS.ts

+3
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,8 @@ const ONYXKEYS = {
799799
RULES_CUSTOM_FORM_DRAFT: 'rulesCustomFormDraft',
800800
DEBUG_DETAILS_FORM: 'debugDetailsForm',
801801
DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft',
802+
ONBOARDING_WORK_EMAIL_FORM: 'onboardingWorkEmailForm',
803+
ONBOARDING_WORK_EMAIL_FORM_DRAFT: 'onboardingWorkEmailFormDraft',
802804
MERGE_ACCOUNT_DETAILS_FORM: 'mergeAccountDetailsForm',
803805
MERGE_ACCOUNT_DETAILS_FORM_DRAFT: 'mergeAccountDetailsFormDraft',
804806
WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm',
@@ -903,6 +905,7 @@ type OnyxFormValuesMapping = {
903905
[ONYXKEYS.FORMS.RULES_CUSTOM_FORM]: FormTypes.RulesCustomForm;
904906
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
905907
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm;
908+
[ONYXKEYS.FORMS.ONBOARDING_WORK_EMAIL_FORM]: FormTypes.OnboardingWorkEmailForm;
906909
[ONYXKEYS.FORMS.MERGE_ACCOUNT_DETAILS_FORM]: FormTypes.MergeAccountDetailsForm;
907910
[ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm;
908911
[ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;

src/ROUTES.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,14 @@ const ROUTES = {
18831883
route: 'onboarding/join-workspaces',
18841884
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/join-workspaces`, backTo),
18851885
},
1886+
ONBOARDING_WORK_EMAIL: {
1887+
route: 'onboarding/work-email',
1888+
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/work-email`, backTo),
1889+
},
1890+
ONBOARDING_WORK_EMAIL_VALIDATION: {
1891+
route: 'onboarding/work-email-validation',
1892+
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/work-email-validation`, backTo),
1893+
},
18861894
WELCOME_VIDEO_ROOT: 'onboarding/welcome-video',
18871895
EXPLANATION_MODAL_ROOT: 'onboarding/explanation',
18881896
WORKSPACE_CONFIRMATION: {

src/SCREENS.ts

+2
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,8 @@ const SCREENS = {
642642
EMPLOYEES: 'Onboarding_Employees',
643643
ACCOUNTING: 'Onboarding_Accounting',
644644
WORKSPACES: 'Onboarding_Workspaces',
645+
WORK_EMAIL: 'Onboarding_Work_Email',
646+
WORK_EMAIL_VALIDATION: 'Onboarding_Work_Email_Validation',
645647
},
646648

647649
WELCOME_VIDEO: {

src/components/AutoEmailLink.tsx

+29-18
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,43 @@ type AutoEmailLinkProps = {
1818

1919
function AutoEmailLink({text, style}: AutoEmailLinkProps) {
2020
const styles = useThemeStyles();
21+
const emailRegex = COMMON_CONST.REG_EXP.EXTRACT_EMAIL;
22+
const matches = [...text.matchAll(emailRegex)];
23+
24+
if (matches.length === 0) {
25+
return <Text style={style}>{text}</Text>;
26+
}
27+
28+
let lastIndex = 0;
29+
2130
return (
2231
<Text style={style}>
23-
{text.split(COMMON_CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => {
24-
if (COMMON_CONST.REG_EXP.EMAIL.test(str)) {
25-
return (
26-
<TextLink
27-
// eslint-disable-next-line react/no-array-index-key
28-
key={`${index}-${str}`}
29-
href={`mailto:${str}`}
30-
style={styles.link}
31-
>
32-
{str}
33-
</TextLink>
34-
);
32+
{matches.flatMap((match, index) => {
33+
const email = match[0];
34+
const startIndex = match.index ?? 0;
35+
const elements = [];
36+
37+
// Push plain text before email
38+
if (startIndex > lastIndex) {
39+
elements.push(text.slice(lastIndex, startIndex));
3540
}
3641

37-
return (
38-
<Text
39-
style={style}
42+
// Push email as a link
43+
elements.push(
44+
<TextLink
4045
// eslint-disable-next-line react/no-array-index-key
41-
key={`${index}-${str}`}
46+
key={`email-${index}`}
47+
href={`mailto:${email}`}
48+
style={styles.link}
4249
>
43-
{str}
44-
</Text>
50+
{email}
51+
</TextLink>,
4552
);
53+
54+
lastIndex = startIndex + email.length;
55+
return elements;
4656
})}
57+
{lastIndex < text.length && text.slice(lastIndex)}
4758
</Text>
4859
);
4960
}

src/components/Form/FormProvider.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProps<TF
7373
/** Whether HTML is allowed in form inputs */
7474
allowHTML?: boolean;
7575

76+
/** Whether to render the submit button above the footer. */
77+
shouldRenderFooterAboveSubmit?: boolean;
78+
7679
/** Whether the form is loading */
7780
isLoading?: boolean;
7881

@@ -98,13 +101,14 @@ function FormProvider(
98101
shouldTrimValues = true,
99102
allowHTML = false,
100103
isLoading = false,
104+
shouldRenderFooterAboveSubmit = false,
101105
...rest
102106
}: FormProviderProps,
103107
forwardedRef: ForwardedRef<FormRef>,
104108
) {
105-
const [network] = useOnyx(ONYXKEYS.NETWORK);
106-
const [formState] = useOnyx<OnyxFormKey, Form>(`${formID}`);
107-
const [draftValues] = useOnyx<OnyxFormDraftKey, Form>(`${formID}Draft`);
109+
const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true});
110+
const [formState] = useOnyx<OnyxFormKey, Form>(`${formID}`, {canBeMissing: true});
111+
const [draftValues] = useOnyx<OnyxFormDraftKey, Form>(`${formID}Draft`, {canBeMissing: true});
108112
const {preferredLocale, translate} = useLocalize();
109113
const inputRefs = useRef<InputRefs>({});
110114
const touchedInputs = useRef<Record<string, boolean>>({});
@@ -432,6 +436,7 @@ function FormProvider(
432436
errors={errors}
433437
isLoading={isLoading}
434438
enabledWhenOffline={enabledWhenOffline}
439+
shouldRenderFooterAboveSubmit={shouldRenderFooterAboveSubmit}
435440
>
436441
{typeof children === 'function' ? children({inputValues}) : children}
437442
</FormWrapper>

src/components/Form/FormWrapper.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type FormWrapperProps = ChildrenProps &
3838
/** Callback to submit the form */
3939
onSubmit: () => void;
4040

41+
/** should render the extra button above submit button */
42+
shouldRenderFooterAboveSubmit?: boolean;
43+
4144
/** Whether the form is loading */
4245
isLoading?: boolean;
4346

@@ -76,6 +79,7 @@ function FormWrapper({
7679
shouldHideFixErrorsAlert = false,
7780
disablePressOnEnter = false,
7881
isSubmitDisabled = false,
82+
shouldRenderFooterAboveSubmit = false,
7983
isLoading = false,
8084
shouldScrollToEnd = false,
8185
addBottomSafeAreaPadding,
@@ -87,7 +91,7 @@ function FormWrapper({
8791
const formRef = useRef<RNScrollView>(null);
8892
const formContentRef = useRef<View>(null);
8993

90-
const [formState] = useOnyx<OnyxFormKey, Form>(`${formID}`);
94+
const [formState] = useOnyx<OnyxFormKey, Form>(`${formID}`, {canBeMissing: true});
9195

9296
const errorMessage = useMemo(() => (formState ? getLatestErrorMessage(formState) : undefined), [formState]);
9397

@@ -168,6 +172,7 @@ function FormWrapper({
168172
isSubmitActionDangerous={isSubmitActionDangerous}
169173
disablePressOnEnter={disablePressOnEnter}
170174
enterKeyEventListenerPriority={1}
175+
shouldRenderFooterAboveSubmit={shouldRenderFooterAboveSubmit}
171176
shouldBlendOpacity={shouldSubmitButtonBlendOpacity}
172177
/>
173178
),
@@ -196,6 +201,7 @@ function FormWrapper({
196201
submitButtonStylesWithBottomSafeAreaPadding,
197202
submitButtonText,
198203
submitFlexEnabled,
204+
shouldRenderFooterAboveSubmit,
199205
],
200206
);
201207

src/components/Form/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
164164
/** Disable press on enter for submit button */
165165
disablePressOnEnter?: boolean;
166166

167+
/** Render extra button above submit button */
168+
shouldRenderFooterAboveSubmit?: boolean;
167169
/**
168170
* Determines whether the form should automatically scroll to the end upon rendering or when the value changes.
169171
* If `true`, the form will smoothly scroll to the bottom after interactions have completed.

src/components/FormAlertWithSubmitButton.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type FormAlertWithSubmitButtonProps = {
6363
/** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */
6464
enterKeyEventListenerPriority?: number;
6565

66+
/** should render the extra button above submit button */
67+
shouldRenderFooterAboveSubmit?: boolean;
68+
6669
/**
6770
* Whether the button should have a background layer in the color of theme.appBG.
6871
* This is needed for buttons that allow content to display under them.
@@ -92,6 +95,7 @@ function FormAlertWithSubmitButton({
9295
useSmallerSubmitButtonSize = false,
9396
errorMessageStyle,
9497
enterKeyEventListenerPriority = 0,
98+
shouldRenderFooterAboveSubmit = false,
9599
shouldBlendOpacity = false,
96100
addButtonBottomPadding = true,
97101
}: FormAlertWithSubmitButtonProps) {
@@ -115,6 +119,7 @@ function FormAlertWithSubmitButton({
115119
>
116120
{(isOffline: boolean | undefined) => (
117121
<View>
122+
{shouldRenderFooterAboveSubmit && footerContent}
118123
{isOffline && !enabledWhenOffline ? (
119124
<Button
120125
success
@@ -143,7 +148,7 @@ function FormAlertWithSubmitButton({
143148
large={!useSmallerSubmitButtonSize}
144149
/>
145150
)}
146-
{footerContent}
151+
{!shouldRenderFooterAboveSubmit && footerContent}
147152
</View>
148153
)}
149154
</FormAlertWrapper>

src/components/ValidateCodeActionForm/type.ts

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ type ValidateCodeActionFormProps = {
3333
/** Ref for validate code form */
3434
forwardedRef: ForwardedRef<ValidateCodeFormHandle>;
3535

36+
/** Whether to show the skip button */
37+
shouldShowSkipButton?: boolean;
38+
39+
/** Function to call when the skip button is pressed */
40+
handleSkipButtonPress?: () => void;
41+
3642
/** Text for submit button */
3743
submitButtonText?: string;
3844

src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx

+19-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ type ValidateCodeFormProps = {
7575

7676
/** Whether the form is loading or not */
7777
isLoading?: boolean;
78+
79+
/** Whether to show skip button */
80+
shouldShowSkipButton?: boolean;
81+
82+
/** Function to call when skip button is pressed */
83+
handleSkipButtonPress?: () => void;
7884
};
7985

8086
function BaseValidateCodeForm({
@@ -91,6 +97,8 @@ function BaseValidateCodeForm({
9197
hideSubmitButton,
9298
submitButtonText,
9399
isLoading,
100+
shouldShowSkipButton = false,
101+
handleSkipButtonPress,
94102
}: ValidateCodeFormProps) {
95103
const {translate} = useLocalize();
96104
const {isOffline} = useNetwork();
@@ -100,7 +108,7 @@ function BaseValidateCodeForm({
100108
const [formError, setFormError] = useState<ValidateCodeFormError>({});
101109
const [validateCode, setValidateCode] = useState('');
102110
const inputValidateCodeRef = useRef<MagicCodeInputHandle>(null);
103-
const [account = {}] = useOnyx(ONYXKEYS.ACCOUNT);
111+
const [account = {}] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true});
104112
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case
105113
const shouldDisableResendValidateCode = !!isOffline || account?.isLoading;
106114
const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -277,14 +285,23 @@ function BaseValidateCodeForm({
277285
messages={{0: translate('validateCodeModal.successfulNewCodeRequest')}}
278286
/>
279287
)}
288+
280289
<OfflineWithFeedback
281290
shouldDisplayErrorAbove
282291
pendingAction={validatePendingAction}
283292
errors={canShowError ? validateError : undefined}
284-
errorRowStyles={[styles.mt2]}
293+
errorRowStyles={[styles.mt2, styles.textWrap]}
285294
onClose={() => clearError()}
286295
style={buttonStyles}
287296
>
297+
{shouldShowSkipButton && (
298+
<Button
299+
text={translate('common.skip')}
300+
onPress={handleSkipButtonPress}
301+
success={false}
302+
large
303+
/>
304+
)}
288305
{!hideSubmitButton && (
289306
<Button
290307
isDisabled={isOffline}

src/languages/en.ts

+25
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ import type {
227227
WelcomeEnterMagicCodeParams,
228228
WelcomeToRoomParams,
229229
WeSentYouMagicSignInLinkParams,
230+
WorkEmailMergingBlockedParams,
231+
WorkEmailResendCodeParams,
230232
WorkspaceLockedPlanTypeParams,
231233
WorkspaceMemberList,
232234
WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
@@ -532,6 +534,7 @@ const translations = {
532534
skip: 'Skip',
533535
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `Need something specific? Chat with your account manager, ${accountManagerDisplayName}.`,
534536
chatNow: 'Chat now',
537+
workEmail: 'Work email',
535538
destination: 'Destination',
536539
subrate: 'Subrate',
537540
perDiem: 'Per diem',
@@ -2021,6 +2024,28 @@ const translations = {
20212024
error: {
20222025
requiredFirstName: 'Please input your first name to continue',
20232026
},
2027+
workEmail: {
2028+
title: 'What’s your work email?',
2029+
subtitle: 'Expensify works best when you connect your work email.',
2030+
explanationModal: {
2031+
descriptionOne: 'Forward to [email protected] for scanning',
2032+
descriptionTwo: 'Join your colleagues already using Expensify',
2033+
descriptionThree: 'Enjoy a more customized experience',
2034+
},
2035+
addWorkEmail: 'Add work email',
2036+
},
2037+
workEmailValidation: {
2038+
title: 'Verify your work email',
2039+
magicCodeSent: ({workEmail}: WorkEmailResendCodeParams) => `Please enter the magic code sent to ${workEmail}. It should arrive in a minute or two.`,
2040+
},
2041+
workEmailValidationError: {
2042+
publicEmail: 'Please enter a valid work email from a private domain e.g. [email protected]',
2043+
offline: 'We couldn’t add your work email as you appear to be offline',
2044+
},
2045+
mergeBlockScreen: {
2046+
title: 'Couldn’t add work email',
2047+
subtitle: ({workEmail}: WorkEmailMergingBlockedParams) => `We couldn’t add ${workEmail}. Please try again later in Settings or chat with Concierge for guidance.`,
2048+
},
20242049
},
20252050
featureTraining: {
20262051
doNotShowAgain: "Don't show me this again",

src/languages/es.ts

+26
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ import type {
227227
WelcomeEnterMagicCodeParams,
228228
WelcomeToRoomParams,
229229
WeSentYouMagicSignInLinkParams,
230+
WorkEmailMergingBlockedParams,
231+
WorkEmailResendCodeParams,
230232
WorkspaceLockedPlanTypeParams,
231233
WorkspaceMemberList,
232234
WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
@@ -523,6 +525,7 @@ const translations = {
523525
minuteAbbreviation: 'm',
524526
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `¿Necesitas algo específico? Habla con tu gerente de cuenta, ${accountManagerDisplayName}.`,
525527
chatNow: 'Chatear ahora',
528+
workEmail: 'correo electrónico de trabajo',
526529
destination: 'Destino',
527530
subrate: 'Subtasa',
528531
perDiem: 'Per diem',
@@ -2023,6 +2026,29 @@ const translations = {
20232026
error: {
20242027
requiredFirstName: 'Introduce tu nombre para continuar',
20252028
},
2029+
workEmail: {
2030+
title: 'Cuál es tu correo electrónico de trabajo',
2031+
subtitle: 'Expensify funciona mejor cuando conectas tu correo electrónico de trabajo.',
2032+
explanationModal: {
2033+
descriptionOne: 'Reenvía a [email protected] para escanear',
2034+
descriptionTwo: 'Únete a tus compañeros de trabajo que ya están usando Expensify',
2035+
descriptionThree: 'Disfruta de una experiencia más personalizada',
2036+
},
2037+
addWorkEmail: 'Añadir correo electrónico de trabajo',
2038+
},
2039+
workEmailValidation: {
2040+
title: 'Verifica tu correo electrónico de trabajo',
2041+
magicCodeSent: ({workEmail}: WorkEmailResendCodeParams) => `Por favor, introduce el código mágico enviado a ${workEmail}. Debería llegar en uno o dos minutos.`,
2042+
},
2043+
workEmailValidationError: {
2044+
publicEmail: 'Por favor, introduce un correo electrónico laboral válido de un dominio privado, por ejemplo: [email protected]',
2045+
offline: 'No pudimos añadir tu correo electrónico laboral porque parece que estás sin conexión.',
2046+
},
2047+
mergeBlockScreen: {
2048+
title: 'No se pudo añadir el correo electrónico de trabajo',
2049+
subtitle: ({workEmail}: WorkEmailMergingBlockedParams) =>
2050+
`No pudimos añadir ${workEmail}. Por favor, inténtalo de nuevo más tarde en Configuración o chatea con Concierge para obtener ayuda.`,
2051+
},
20262052
},
20272053
featureTraining: {
20282054
doNotShowAgain: 'No muestres esto otra vez',

0 commit comments

Comments
 (0)