Skip to content

Commit

Permalink
Merge pull request #51681 from allroundexperts/feat-48189
Browse files Browse the repository at this point in the history
Give users on a domain the ability to join their colleagues when the company is already using Expensify
  • Loading branch information
marcaaron authored Dec 13, 2024
2 parents 2084695 + c71dacd commit d59c91c
Show file tree
Hide file tree
Showing 41 changed files with 731 additions and 88 deletions.
8 changes: 8 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ const ONYXKEYS = {
/** Store the information of magic code */
VALIDATE_ACTION_CODE: 'validate_action_code',

/** A list of policies that a user can join */
JOINABLE_POLICIES: 'joinablePolicies',

/** Flag to indicate if the joinablePolicies are loading */
JOINABLE_POLICIES_LOADING: 'joinablePoliciesLoading',

/** Information about the current session (authToken, accountID, email, loading, error) */
SESSION: 'session',
STASHED_SESSION: 'stashedSession',
Expand Down Expand Up @@ -903,6 +909,8 @@ type OnyxValuesMapping = {
[ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList;
[ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction;
[ONYXKEYS.VALIDATE_ACTION_CODE]: OnyxTypes.ValidateMagicCodeAction;
[ONYXKEYS.JOINABLE_POLICIES]: OnyxTypes.JoinablePolicies;
[ONYXKEYS.JOINABLE_POLICIES_LOADING]: boolean;
[ONYXKEYS.SESSION]: OnyxTypes.Session;
[ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata;
[ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session;
Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,10 @@ const ROUTES = {
route: 'onboarding/personal-details',
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/personal-details`, backTo),
},
ONBOARDING_PRIVATE_DOMAIN: {
route: 'onboarding/private-domain',
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/private-domain`, backTo),
},
ONBOARDING_EMPLOYEES: {
route: 'onboarding/employees',
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/employees`, backTo),
Expand All @@ -1385,6 +1389,10 @@ const ROUTES = {
route: 'onboarding/purpose',
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/purpose`, backTo),
},
ONBOARDING_WORKSPACES: {
route: 'onboarding/join-workspaces',
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/join-workspaces`, backTo),
},
WELCOME_VIDEO_ROOT: 'onboarding/welcome-video',
EXPLANATION_MODAL_ROOT: 'onboarding/explanation',
MIGRATED_USER_WELCOME_MODAL: 'onboarding/migrated-user-welcome',
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,8 +579,10 @@ const SCREENS = {
ONBOARDING: {
PERSONAL_DETAILS: 'Onboarding_Personal_Details',
PURPOSE: 'Onboarding_Purpose',
PRIVATE_DOMAIN: 'Onboarding_Private_Domain',
EMPLOYEES: 'Onboarding_Employees',
ACCOUNTING: 'Onboarding_Accounting',
WORKSPACES: 'Onboarding_Workspaces',
},

WELCOME_VIDEO: {
Expand Down
23 changes: 23 additions & 0 deletions src/components/OnboardingWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen';

type OnboardingWrapperProps = {
/** Rendered child component */
children: React.ReactNode;
};

function OnboardingWrapper({children}: OnboardingWrapperProps) {
const styles = useThemeStyles();

return (
<FocusTrapForScreens>
<View style={styles.h100}>{children}</View>
</FocusTrapForScreens>
);
}

OnboardingWrapper.displayName = 'OnboardingWrapper';

export default OnboardingWrapper;
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ type ValidateCodeFormProps = {
/** Function to clear error of the form */
clearError: () => void;

/** Whether to show the verify button */
hideSubmitButton?: boolean;

/** Function is called when validate code modal is mounted and on magic code resend */
sendValidateCode: () => void;

Expand All @@ -82,6 +85,7 @@ function BaseValidateCodeForm({
clearError,
sendValidateCode,
buttonStyles,
hideSubmitButton,
isLoading,
}: ValidateCodeFormProps) {
const {translate} = useLocalize();
Expand Down Expand Up @@ -264,16 +268,18 @@ function BaseValidateCodeForm({
onClose={() => clearError()}
style={buttonStyles}
>
<Button
isDisabled={isOffline}
text={translate('common.verify')}
onPress={validateAndSubmitForm}
style={[styles.mt4]}
success
large
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isLoading={account?.isLoading || isLoading}
/>
{!hideSubmitButton && (
<Button
isDisabled={isOffline}
text={translate('common.verify')}
onPress={validateAndSubmitForm}
style={[styles.mt4]}
success
large
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isLoading={account?.isLoading || isLoading}
/>
)}
</OfflineWithFeedback>
</>
);
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/useOnboardingFlow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useEffect} from 'react';
import {NativeModules} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import * as LoginUtils from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
Expand All @@ -25,6 +26,9 @@ function useOnboardingFlowRouter() {

const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING);

const [session] = useOnyx(ONYXKEYS.SESSION);
const isPrivateDomain = !!session?.email && !LoginUtils.isEmailPublicDomain(session?.email);

const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY);
useEffect(() => {
if (isLoadingOnyxValue(isOnboardingCompletedMetadata, tryNewDotdMetadata, dismissedProductTrainingMetadata)) {
Expand Down Expand Up @@ -57,13 +61,13 @@ function useOnboardingFlowRouter() {
// But if the hybrid app onboarding is completed, but NewDot onboarding is not completed, we start NewDot onboarding flow
// This is a special case when user created an account from NewDot without finishing the onboarding flow and then logged in from OldDot
if (isHybridAppOnboardingCompleted === true && isOnboardingCompleted === false) {
OnboardingFlow.startOnboardingFlow();
OnboardingFlow.startOnboardingFlow(isPrivateDomain);
}
}

// If the user is not transitioning from OldDot to NewDot, we should start NewDot onboarding flow if it's not completed yet
if (!NativeModules.HybridAppModule && isOnboardingCompleted === false) {
OnboardingFlow.startOnboardingFlow();
OnboardingFlow.startOnboardingFlow(isPrivateDomain);
}
}, [
isOnboardingCompleted,
Expand All @@ -76,6 +80,7 @@ function useOnboardingFlowRouter() {
dismissedProductTrainingMetadata,
dismissedProductTraining?.migratedUserWelcomeModal,
dismissedProductTraining,
isPrivateDomain,
]);

return {isOnboardingCompleted, isHybridAppOnboardingCompleted};
Expand Down
12 changes: 12 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ import type {
WelcomeNoteParams,
WelcomeToRoomParams,
WeSentYouMagicSignInLinkParams,
WorkspaceMemberList,
WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
WorkspaceYouMayJoin,
YourPlanPriceParams,
ZipCodeExampleFormatParams,
} from './params';
Expand Down Expand Up @@ -478,6 +480,7 @@ const translations = {
links: 'Links',
days: 'days',
rename: 'Rename',
skip: 'Skip',
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `Need something specific? Chat with your account manager, ${accountManagerDisplayName}.`,
chatNow: 'Chat now',
},
Expand Down Expand Up @@ -1753,6 +1756,11 @@ const translations = {
},
getStarted: 'Get started',
whatsYourName: "What's your name?",
peopleYouMayKnow: 'People you may know are already here! Verify your email to join them.',
workspaceYouMayJoin: ({domain, email}: WorkspaceYouMayJoin) => `Someone from ${domain} has already created a workspace. Please enter the magic code sent to ${email}.`,
joinAWorkspace: 'Join a workspace',
listOfWorkspaces: "Here's the list of workspaces you can join. Don't worry, you can always join them later if you prefer.",
workspaceMemberList: ({employeeCount, policyOwner}: WorkspaceMemberList) => `${employeeCount} member${employeeCount > 1 ? 's' : ''}${policyOwner}`,
whereYouWork: 'Where do you work?',
errorSelection: 'Please make a selection to continue.',
purpose: {
Expand Down Expand Up @@ -2766,6 +2774,10 @@ const translations = {
noAccountsFound: 'No accounts found',
noAccountsFoundDescription: 'Add the account in QuickBooks Online and sync the connection again.',
},
workspaceList: {
joinNow: 'Join now',
askToJoin: 'Ask to join',
},
xero: {
organization: 'Xero organization',
organizationDescription: "Choose the Xero organization that you'd like to import data from.",
Expand Down
12 changes: 12 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ import type {
WelcomeNoteParams,
WelcomeToRoomParams,
WeSentYouMagicSignInLinkParams,
WorkspaceMemberList,
WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams,
WorkspaceYouMayJoin,
YourPlanPriceParams,
ZipCodeExampleFormatParams,
} from './params';
Expand Down Expand Up @@ -349,6 +351,7 @@ const translations = {
semicolon: 'el punto y coma',
please: 'Por favor',
rename: 'Renombrar',
skip: 'Saltarse',
contactUs: 'contáctenos',
pleaseEnterEmailOrPhoneNumber: 'Por favor, escribe un correo electrónico o número de teléfono',
fixTheErrors: 'corrige los errores',
Expand Down Expand Up @@ -1756,6 +1759,11 @@ const translations = {
},
getStarted: 'Comenzar',
whatsYourName: '¿Cómo te llamas?',
peopleYouMayKnow: 'Las personas que tal vez conozcas ya están aquí. Verifica tu correo electrónico para unirte a ellos.',
workspaceMemberList: ({employeeCount, policyOwner}: WorkspaceMemberList) => `${employeeCount} miembro${employeeCount > 1 ? 's' : ''}${policyOwner}`,
workspaceYouMayJoin: ({domain, email}: WorkspaceYouMayJoin) => `Alguien de ${domain} ya ha creado un espacio de trabajo. Por favor, introduce el código mágico enviado a ${email}.`,
joinAWorkspace: 'Unirse a un espacio de trabajo',
listOfWorkspaces: 'Aquí está la lista de espacios de trabajo a los que puedes unirte. No te preocupes, siempre puedes unirte a ellos más tarde si lo prefieres.',
whereYouWork: '¿Dónde trabajas?',
errorSelection: 'Por favor selecciona una opción para continuar.',
purpose: {
Expand Down Expand Up @@ -2797,6 +2805,10 @@ const translations = {
noAccountsFound: 'No se ha encontrado ninguna cuenta',
noAccountsFoundDescription: 'Añade la cuenta en QuickBooks Online y sincroniza de nuevo la conexión.',
},
workspaceList: {
joinNow: 'Únete ahora',
askToJoin: 'Pedir unirse',
},
xero: {
organization: 'Organización Xero',
organizationDescription: 'Seleccione la organización en Xero desde la que está importando los datos.',
Expand Down
12 changes: 12 additions & 0 deletions src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,16 @@ type ImportedTypesParams = {
importedTypes: string[];
};

type WorkspaceYouMayJoin = {
domain: string;
email: string;
};

type WorkspaceMemberList = {
employeeCount: number;
policyOwner: string;
};

type FileLimitParams = {
fileLimit: number;
};
Expand Down Expand Up @@ -771,6 +781,8 @@ export type {
OptionalParam,
AssignCardParams,
ImportedTypesParams,
WorkspaceYouMayJoin,
WorkspaceMemberList,
ImportPerDiemRatesSuccessfullDescriptionParams,
CurrencyCodeParams,
CompanyNameParams,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/JoinAccessiblePolicyParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type JoinAccessiblePolicyParams = {
policyID: string;
};

export default JoinAccessiblePolicyParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type ValidateUserAndGetAccessiblePoliciesParams = {
validateCode: string;
};

export default ValidateUserAndGetAccessiblePoliciesParams;
2 changes: 2 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,12 @@ export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectP
export type {default as UpdateInvoiceCompanyNameParams} from './UpdateInvoiceCompanyNameParams';
export type {default as UpdateInvoiceCompanyWebsiteParams} from './UpdateInvoiceCompanyWebsiteParams';
export type {default as UpdateQuickbooksDesktopExpensesExportDestinationTypeParams} from './UpdateQuickbooksDesktopExpensesExportDestinationTypeParams';
export type {default as ValidateUserAndGetAccessiblePoliciesParams} from './ValidateUserAndGetAccessiblePoliciesParams';
export type {default as UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams} from './UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams';
export type {default as TogglePolicyPerDiemParams} from './TogglePolicyPerDiemParams';
export type {default as OpenPolicyPerDiemRatesPageParams} from './OpenPolicyPerDiemRatesPageParams';
export type {default as TogglePlatformMuteParams} from './TogglePlatformMuteParams';
export type {default as JoinAccessiblePolicyParams} from './JoinAccessiblePolicyParams';
export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams';
export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams';
export type {default as DismissProductTrainingParams} from './DismissProductTraining';
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ const WRITE_COMMANDS = {
SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax',
SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName',
JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink',
JOIN_ACCESSIBLE_POLICY: 'JoinAccessiblePolicy',
ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest',
DECLINE_JOIN_REQUEST: 'DeclineJoinRequest',
CREATE_POLICY_TAX: 'CreatePolicyTax',
Expand Down Expand Up @@ -438,6 +439,7 @@ const WRITE_COMMANDS = {
SELF_TOUR_VIEWED: 'SelfTourViewed',
UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName',
UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite',
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
DISMISS_PRODUCT_TRAINING: 'DismissProductTraining',
} as const;

Expand Down Expand Up @@ -676,6 +678,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SEARCH]: Parameters.SearchParams;
[WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX]: Parameters.SetPolicyCategoryTaxParams;
[WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[WRITE_COMMANDS.VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES]: Parameters.ValidateUserAndGetAccessiblePoliciesParams;
[WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
[WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams;
[WRITE_COMMANDS.SET_POLICY_TAXES_CURRENCY_DEFAULT]: Parameters.SetPolicyCurrencyDefaultParams;
Expand Down Expand Up @@ -889,6 +892,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_INVOICE_COMPANY_NAME]: Parameters.UpdateInvoiceCompanyNameParams;
[WRITE_COMMANDS.UPDATE_INVOICE_COMPANY_WEBSITE]: Parameters.UpdateInvoiceCompanyWebsiteParams;

[WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY]: Parameters.JoinAccessiblePolicyParams;
// Dismis Product Training
[WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING]: Parameters.DismissProductTrainingParams;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import OnboardingRefManager from '@libs/OnboardingRefManager';
import OnboardingAccounting from '@pages/OnboardingAccounting';
import OnboardingEmployees from '@pages/OnboardingEmployees';
import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails';
import OnboardingPrivateDomain from '@pages/OnboardingPrivateDomain';
import OnboardingPurpose from '@pages/OnboardingPurpose';
import OnboardingWorkspaces from '@pages/OnboardingWorkspaces';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
Expand Down Expand Up @@ -74,6 +76,14 @@ function OnboardingModalNavigator() {
name={SCREENS.ONBOARDING.PERSONAL_DETAILS}
component={OnboardingPersonalDetails}
/>
<Stack.Screen
name={SCREENS.ONBOARDING.PRIVATE_DOMAIN}
component={OnboardingPrivateDomain}
/>
<Stack.Screen
name={SCREENS.ONBOARDING.WORKSPACES}
component={OnboardingWorkspaces}
/>
<Stack.Screen
name={SCREENS.ONBOARDING.EMPLOYEES}
component={OnboardingEmployees}
Expand Down
5 changes: 4 additions & 1 deletion src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useThemePreference from '@hooks/useThemePreference';
import Firebase from '@libs/Firebase';
import {FSPage} from '@libs/Fullstory';
import Log from '@libs/Log';
import * as LoginUtils from '@libs/LoginUtils';
import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
Expand Down Expand Up @@ -92,6 +93,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {setActiveWorkspaceID} = useActiveWorkspace();
const [user] = useOnyx(ONYXKEYS.USER);
const [session] = useOnyx(ONYXKEYS.SESSION);
const isPrivateDomain = !!session?.email && !LoginUtils.isEmailPublicDomain(session?.email);

const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
Expand All @@ -105,7 +108,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh
// If the user haven't completed the flow, we want to always redirect them to the onboarding flow.
// We also make sure that the user is authenticated.
if (!NativeModules.HybridAppModule && !isOnboardingCompleted && authenticated && !shouldShowRequire2FAModal) {
const {adaptedState} = getAdaptedStateFromPath(getOnboardingInitialPath(), linkingConfig.config);
const {adaptedState} = getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config);
return adaptedState;
}

Expand Down
Loading

0 comments on commit d59c91c

Please sign in to comment.