diff --git a/Mobile-Expensify b/Mobile-Expensify index 1f36847737e6..0f12776c58b3 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 1f36847737e603a72b4b0662d94e8d22c0f16698 +Subproject commit 0f12776c58b3649f0e6d9a333a6c3ea6825bd937 diff --git a/android/app/build.gradle b/android/app/build.gradle index 8e5514cf9675..708e7035b8ec 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,8 +111,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009036711 - versionName "9.3.67-11" + versionCode 1009036712 + versionName "9.3.67-12" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index eb57c9a622bc..0e6d7921274d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.67.11 + 9.3.67.12 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5cccd33662bc..b9dd5669ef3f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.3.67 CFBundleVersion - 9.3.67.11 + 9.3.67.12 NSExtension NSExtensionPointIdentifier diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index 71b9012de36c..35519c3e3cff 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.3.67 CFBundleVersion - 9.3.67.11 + 9.3.67.12 NSExtension NSExtensionAttributes diff --git a/package-lock.json b/package-lock.json index eb40b0a97f99..a2a8523f9498 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.67-11", + "version": "9.3.67-12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.67-11", + "version": "9.3.67-12", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3b1978d4b896..66043cf0cb9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.67-11", + "version": "9.3.67-12", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST/index.ts b/src/CONST/index.ts index fb3627d456be..a61801d2960b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -887,7 +887,6 @@ const CONST = { BULK_DUPLICATE_REPORT: 'bulkDuplicateReport', BULK_EDIT: 'bulkEdit', NEW_MANUAL_EXPENSE_FLOW: 'newManualExpenseFlow', - SUBMIT_2026: 'submit2026', BULK_SUBMIT_APPROVE_PAY: 'bulkSubmitApprovePay', }, BUTTON_STATES: { @@ -3517,8 +3516,6 @@ const CONST = { // Often referred to as "collect" workspaces TEAM: 'team', - - SUBMIT: 'submit2026', }, RULE_CONDITIONS: { MATCHES: 'matches', @@ -3537,7 +3534,6 @@ const CONST = { ADMIN: 'admin', AUDITOR: 'auditor', USER: 'user', - EDITOR: 'editor', }, AUTO_REIMBURSEMENT_MAX_LIMIT_CENTS: 2000000, diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx index c1dee29c6d69..87fb63d098a4 100644 --- a/src/components/SidePanel/SidePanelContextProvider.tsx +++ b/src/components/SidePanel/SidePanelContextProvider.tsx @@ -10,7 +10,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import SidePanelActions from '@libs/actions/SidePanel'; import DateUtils from '@libs/DateUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; -import {canEditWorkspaceSettings, shouldShowPolicy} from '@libs/PolicyUtils'; +import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -82,7 +82,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) { const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM; const isRHPHomePage = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE; - const isUserAdmin = canEditWorkspaceSettings(activePolicy); + const isUserAdmin = isPolicyAdmin(activePolicy, sessionEmail); const isPolicyActive = shouldShowPolicy(activePolicy, false, sessionEmail ?? ''); const adminsChatReportID = activePolicy?.chatReportIDAdmins?.toString(); diff --git a/src/components/WorkspaceMemberRoleList.tsx b/src/components/WorkspaceMemberRoleList.tsx index 801a8698faba..432dd71ff5ae 100644 --- a/src/components/WorkspaceMemberRoleList.tsx +++ b/src/components/WorkspaceMemberRoleList.tsx @@ -1,15 +1,12 @@ -import {emailSelector} from '@selectors/Session'; import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import {isControlPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {isControlPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -35,7 +32,6 @@ type WorkspaceMemberRoleListProps = { function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLoading = false, onSelectRole = () => {}}: WorkspaceMemberRoleListProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [currentUserEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); const workspaceRoles: ListItemType[] = [ { @@ -62,18 +58,7 @@ function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLo ]; const isPolicyControl = isControlPolicy(policy); - // Only strict admins can assign the ADMIN role. Editors (e.g. Submit workspace owners) can - // invite/manage members but must not be able to escalate anyone to admin. - const canAssignAdminRole = isPolicyAdmin(policy, currentUserEmail); - const availableRoleItems: ListItemType[] = workspaceRoles.filter((item) => { - if (item.value === CONST.POLICY.ROLE.AUDITOR && !isPolicyControl) { - return false; - } - if (item.value === CONST.POLICY.ROLE.ADMIN && !canAssignAdminRole) { - return false; - } - return true; - }); + const availableRoleItems: ListItemType[] = workspaceRoles.filter((item) => isPolicyControl || item.value !== CONST.POLICY.ROLE.AUDITOR); return ( <> diff --git a/src/hooks/useAutoCreateSubmitWorkspace.ts b/src/hooks/useAutoCreateSubmitWorkspace.ts deleted file mode 100644 index 7d97960bffcb..000000000000 --- a/src/hooks/useAutoCreateSubmitWorkspace.ts +++ /dev/null @@ -1,115 +0,0 @@ -import {useCallback, useMemo} from 'react'; -import type {OnyxCollection} from 'react-native-onyx'; -import {navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; -import {createDisplayName} from '@libs/PersonalDetailsUtils'; -import {canEditWorkspaceSettings, isGroupPolicy} from '@libs/PolicyUtils'; -import {createWorkspace, generateDefaultWorkspaceName, generatePolicyID} from '@userActions/Policy/Policy'; -import {completeOnboarding} from '@userActions/Report'; -import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActions/Welcome'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; -import useOnboardingWorkspaceCreationState from './useOnboardingWorkspaceCreationState'; -import useOnyx from './useOnyx'; - -/** - * Hook that provides a function to auto-create a Submit workspace for EMPLOYER - * users during onboarding and complete the onboarding flow. - * - * Shared by BaseOnboardingPersonalDetails, BaseOnboardingPurpose, and BaseOnboardingWorkspaces. - */ -function useAutoCreateSubmitWorkspace() { - const { - onboardingPolicyID, - onboardingAdminsChatReportID, - introSelected, - isSelfTourViewed, - betas, - currentUserEmail, - currentUserAccountID, - localCurrencyCode, - activePolicy, - translate, - formatPhoneNumber, - isRestrictedPolicyCreation, - hasActiveAdminPolicies, - onboardingMessages, - lastWorkspaceNumber, - isSmallScreenWidth, - } = useOnboardingWorkspaceCreationState(); - - const groupPolicySelector = useMemo( - () => (policies: OnyxCollection) => Object.values(policies ?? {}).some((policy) => isGroupPolicy(policy) && canEditWorkspaceSettings(policy)), - [], - ); - const [hasEditableGroupPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPolicySelector}); - - const autoCreateSubmitWorkspace = useCallback( - (firstName: string, lastName: string) => { - const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasEditableGroupPolicy; - const displayName = createDisplayName(currentUserEmail, {firstName, lastName}, formatPhoneNumber); - - const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace - ? createWorkspace({ - policyOwnerEmail: undefined, - makeMeAdmin: true, - policyName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate, displayName), - policyID: generatePolicyID(), - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - currency: localCurrencyCode, - file: undefined, - shouldAddOnboardingTasks: false, - introSelected, - activePolicy, - currentUserAccountIDParam: currentUserAccountID, - currentUserEmailParam: currentUserEmail, - shouldAddGuideWelcomeMessage: false, - type: CONST.POLICY.TYPE.SUBMIT, - betas, - isSelfTourViewed, - hasActiveAdminPolicies, - }) - : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; - - completeOnboarding({ - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - onboardingMessage: onboardingMessages[CONST.ONBOARDING_CHOICES.EMPLOYER], - firstName, - lastName, - adminsChatReportID: newAdminsChatReportID, - onboardingPolicyID: newPolicyID, - introSelected, - isSelfTourViewed, - betas, - }); - - setOnboardingAdminsChatReportID(); - setOnboardingPolicyID(); - - navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(newPolicyID, isSmallScreenWidth); - }, - [ - currentUserEmail, - currentUserAccountID, - lastWorkspaceNumber, - translate, - formatPhoneNumber, - isRestrictedPolicyCreation, - onboardingPolicyID, - hasEditableGroupPolicy, - onboardingAdminsChatReportID, - localCurrencyCode, - introSelected, - activePolicy, - isSelfTourViewed, - onboardingMessages, - betas, - hasActiveAdminPolicies, - isSmallScreenWidth, - ], - ); - - return autoCreateSubmitWorkspace; -} - -export default useAutoCreateSubmitWorkspace; diff --git a/src/hooks/useAutoCreateTrackWorkspace.ts b/src/hooks/useAutoCreateTrackWorkspace.ts index 99cf11eef483..78dc14f3edb9 100644 --- a/src/hooks/useAutoCreateTrackWorkspace.ts +++ b/src/hooks/useAutoCreateTrackWorkspace.ts @@ -1,3 +1,4 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; import {useCallback, useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import isSidePanelReportSupported from '@components/SidePanel/isSidePanelReportSupported'; @@ -11,10 +12,17 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActio import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnboardingPurpose, OnboardingRHPVariant, Policy} from '@src/types/onyx'; +import useActivePolicy from './useActivePolicy'; import useArchivedReportsIdSet from './useArchivedReportsIdSet'; -import useOnboardingWorkspaceCreationState from './useOnboardingWorkspaceCreationState'; +import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useHasActiveAdminPolicies from './useHasActiveAdminPolicies'; +import useLastWorkspaceNumber from './useLastWorkspaceNumber'; +import useLocalize from './useLocalize'; +import useOnboardingMessages from './useOnboardingMessages'; import useOnyx from './useOnyx'; import usePermissions from './usePermissions'; +import usePreferredPolicy from './usePreferredPolicy'; +import useResponsiveLayout from './useResponsiveLayout'; /** * Hook that provides a function to auto-create a workspace for Track (PERSONAL_SPEND) @@ -23,57 +31,57 @@ import usePermissions from './usePermissions'; * Shared by BaseOnboardingPersonalDetails and BaseOnboardingPurpose. */ function useAutoCreateTrackWorkspace() { - const { - onboardingPolicyID, - onboardingAdminsChatReportID, - introSelected, - isSelfTourViewed, - betas, - currentUserEmail, - currentUserAccountID, - localCurrencyCode, - activePolicy, - translate, - formatPhoneNumber, - isRestrictedPolicyCreation, - hasActiveAdminPolicies, - onboardingMessages, - lastWorkspaceNumber, - isSmallScreenWidth, - } = useOnboardingWorkspaceCreationState(); - + const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID); + const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [session] = useOnyx(ONYXKEYS.SESSION); const paidGroupPolicySelector = useMemo( - () => (policies: OnyxCollection) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserEmail)), - [currentUserEmail], + () => (policies: OnyxCollection) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)), + [session?.email], ); const [hasPaidGroupAdminPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: paidGroupPolicySelector}); - const [conciergeChatReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const archivedReportsIdSet = useArchivedReportsIdSet(); const {isBetaEnabled} = usePermissions(); + const {translate, formatPhoneNumber} = useLocalize(); + const activePolicy = useActivePolicy(); + const {isRestrictedPolicyCreation} = usePreferredPolicy(); + const hasActiveAdminPolicies = useHasActiveAdminPolicies(); + const lastWorkspaceNumber = useLastWorkspaceNumber(); + const {onboardingMessages} = useOnboardingMessages(); + + // We use isSmallScreenWidth instead of shouldUseNarrowLayout because navigateAfterOnboarding + // relies on actual device screen width to handle navigation stack differences: on small screens, + // removing OnboardingModalNavigator redirects to HOME, requiring explicit navigation to the last + // accessed report. This behavior is tied to screen size, not responsive layout mode. + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); const mergedAccountConciergeReportID = !onboardingValues?.shouldRedirectToClassicAfterMerge && onboardingValues?.shouldValidate ? conciergeChatReportID : undefined; const autoCreateTrackWorkspace = useCallback( async (firstName: string, lastName: string, onboardingPurposeSelected: OnboardingPurpose) => { const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasPaidGroupAdminPolicy; - const displayName = createDisplayName(currentUserEmail, {firstName, lastName}, formatPhoneNumber); + const displayName = createDisplayName(session?.email ?? '', {firstName, lastName}, formatPhoneNumber); const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace ? createWorkspace({ policyOwnerEmail: undefined, makeMeAdmin: true, - policyName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate, displayName), + policyName: generateDefaultWorkspaceName(session?.email ?? '', lastWorkspaceNumber, translate, displayName), policyID: generatePolicyID(), engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, - currency: localCurrencyCode, + currency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD, file: undefined, shouldAddOnboardingTasks: false, introSelected, activePolicy, - currentUserAccountIDParam: currentUserAccountID, - currentUserEmailParam: currentUserEmail, + currentUserAccountIDParam: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + currentUserEmailParam: session?.email ?? '', shouldAddGuideWelcomeMessage: false, onboardingPurposeSelected, betas, @@ -121,8 +129,8 @@ function useAutoCreateTrackWorkspace() { } }, [ - currentUserEmail, - currentUserAccountID, + session?.email, + session?.accountID, lastWorkspaceNumber, translate, formatPhoneNumber, @@ -130,7 +138,7 @@ function useAutoCreateTrackWorkspace() { onboardingPolicyID, hasPaidGroupAdminPolicy, onboardingAdminsChatReportID, - localCurrencyCode, + currentUserPersonalDetails.localCurrencyCode, introSelected, activePolicy, isSelfTourViewed, diff --git a/src/hooks/useOnboardingWorkspaceCreationState.ts b/src/hooks/useOnboardingWorkspaceCreationState.ts deleted file mode 100644 index cb09fb0b9a29..000000000000 --- a/src/hooks/useOnboardingWorkspaceCreationState.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {hasSeenTourSelector} from '@selectors/Onboarding'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import useActivePolicy from './useActivePolicy'; -import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; -import useHasActiveAdminPolicies from './useHasActiveAdminPolicies'; -import useLastWorkspaceNumber from './useLastWorkspaceNumber'; -import useLocalize from './useLocalize'; -import useOnboardingMessages from './useOnboardingMessages'; -import useOnyx from './useOnyx'; -import usePreferredPolicy from './usePreferredPolicy'; -import useResponsiveLayout from './useResponsiveLayout'; - -/** - * Shared state for the onboarding workspace auto-creation hooks - * (`useAutoCreateSubmitWorkspace`, `useAutoCreateTrackWorkspace`). - * - * Email and accountID come from `ONYXKEYS.SESSION` because session is hydrated - * earlier in onboarding than personal details. - */ -function useOnboardingWorkspaceCreationState() { - const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID); - const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [session] = useOnyx(ONYXKEYS.SESSION); - - const currentUserEmail = session?.email ?? ''; - const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID; - - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const localCurrencyCode = currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD; - - const activePolicy = useActivePolicy(); - const {translate, formatPhoneNumber} = useLocalize(); - const {isRestrictedPolicyCreation} = usePreferredPolicy(); - const hasActiveAdminPolicies = useHasActiveAdminPolicies(); - const {onboardingMessages} = useOnboardingMessages(); - const lastWorkspaceNumber = useLastWorkspaceNumber(); - - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth} = useResponsiveLayout(); - - return { - onboardingPolicyID, - onboardingAdminsChatReportID, - introSelected, - isSelfTourViewed, - betas, - currentUserPersonalDetails, - currentUserEmail, - currentUserAccountID, - localCurrencyCode, - activePolicy, - translate, - formatPhoneNumber, - isRestrictedPolicyCreation, - hasActiveAdminPolicies, - onboardingMessages, - lastWorkspaceNumber, - isSmallScreenWidth, - }; -} - -export default useOnboardingWorkspaceCreationState; diff --git a/src/languages/de.ts b/src/languages/de.ts index ea410d8ef92d..e759fc39d1a4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6988,10 +6988,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc label: 'Steuerung', description: 'Für Organisationen mit erweiterten Anforderungen.', }, - submit2026: { - label: 'Einreichen', - description: 'Für Mitarbeiter, die Ausgaben bei ihrem Arbeitgeber einreichen möchten.', - }, }, description: 'Wähle ein passendes Abo für dich. Eine detaillierte Liste der Funktionen und Preise findest du in unserem', subscriptionLink: 'Hilfeseite zu Plantypen und Preisen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 57a6fbfc9a83..b444c7bd5832 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7000,10 +7000,6 @@ const translations = { label: 'Control', description: 'For organizations with advanced requirements.', }, - submit2026: { - label: 'Submit', - description: 'For employees looking to submit expenses to their employer.', - }, }, description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", subscriptionLink: 'plan types and pricing help page', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0fe1ab8d82a7..d9147fe1457c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6421,10 +6421,6 @@ ${amount} para ${merchant} - ${date}`, label: 'Controlar', description: 'Para organizaciones con requisitos avanzados.', }, - submit2026: { - label: 'Enviar', - description: 'Para empleados que buscan enviar gastos a su empleador.', - }, }, description: 'Elige el plan adecuado para ti. Para ver una lista detallada de funciones y precios, consulta nuestra', subscriptionLink: 'página de ayuda sobre tipos de planes y precios', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b3a2f6e89568..d017c7dad33e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7010,10 +7010,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e label: 'Contrôle', description: 'Pour les organisations ayant des exigences avancées.', }, - submit2026: { - label: 'Soumettre', - description: 'Pour les employés souhaitant soumettre des dépenses à leur employeur.', - }, }, description: 'Choisissez l’offre qui vous convient. Pour une liste détaillée des fonctionnalités et des tarifs, consultez notre', subscriptionLink: "page d'aide sur les types de forfaits et les tarifs", diff --git a/src/languages/it.ts b/src/languages/it.ts index 888fa02f473b..2522cf5f2479 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6973,10 +6973,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, label: 'Controllo', description: 'Per le organizzazioni con requisiti avanzati.', }, - submit2026: { - label: 'Invia', - description: 'Per i dipendenti che desiderano inviare le spese al proprio datore di lavoro.', - }, }, description: 'Scegli il piano più adatto a te. Per un elenco dettagliato di funzionalità e prezzi, consulta la nostra', subscriptionLink: 'pagina di aiuto su tipi di piano e prezzi', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 7d72e906a2a7..9d48671624b6 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6896,10 +6896,6 @@ ${reportName} label: 'コントロール', description: '高度な要件を持つ組織向け。', }, - submit2026: { - label: '提出', - description: '雇用主に経費を提出したい従業員向け。', - }, }, description: '自分に合ったプランをお選びください。機能と料金の詳細な一覧は、こちらのページをご覧ください', subscriptionLink: 'プランの種類と料金のヘルプページ', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 92b3e39d7f5a..fcd20ebc30c5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6952,10 +6952,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, label: 'Beheer', description: 'Voor organisaties met geavanceerde vereisten.', }, - submit2026: { - label: 'Indienen', - description: 'Voor werknemers die onkosten bij hun werkgever willen indienen.', - }, }, description: 'Kies een pakket dat bij je past. Voor een gedetailleerd overzicht van functies en prijzen, bekijk onze', subscriptionLink: 'hulppagina voor abonnementstypen en prijzen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fafa4d486877..91a037b6a951 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6945,10 +6945,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, label: 'Sterowanie', description: 'Dla organizacji z zaawansowanymi wymaganiami.', }, - submit2026: { - label: 'Prześlij', - description: 'Dla pracowników, którzy chcą przesyłać wydatki do pracodawcy.', - }, }, description: 'Wybierz plan odpowiedni dla siebie. Szczegółową listę funkcji i cen znajdziesz w naszej', subscriptionLink: 'strona pomocy dotycząca typów planów i cen', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 6db7a0023f7b..1c929bc939bc 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6951,10 +6951,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, label: 'Controle', description: 'Para organizações com requisitos avançados.', }, - submit2026: { - label: 'Enviar', - description: 'Para funcionários que desejam enviar despesas ao empregador.', - }, }, description: 'Escolha o plano ideal para você. Para ver a lista detalhada de recursos e preços, confira nosso', subscriptionLink: 'página de ajuda sobre tipos de plano e preços', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index cd4e73d686fb..85754bfc6377 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6778,10 +6778,6 @@ ${reportName} label: '控制', description: '适用于具有高级需求的组织。', }, - submit2026: { - label: '提交', - description: '适用于希望向雇主提交费用的员工。', - }, }, description: '选择适合您的方案。要查看详细的功能和价格列表,请访问我们的', subscriptionLink: '方案类型和价格帮助页面', diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f48318b86a78..9fa421f0f89c 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -881,32 +881,6 @@ function isPaidGroupPolicy(policy: OnyxInputOrEntry): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE; } -function isSubmitPolicy(policy: OnyxInputOrEntry): boolean { - return policy?.type === CONST.POLICY.TYPE.SUBMIT; -} - -function isPolicyEditor(policy: OnyxEntry): boolean { - return policy?.role === CONST.POLICY.ROLE.EDITOR; -} - -/** - * Returns true if the user can edit workspace settings — admins on any workspace, or editors on Submit workspaces. - */ -function canEditWorkspaceSettings(policy: OnyxEntry): boolean { - return isPolicyAdmin(policy) || isPolicyEditor(policy); -} - -/** - * Returns true for any group workspace: paid (Team/Corporate) or Submit. - * - * Note: not to be confused with `ReportUtils.isGroupPolicy(policyType: string)`, - * which excludes Submit. Use this helper when Submit workspaces should be treated - * like paid workspaces (e.g. access gating for shared workspace pages). - */ -function isGroupPolicy(policy: OnyxInputOrEntry): boolean { - return isPaidGroupPolicy(policy) || isSubmitPolicy(policy); -} - function getOwnedPaidPolicies(policies: OnyxCollection | null, currentUserAccountID: number | undefined): Policy[] { return Object.values(policies ?? {}).filter((policy): policy is Policy => isPolicyOwner(policy, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) && isPaidGroupPolicy(policy)); } @@ -2190,10 +2164,6 @@ export { isDelayedSubmissionEnabled, getCorrectedAutoReportingFrequency, isPaidGroupPolicy, - isSubmitPolicy, - isPolicyEditor, - canEditWorkspaceSettings, - isGroupPolicy, isPendingDeletePolicy, isPolicyAdmin, isPolicyUser, diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 3c26d554354d..7193ad676243 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -539,7 +539,7 @@ function getSubscriptionPrice( privateSubscriptionType: SubscriptionType | undefined, hasTeam2025Pricing: boolean, ): number { - if (!privateSubscriptionType || !plan || plan === CONST.POLICY.TYPE.SUBMIT) { + if (!privateSubscriptionType || !plan) { return 0; } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index a59721afb0ca..4c8704f37d83 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -1083,55 +1083,28 @@ function inviteMemberToWorkspace(policyID: string, inviterEmail?: string) { } /** - * Add member to the selected private domain workspace based on policyID. - * - * The optimistic merge is intentionally limited to `isLoading: true`. We can't - * tell the policy's real type/role from the joinable-policy payload (it's not - * exposed on `JoinablePolicy`), so writing speculative `type: SUBMIT` / - * `role: EDITOR` would corrupt Team/Corporate policies joined via the same - * private-domain flow. The `isLoading` flag is enough to suppress the brief - * NotFoundPage flash in `WorkspaceInitialPage` / `AccessOrNotFoundWrapper` - * until the backend response hydrates the policy with its actual shape. + * Add member to the selected private domain workspace based on policyID */ function joinAccessiblePolicy(policyID: string) { const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const; - const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; - const optimisticData: Array> = [ + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: memberJoinKey, value: {policyID}, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: {isLoading: true}, - }, - ]; - - const successData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: {isLoading: false}, - }, ]; - const failureData: Array> = [ + const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: memberJoinKey, value: {policyID, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd')}, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: {isLoading: false}, - }, ]; - API.write(WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY, {policyID}, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY, {policyID}, {optimisticData, failureData}); } /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 888e10bc363f..17ed8758a302 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -214,7 +214,7 @@ type BuildPolicyDataOptions = { onboardingPurposeSelected?: OnboardingPurpose; shouldAddGuideWelcomeMessage?: boolean; shouldCreateControlPolicy?: boolean; - type?: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.SUBMIT; + type?: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE; // TODO: Make it required once we complete refactoring the buildPolicyData function to use isSelfTourViewed. Refactor issue: https://github.com/Expensify/App/issues/66424 isSelfTourViewed?: boolean; betas: OnyxEntry; @@ -2290,7 +2290,7 @@ function createDraftInitialWorkspace( makeMeAdmin = false, currency = '', file?: File, - type: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.SUBMIT = CONST.POLICY.TYPE.TEAM, + type: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE = CONST.POLICY.TYPE.TEAM, isAnnualSubscription = false, ) { const {customUnits, outputCurrency} = buildOptimisticDistanceRateCustomUnits(currency); @@ -2367,27 +2367,6 @@ type BuildPolicyDataKeys = | typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD | typeof ONYXKEYS.PERSONAL_DETAILS_LIST; -function getRoleForNewWorkspaceMember(isSubmitWorkspace: boolean, makeMeAdmin: boolean): ValueOf { - if (isSubmitWorkspace) { - return CONST.POLICY.ROLE.EDITOR; - } - return makeMeAdmin ? CONST.POLICY.ROLE.ADMIN : CONST.POLICY.ROLE.USER; -} - -function getApprovalModeForNewWorkspace( - isSubmitWorkspace: boolean, - shouldEnableWorkflowsByDefault: boolean, - engagementChoice?: OnboardingPurpose, -): ValueOf { - if (isSubmitWorkspace) { - return CONST.POLICY.APPROVAL_MODE.ADVANCED; - } - if (shouldEnableWorkflowsByDefault && engagementChoice !== CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) { - return CONST.POLICY.APPROVAL_MODE.BASIC; - } - return CONST.POLICY.APPROVAL_MODE.OPTIONAL; -} - /** * Generates onyx data for creating a new workspace * @@ -2450,9 +2429,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData feature.id === CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED && feature.enabled); + const areDistanceRatesEnabled = !!featuresMap?.find((feature) => feature.id === CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED && feature.enabled); // WARNING: The data below should be kept in sync with the API so we create the policy with the correct configuration. const optimisticData: Array< @@ -2492,7 +2469,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData Categories with the side panel open so - * the #admins room is visible in Concierge Anywhere. - */ -function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, isSmallScreenWidth = false) { - setDisableDismissOnEscape(false); - - if (!policyID) { - Navigation.navigate(ROUTES.HOME); - return; - } - - setOnboardingRHPVariant(CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM); - Navigation.navigate(ROUTES.WORKSPACES_LIST.route); - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); - SidePanelActions.openSidePanel(!isSmallScreenWidth); - }); -} - -function navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyID?: string, isSmallScreenWidth = false) { - Navigation.dismissModal(); - Navigation.setNavigationActionToMicrotaskQueue(() => { - navigateToSubmitWorkspaceAfterOnboarding(policyID, isSmallScreenWidth); - }); -} - -export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue, navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue}; +export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue}; diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 956f91be9169..85b3e67b4bc8 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -10,7 +10,6 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet'; -import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace'; import useAutoCreateTrackWorkspace from '@hooks/useAutoCreateTrackWorkspace'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; @@ -55,7 +54,6 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat const [onboardingPersonalDetailsForm] = useOnyx(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const autoCreateTrackWorkspace = useAutoCreateTrackWorkspace(); - const autoCreateSubmitWorkspace = useAutoCreateSubmitWorkspace(); // When we merge public email with work email, we now want to navigate to the // concierge chat report of the new work email and not the last accessed report. @@ -66,7 +64,6 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat const {inputCallbackRef} = useAutoFocusInput(); const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false); const {isBetaEnabled} = usePermissions(); - const canUseSubmit2026 = isBetaEnabled(CONST.BETAS.SUBMIT_2026); const onboardingStep = useOnboardingStepCounter(SCREENS.ONBOARDING.PERSONAL_DETAILS); const isPrivateDomainAndHasAccessiblePolicies = !account?.isFromPublicDomain && !!account?.hasAccessibleDomainPolicies; @@ -134,16 +131,6 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat clearPersonalDetailsDraft(); setPersonalDetails(firstName, lastName); - if (onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.EMPLOYER && canUseSubmit2026) { - if (isPrivateDomainAndHasAccessiblePolicies && isValidated) { - Navigation.navigate(ROUTES.ONBOARDING_WORKSPACES.getRoute(route.params?.backTo)); - return; - } - updateDisplayName(firstName, lastName, formatPhoneNumber, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - autoCreateSubmitWorkspace(firstName, lastName); - return; - } - if (isPrivateDomainAndHasAccessiblePolicies && (!onboardingPurposeSelected || isVsb || isSmb)) { const nextRoute = isValidated ? ROUTES.ONBOARDING_WORKSPACES : ROUTES.ONBOARDING_PRIVATE_DOMAIN; Navigation.navigate(nextRoute.getRoute(route.params?.backTo)); @@ -170,14 +157,12 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat session?.email, isPrivateDomainAndHasAccessiblePolicies, onboardingPurposeSelected, - canUseSubmit2026, isVsb, isSmb, completeOnboarding, isValidated, route.params?.backTo, autoCreateTrackWorkspace, - autoCreateSubmitWorkspace, ], ); diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index c33ec373f627..2643a641699a 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -9,21 +9,18 @@ import type {MenuItemProps} from '@components/MenuItem'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; -import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace'; import useAutoCreateTrackWorkspace from '@hooks/useAutoCreateTrackWorkspace'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnboardingMessages from '@hooks/useOnboardingMessages'; import useOnboardingStepCounter from '@hooks/useOnboardingStepCounter'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import OnboardingRefManager from '@libs/OnboardingRefManager'; import type {TOnboardingRef} from '@libs/OnboardingRefManager'; -import {isCurrentUserValidated} from '@libs/UserUtils'; import variables from '@styles/variables'; import {completeOnboarding} from '@userActions/Report'; import {setOnboardingErrorMessage, setOnboardingPurposeSelected} from '@userActions/Welcome'; @@ -77,12 +74,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [betas] = useOnyx(ONYXKEYS.BETAS); - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); - const [session] = useOnyx(ONYXKEYS.SESSION); - const {isBetaEnabled} = usePermissions(); - const canUseSubmit2026 = isBetaEnabled(CONST.BETAS.SUBMIT_2026); - const isValidated = isCurrentUserValidated(loginList, session?.email); - const autoCreateSubmitWorkspace = useAutoCreateSubmitWorkspace(); const autoCreateTrackWorkspace = useAutoCreateTrackWorkspace(); const paddingHorizontal = onboardingIsMediumOrLargerScreenWidth ? styles.ph8 : styles.ph5; @@ -111,23 +102,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro return; } - if (choice === CONST.ONBOARDING_CHOICES.EMPLOYER && canUseSubmit2026) { - if (isPrivateDomainAndHasAccessiblePolicies && isValidated) { - Navigation.navigate( - personalDetailsForm?.firstName ? ROUTES.ONBOARDING_WORKSPACES.getRoute(route.params?.backTo) : ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute(route.params?.backTo), - ); - return; - } - - if (personalDetailsForm?.firstName) { - autoCreateSubmitWorkspace(personalDetailsForm.firstName, personalDetailsForm.lastName ?? ''); - return; - } - - Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute(route.params?.backTo)); - return; - } - if (isPrivateDomainAndHasAccessiblePolicies && personalDetailsForm?.firstName) { if (choice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) { autoCreateTrackWorkspace(personalDetailsForm.firstName, personalDetailsForm.lastName ?? '', choice); diff --git a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx index 6d81539a268d..1814b46388d6 100644 --- a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx +++ b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx @@ -9,7 +9,6 @@ import SelectionList from '@components/SelectionList'; import BareUserListItem from '@components/SelectionList/ListItem/BareUserListItem'; import Text from '@components/Text'; import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet'; -import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -20,7 +19,7 @@ import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {navigateAfterOnboardingWithMicrotaskQueue, navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; +import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import {isCurrentUserValidated} from '@libs/UserUtils'; @@ -70,34 +69,20 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING); const isVsb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB; const isSmb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB; - const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); - const canUseSubmit2026 = isBetaEnabled(CONST.BETAS.SUBMIT_2026); - const isEmployerWithSubmit = onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.EMPLOYER && canUseSubmit2026; - const autoCreateSubmitWorkspace = useAutoCreateSubmitWorkspace(); const shouldHideBackButton = onboardingValues?.shouldValidate === false && route.params?.backTo === ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute(); const onboardingStep = useOnboardingStepCounter(SCREENS.ONBOARDING.WORKSPACES); const handleJoinWorkspace = (policy: JoinablePolicy) => { - // Only mirror the EMPLOYER ("Get paid back by my employer") + Submit-2026 onboarding flow - // when the user actually picked EMPLOYER, or when no Purpose was selected (private-domain - // users who reach this screen without going through the Purpose step). Users on the Submit - // beta who picked a different Purpose (e.g. MANAGE_TEAM) must not be re-routed through - // the Submit flow. - const shouldUseSubmitFlow = canUseSubmit2026 && (!onboardingPurposeSelected || onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.EMPLOYER); - if (policy.automaticJoiningEnabled) { joinAccessiblePolicy(policy.policyID); } else { askToJoinPolicy(policy.policyID); } - - const engagementChoice = shouldUseSubmitFlow ? CONST.ONBOARDING_CHOICES.EMPLOYER : CONST.ONBOARDING_CHOICES.LOOKING_AROUND; completeOnboarding({ - engagementChoice, - onboardingMessage: onboardingMessages[engagementChoice], + engagementChoice: CONST.ONBOARDING_CHOICES.LOOKING_AROUND, + onboardingMessage: onboardingMessages[CONST.ONBOARDING_CHOICES.LOOKING_AROUND], firstName: onboardingPersonalDetails?.firstName ?? '', lastName: onboardingPersonalDetails?.lastName ?? '', - onboardingPolicyID: shouldUseSubmitFlow && policy.automaticJoiningEnabled ? policy.policyID : undefined, companySize: onboardingCompanySize, introSelected, isSelfTourViewed, @@ -106,11 +91,6 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding setOnboardingAdminsChatReportID(); setOnboardingPolicyID(policy.policyID); - if (shouldUseSubmitFlow && policy.automaticJoiningEnabled) { - navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policy.policyID, isSmallScreenWidth); - return; - } - navigateAfterOnboardingWithMicrotaskQueue( isSmallScreenWidth, isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), @@ -166,11 +146,6 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding }); const skipJoiningWorkspaces = () => { - if (isEmployerWithSubmit) { - autoCreateSubmitWorkspace(onboardingPersonalDetails?.firstName ?? '', onboardingPersonalDetails?.lastName ?? ''); - return; - } - if (isVsb) { Navigation.navigate(ROUTES.ONBOARDING_ACCOUNTING.getRoute(route.params?.backTo)); return; diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 636662d1062d..87fab021f830 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -14,7 +14,7 @@ import {openWorkspace} from '@libs/actions/Policy/Policy'; import {isValidMoneyRequestType} from '@libs/IOUUtils'; import goBackFromWorkspaceSettingPages from '@libs/Navigation/helpers/goBackFromWorkspaceSettingPages'; import Navigation from '@libs/Navigation/Navigation'; -import {canEditWorkspaceSettings, canSendInvoice, isControlPolicy, isGroupPolicy, isPolicyAccessible, isPolicyFeatureEnabled as isPolicyFeatureEnabledUtil} from '@libs/PolicyUtils'; +import {canSendInvoice, isControlPolicy, isPaidGroupPolicy, isPolicyAccessible, isPolicyAdmin, isPolicyFeatureEnabled as isPolicyFeatureEnabledUtil} from '@libs/PolicyUtils'; import {canCreateRequest} from '@libs/ReportUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -30,9 +30,9 @@ import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const ACCESS_VARIANTS = { - [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => isGroupPolicy(policy), + [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => isPaidGroupPolicy(policy), [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => isControlPolicy(policy), - [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry) => canEditWorkspaceSettings(policy), + [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => isPolicyAdmin(policy, login), [CONST.IOU.ACCESS_VARIANTS.CREATE]: ( policy: OnyxEntry, login: string, @@ -164,8 +164,7 @@ function AccessOrNotFoundWrapper({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPolicyIDInRoute, policyID]); - const isPolicyEmpty = !Object.entries(policy ?? {}).length || !policy?.id; - const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && (isLoadingReportData !== false || !!policy?.isLoading) && isPolicyEmpty; + const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); const isFeatureEnabled = featureName ? isPolicyFeatureEnabledUtil(policy, featureName) : true; diff --git a/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx b/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx index cb97854f9c0b..d5de898ef1c1 100644 --- a/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx @@ -55,17 +55,7 @@ function DynamicWorkspaceOverviewPlanTypePage({policy}: WithPolicyProps) { }, [policy?.type]); const workspacePlanTypes = Object.values(CONST.POLICY.TYPE) - .filter((type) => { - if (type === CONST.POLICY.TYPE.PERSONAL) { - return false; - } - // Guard: don't leak the SUBMIT plan type into the plan-type list for paid workspaces. - // Submit-specific plan-type UX (exposing SUBMIT for Submit policies) ships in #87263. - if (type === CONST.POLICY.TYPE.SUBMIT) { - return false; - } - return true; - }) + .filter((type) => type !== CONST.POLICY.TYPE.PERSONAL) .map((policyType) => ({ value: policyType, text: translate(`workspace.planTypePage.planTypes.${policyType as PersonalPolicyTypeExcludedProps}.label`), @@ -91,14 +81,6 @@ function DynamicWorkspaceOverviewPlanTypePage({policy}: WithPolicyProps) { ) : null; const handleUpdatePlan = () => { - // Submit policies don't expose SUBMIT in the option list, but the editor can - // still pick Team/Corporate. Route any selection from a Submit policy to the - // upgrade screen — the polished Submit-specific upgrade UX ships in #87263. - if (policyID && policy?.type === CONST.POLICY.TYPE.SUBMIT && (currentPlan === CONST.POLICY.TYPE.TEAM || currentPlan === CONST.POLICY.TYPE.CORPORATE)) { - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID)); - return; - } - if (policyID && policy?.type === CONST.POLICY.TYPE.TEAM && currentPlan === CONST.POLICY.TYPE.CORPORATE) { Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID)); return; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 19d3961f93bf..ad9bc4901466 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -14,6 +14,7 @@ import ScrollView from '@components/ScrollView'; import useCardFeedErrors from '@hooks/useCardFeedErrors'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -32,14 +33,14 @@ import {clearErrors, openPolicyInitialPage, removeWorkspace} from '@libs/actions import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import { - canEditWorkspaceSettings, canPolicyAccessFeature, shouldShowPolicy as checkIfShouldShowPolicy, goBackFromInvalidPolicy, hasAccountingFeatureConnection, hasPolicyCategoriesError, - isGroupPolicy, + isPaidGroupPolicy, isPendingDeletePolicy, + isPolicyAdmin, isTimeTrackingEnabled, shouldShowEmployeeListError, shouldShowSyncError, @@ -94,7 +95,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const {translate} = useLocalize(); const {convertToDisplayString} = useCurrencyListActions(); const {isBetaEnabled} = usePermissions(); - + const {login} = useCurrentUserPersonalDetails(); const isFocused = useIsFocused(); const activeRoute = useNavigationState((state) => findFocusedRoute(state)?.name); const waitForNavigate = useWaitForNavigation(); @@ -133,8 +134,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const policyName = policy?.name ?? ''; const hasPolicyCreationError = policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors); - const shouldShowProtectedItems = canEditWorkspaceSettings(policy); - + const shouldShowProtectedItems = isPolicyAdmin(policy, login); const accountingConnectionNames = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES; const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy), accountingConnectionNames); const hasMembersError = shouldShowEmployeeListError(policy); @@ -184,9 +184,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const shouldShowPolicy = checkIfShouldShowPolicy(policy, true, currentUserLogin); const isPendingDelete = isPendingDeletePolicy(policy); const prevIsPendingDelete = isPendingDeletePolicy(prevPolicy); - // While the policy is being fetched (e.g., right after joinAccessiblePolicy), the role is not yet populated, - // so checkIfShouldShowPolicy returns false. Suppress NotFound during this loading window. - const shouldShowNotFoundPage = !shouldShowPolicy && !policy?.isLoading && (!isPendingDelete || prevIsPendingDelete); + + const shouldShowNotFoundPage = !shouldShowPolicy && (!isPendingDelete || prevIsPendingDelete); const fetchPolicyData = () => { if (policyDraft?.id || !isFocused) { return; @@ -228,7 +227,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac }, ]; - if (isGroupPolicy(policy) && shouldShowProtectedItems) { + if (isPaidGroupPolicy(policy) && shouldShowProtectedItems) { workspaceMenuItems.push({ translationKey: 'common.reports', icon: expensifyIcons.Document, diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 2d13f4524d63..8ef72782aad5 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -62,13 +62,13 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils'; import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; import { - canEditWorkspaceSettings, getConnectionExporters, getMemberAccountIDsForWorkspace, isControlPolicy, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy, + isPolicyAdmin as isPolicyAdminUtils, isPolicyApprover, } from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; @@ -155,7 +155,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const isPolicyAdmin = canEditWorkspaceSettings(policy); + const isPolicyAdmin = isPolicyAdminUtils(policy); const isLoading = useMemo( () => !isOfflineAndNoMemberDataAvailable && (!isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), [isOfflineAndNoMemberDataAvailable, personalDetails, policy?.employeeList], diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 4f8d0cec2cc7..445882b87b8b 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -53,7 +53,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import { - canEditWorkspaceSettings, getConnectionExporters, getUserFriendlyWorkspaceType, goBackFromInvalidPolicy, @@ -110,7 +109,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const isBankAccountVerified = !!settings?.paymentBankAccountID; const shouldBlockCurrencyChange = useShouldBlockCurrencyChange(policyID); - const isPolicyAdmin = canEditWorkspaceSettings(policy); + const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); const outputCurrency = policy?.outputCurrency ?? ''; const currencySymbol = getCurrencySymbol(outputCurrency) ?? ''; const formattedCurrency = !isEmptyObject(policy) ? `${outputCurrency} - ${currencySymbol}` : ''; @@ -178,7 +177,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const policyName = policy?.name ?? ''; const policyDescription = policy?.description ?? translate('workspace.common.defaultDescription'); const policyCurrency = policy?.outputCurrency ?? ''; - const readOnly = !canEditWorkspaceSettings(policy); + const readOnly = !isPolicyAdminPolicyUtils(policy); const currencyReadOnly = readOnly || isBankAccountVerified; const isOwner = isPolicyOwner(policy, currentUserPersonalDetails.accountID); const shouldShowAddress = !readOnly || !!formattedAddress; diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 1d90f6414c89..a2e1c8846792 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -19,7 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {openWorkspaceView} from '@libs/actions/BankAccounts'; import goBackFromWorkspaceSettingPages from '@libs/Navigation/helpers/goBackFromWorkspaceSettingPages'; import Navigation from '@libs/Navigation/Navigation'; -import {canEditWorkspaceSettings, isPendingDeletePolicy, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils'; +import {isPendingDeletePolicy, isPolicyAdmin, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -172,7 +172,7 @@ function WorkspacePageWithSections({ } // We check isPendingDelete and prevIsPendingDelete to prevent the NotFound view from showing right after we delete the workspace - return (!isEmptyObject(policy) && !canEditWorkspaceSettings(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !(isPendingDelete && !prevIsPendingDelete)); + return (!isEmptyObject(policy) && !isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !(isPendingDelete && !prevIsPendingDelete)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [policy, shouldShowNonAdmin, shouldShowPolicy]); diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 576f4af16821..8661e2e0a8b4 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -15,7 +15,7 @@ import {updateXeroMappings} from '@libs/actions/connections/Xero'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {canEditWorkspaceSettings, canModifyPlan, getDefaultApprover, getPerDiemCustomUnit, isControlPolicy} from '@libs/PolicyUtils'; +import {canModifyPlan, getDefaultApprover, getPerDiemCustomUnit, isControlPolicy} from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import {enablePerDiem} from '@userActions/Policy/PerDiem'; import CONST from '@src/CONST'; @@ -231,10 +231,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { }, [isUpgraded, canPerformUpgrade, confirmUpgrade]), ); - // Gate the page to users who can edit workspace settings (admins on any policy, - // or editors on Submit policies). `canPerformUpgrade` (strict admin) still controls - // whether the upgrade button is active, so editors see the intro but can't upgrade. - if (!canEditWorkspaceSettings(policy)) { + if (!canPerformUpgrade) { return ; } @@ -267,7 +264,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { policyID={policyID} feature={feature} onUpgrade={onUpgradeToCorporate} - buttonDisabled={isOffline || !canPerformUpgrade} + buttonDisabled={isOffline} loading={policy?.isPendingUpgrade} backTo={route.params.backTo} /> diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index 72e265ee83dd..17cbe5df9fc7 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -14,7 +14,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {canEditWorkspaceSettings, getCorrectedAutoReportingFrequency, goBackFromInvalidPolicy, isGroupPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils'; +import {getCorrectedAutoReportingFrequency, goBackFromInvalidPolicy, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; @@ -118,7 +118,7 @@ function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoRepor diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 32e450265fe0..c45a8debd9f3 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isGroupPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; @@ -94,7 +94,7 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy, route}: WorkspaceAutoR diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 40fb435a3dc8..56618ac811b5 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -49,11 +49,10 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import {getPaymentMethodDescription} from '@libs/PaymentUtils'; import {getDisplayNameOrDefault, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import { - canEditWorkspaceSettings, getCorrectedAutoReportingFrequency, hasDynamicExternalWorkflow, isControlPolicy, - isGroupPolicy as isGroupPolicyUtil, + isPaidGroupPolicy as isPaidGroupPolicyUtil, isPolicyAdmin as isPolicyAdminUtil, } from '@libs/PolicyUtils'; import {hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; @@ -631,7 +630,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { ); - const isGroupPolicy = isGroupPolicyUtil(policy); + const isPaidGroupPolicy = isPaidGroupPolicyUtil(policy); const isLoading = !!(policy?.isLoading && policy?.reimbursementChoice === undefined); return ( @@ -644,7 +643,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { icon={illustrations.Workflows} route={route} shouldShowOfflineIndicatorInWideScreen - shouldShowNotFoundPage={!isGroupPolicy || !canEditWorkspaceSettings(policy)} + shouldShowNotFoundPage={!isPaidGroupPolicy || !isPolicyAdmin} isLoading={isLoading} shouldShowLoading={isLoading} shouldUseScrollView diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx index f7822beb595f..5e14701cac06 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx @@ -23,7 +23,7 @@ import {convertToBackendAmount, convertToFrontendAmountAsString} from '@libs/Cur import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -65,7 +65,7 @@ function WorkspaceWorkflowsApprovalsApprovalLimitPage({policy, isLoadingReportDa const selectedApproverPersonalDetails = selectedApproverEmail ? personalDetailsByEmail?.[selectedApproverEmail] : undefined; const selectedApproverDisplayName = selectedApproverEmail ? Str.removeSMSDomain(selectedApproverPersonalDetails?.displayName ?? selectedApproverEmail) : ''; - const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy); + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy); const approverDisplayName = currentApprover ? Str.removeSMSDomain(currentApprover.displayName) : ''; const isApproverSelected = isEditFlow ? approverDisplayName.length > 0 : true; diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx index 2c81ebc003c4..15da8a38321d 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -36,7 +36,7 @@ function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = tr const [addExpenseApprovalsTaskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${addExpenseApprovalsTaskReportID}`); const formRef = useRef(null); - const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy); + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy); const createApprovalWorkflow = useCallback(() => { if (!approvalWorkflow) { diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx index d43a130d3aa8..6ada3231860a 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx @@ -18,7 +18,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import {convertPolicyEmployeesToApprovalWorkflows, mergeWorkflowMembersWithAvailableMembers} from '@libs/WorkflowUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -102,7 +102,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true const {currentApprovalWorkflow, defaultWorkflowMembers, usedApproverEmails} = getApprovalWorkflowData(); - const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy) || !currentApprovalWorkflow; + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy) || !currentApprovalWorkflow; // Set the initial approval workflow when the page is loaded useEffect(() => { diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx index dcb9a4639f43..cc9f680177a7 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx @@ -15,7 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; -import {canEditWorkspaceSettings, getDefaultApprover, getMemberAccountIDsForWorkspace, isPendingDeletePolicy} from '@libs/PolicyUtils'; +import {getDefaultApprover, getMemberAccountIDsForWorkspace, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import MemberRightIcon from '@pages/workspace/MemberRightIcon'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -41,7 +41,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat const isLoadingApprovalWorkflow = isLoadingOnyxValue(approvalWorkflowResults); const [selectedMembers, setSelectedMembers] = useState([]); - const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy); + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy); const isInitialCreationFlow = approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && approvalWorkflow?.isInitialFlow; const shouldShowListEmptyContent = !isLoadingApprovalWorkflow && approvalWorkflow?.availableMembers.length === 0; const firstApprover = approvalWorkflow?.originalApprovers?.[0]?.email ?? ''; diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index eb859463f640..fdb480dcea5b 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -1115,43 +1115,6 @@ describe('actions/Policy', () => { }); }); - it('creates a Submit workspace with ADVANCED approval mode and correct feature flags', async () => { - const policyID = Policy.generatePolicyID(); - Policy.createWorkspace({ - policyOwnerEmail: ESH_EMAIL, - makeMeAdmin: true, - policyName: WORKSPACE_NAME, - policyID, - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - introSelected: {choice: CONST.ONBOARDING_CHOICES.EMPLOYER}, - currentUserAccountIDParam: ESH_ACCOUNT_ID, - currentUserEmailParam: ESH_EMAIL, - isSelfTourViewed: false, - betas: undefined, - hasActiveAdminPolicies: false, - activePolicy: undefined, - type: CONST.POLICY.TYPE.SUBMIT, - }); - await waitForBatchedUpdates(); - - await TestHelper.getOnyxData({ - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - waitForCollectionCallback: false, - callback: (policy) => { - expect(policy?.type).toBe(CONST.POLICY.TYPE.SUBMIT); - expect(policy?.role).toBe(CONST.POLICY.ROLE.EDITOR); - expect(policy?.approvalMode).toBe(CONST.POLICY.APPROVAL_MODE.ADVANCED); - expect(policy?.areWorkflowsEnabled).toBe(true); - expect(policy?.areTagsEnabled).toBe(true); - expect(policy?.areDistanceRatesEnabled).toBe(true); - expect(policy?.areCompanyCardsEnabled).toBe(false); - expect(policy?.harvesting?.enabled).toBe(false); - expect(policy?.autoReportingFrequency).toBe(CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE); - expect(policy?.employeeList?.[ESH_EMAIL]?.role).toBe(CONST.POLICY.ROLE.EDITOR); - }, - }); - }); - it('should pass areDistanceRatesEnabled as true when creating workspace with distance rates feature enabled', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); diff --git a/tests/ui/OnboardingPurpose.tsx b/tests/ui/OnboardingPurpose.tsx index 5a487f15f185..1b48f6e54cce 100644 --- a/tests/ui/OnboardingPurpose.tsx +++ b/tests/ui/OnboardingPurpose.tsx @@ -13,7 +13,6 @@ import Navigation from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; import OnboardingPurpose from '@pages/OnboardingPurpose'; -import {createWorkspace} from '@userActions/Policy/Policy'; import {completeOnboarding} from '@userActions/Report'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; @@ -25,7 +24,6 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; const mockCompleteOnboarding = jest.mocked(completeOnboarding); -const mockCreateWorkspace = jest.mocked(createWorkspace); jest.mock('@userActions/Report', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -37,19 +35,6 @@ jest.mock('@userActions/Report', () => { }; }); -jest.mock('@userActions/Policy/Policy', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@userActions/Policy/Policy'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...actual, - createWorkspace: jest.fn().mockReturnValue({ - policyID: 'test-policy-id', - adminsChatReportID: 'test-admins-report-id', - }), - }; -}); - TestHelper.setupGlobalFetchMock(); // Helper to translate onboarding purpose keys that use dynamic CONST values @@ -153,132 +138,6 @@ describe('OnboardingPurpose Page', () => { await waitForBatchedUpdatesWithAct(); }); - it('should navigate to personal details page when user selects EMPLOYER with Submit2026 beta and is from public domain', async () => { - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.ACCOUNT, { - isFromPublicDomain: true, - hasAccessibleDomainPolicies: false, - }); - await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - }); - - const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - const user = userEvent.setup(); - const employerLabel = translatePurpose(CONST.ONBOARDING_CHOICES.EMPLOYER); - const employerOption = screen.getByLabelText(employerLabel); - await user.press(employerOption); - - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute('')); - }); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('should navigate to workspaces page when user selects EMPLOYER with Submit2026 beta and is from private domain with name set', async () => { - const testEmail = 'test@user.com'; - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.ACCOUNT, { - isFromPublicDomain: false, - hasAccessibleDomainPolicies: true, - }); - await Onyx.merge(ONYXKEYS.LOGIN_LIST, { - [testEmail]: { - partnerName: 'expensify.com', - partnerUserID: testEmail, - validatedDate: 'fake-validatedDate', - }, - }); - await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, { - firstName: 'Test', - lastName: 'User', - }); - }); - - const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - const user = userEvent.setup(); - const employerLabel = translatePurpose(CONST.ONBOARDING_CHOICES.EMPLOYER); - const employerOption = screen.getByLabelText(employerLabel); - await user.press(employerOption); - - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_WORKSPACES.getRoute('')); - }); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('should create a Submit workspace from Purpose when EMPLOYER is selected and personal details already exist', async () => { - jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {}); - jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback()); - - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.ACCOUNT, { - isFromPublicDomain: true, - hasAccessibleDomainPolicies: false, - }); - await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, { - firstName: 'Test', - lastName: 'User', - }); - }); - - const onyxSetSpy = jest.spyOn(Onyx, 'set'); - onyxSetSpy.mockClear(); - - const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - const user = userEvent.setup(); - const employerLabel = translatePurpose(CONST.ONBOARDING_CHOICES.EMPLOYER); - const employerOption = screen.getByLabelText(employerLabel); - await user.press(employerOption); - - await waitFor(() => { - expect(mockCreateWorkspace).toHaveBeenCalledWith( - expect.objectContaining({ - type: CONST.POLICY.TYPE.SUBMIT, - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - }), - ); - }); - - await waitFor(() => { - expect(mockCompleteOnboarding).toHaveBeenCalledWith( - expect.objectContaining({ - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - onboardingPolicyID: 'test-policy-id', - }), - ); - }); - - await waitFor(() => { - expect(onyxSetSpy).toHaveBeenCalledWith(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM); - expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('test-policy-id')); - }); - - onyxSetSpy.mockRestore(); - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - it('should call completeOnboarding with introSelected when user is from private domain and selects a direct-complete choice', async () => { await TestHelper.signInWithTestUser(); diff --git a/tests/ui/PersonalDetailsOnboarding.tsx b/tests/ui/PersonalDetailsOnboarding.tsx index d7a7dde9d632..d165bcc96aaa 100644 --- a/tests/ui/PersonalDetailsOnboarding.tsx +++ b/tests/ui/PersonalDetailsOnboarding.tsx @@ -14,8 +14,6 @@ import Navigation from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {OnboardingModalNavigatorParamList} from '@navigation/types'; import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; -import {createWorkspace} from '@userActions/Policy/Policy'; -import {completeOnboarding} from '@userActions/Report'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -24,32 +22,6 @@ import SCREENS from '@src/SCREENS'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const mockCreateWorkspace = jest.mocked(createWorkspace); -const mockCompleteOnboarding = jest.mocked(completeOnboarding); - -jest.mock('@userActions/Report', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@userActions/Report'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...actual, - completeOnboarding: jest.fn(), - }; -}); - -jest.mock('@userActions/Policy/Policy', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@userActions/Policy/Policy'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...actual, - createWorkspace: jest.fn().mockReturnValue({ - policyID: 'test-policy-id', - adminsChatReportID: 'test-admins-report-id', - }), - }; -}); - TestHelper.setupGlobalFetchMock(); const Stack = createPlatformStackNavigator(); @@ -171,84 +143,4 @@ describe('OnboardingPersonalDetails Page', () => { unmount(); await waitForBatchedUpdatesWithAct(); }); - - it('should navigate to Onboarding workspaces page when submitting form with EMPLOYER + Submit2026 beta and validated private domain', async () => { - const testEmail = 'test@user.com'; - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.ACCOUNT, { - isFromPublicDomain: false, - hasAccessibleDomainPolicies: true, - }); - await Onyx.merge(ONYXKEYS.LOGIN_LIST, { - [testEmail]: { - partnerName: 'expensify.com', - partnerUserID: testEmail, - validatedDate: 'fake-validatedDate', - }, - }); - await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER); - await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - }); - - const {unmount} = renderOnboardingPersonalDetailsPage(SCREENS.ONBOARDING.PERSONAL_DETAILS, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - fireEvent.press(screen.getByText(TestHelper.translateLocal('common.continue'))); - - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_WORKSPACES.getRoute()); - }); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('should create a Submit workspace when submitting form with EMPLOYER + Submit2026 beta and public domain', async () => { - jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {}); - jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback()); - - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.ACCOUNT, { - isFromPublicDomain: true, - hasAccessibleDomainPolicies: false, - }); - await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER); - await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - }); - - const {unmount} = renderOnboardingPersonalDetailsPage(SCREENS.ONBOARDING.PERSONAL_DETAILS, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - fireEvent.press(screen.getByText(TestHelper.translateLocal('common.continue'))); - - await waitFor(() => { - expect(mockCreateWorkspace).toHaveBeenCalledWith( - expect.objectContaining({ - type: CONST.POLICY.TYPE.SUBMIT, - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - }), - ); - }); - - await waitFor(() => { - expect(mockCompleteOnboarding).toHaveBeenCalledWith( - expect.objectContaining({ - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - }), - ); - }); - - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('test-policy-id')); - }); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); }); diff --git a/tests/ui/WorkspaceOnboarding.tsx b/tests/ui/WorkspaceOnboarding.tsx index d2307caf85cd..134697814764 100644 --- a/tests/ui/WorkspaceOnboarding.tsx +++ b/tests/ui/WorkspaceOnboarding.tsx @@ -13,54 +13,13 @@ import Navigation from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; import OnboardingWorkspaces from '@pages/OnboardingWorkspaces'; -import {joinAccessiblePolicy} from '@userActions/Policy/Member'; -import {createWorkspace} from '@userActions/Policy/Policy'; -import {completeOnboarding} from '@userActions/Report'; import CONST from '@src/CONST'; -import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -const mockCreateWorkspace = jest.mocked(createWorkspace); -const mockCompleteOnboarding = jest.mocked(completeOnboarding); -const mockJoinAccessiblePolicy = jest.mocked(joinAccessiblePolicy); - -jest.mock('@userActions/Report', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@userActions/Report'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...actual, - completeOnboarding: jest.fn(), - }; -}); - -jest.mock('@userActions/Policy/Policy', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@userActions/Policy/Policy'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...actual, - createWorkspace: jest.fn().mockReturnValue({ - policyID: 'test-policy-id', - adminsChatReportID: 'test-admins-report-id', - }), - }; -}); - -jest.mock('@userActions/Policy/Member', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = jest.requireActual('@userActions/Policy/Member'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...actual, - joinAccessiblePolicy: jest.fn(), - }; -}); - TestHelper.setupGlobalFetchMock(); const Stack = createPlatformStackNavigator(); @@ -90,7 +49,6 @@ describe('OnboardingWorkspaces Page', () => { Onyx.init({ keys: ONYXKEYS, }); - return IntlStore.load(CONST.LOCALES.EN); }); beforeEach(() => { @@ -215,118 +173,4 @@ describe('OnboardingWorkspaces Page', () => { unmount(); await waitForBatchedUpdatesWithAct(); }); - - it('should create a Submit workspace when skip is pressed with EMPLOYER purpose and Submit2026 beta', async () => { - jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {}); - jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback()); - - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, { - hasCompletedGuidedSetupFlow: false, - }); - await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER); - await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - }); - - const {unmount} = renderOnboardingWorkspacesPage(SCREENS.ONBOARDING.WORKSPACES, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - const skipButton = screen.getByTestId('onboardingWorkSpaceSkipButton'); - - const mockEvent = { - nativeEvent: {}, - type: 'press', - target: skipButton, - currentTarget: skipButton, - }; - - fireEvent.press(skipButton, mockEvent); - - await waitFor(() => { - expect(mockCreateWorkspace).toHaveBeenCalledWith( - expect.objectContaining({ - type: CONST.POLICY.TYPE.SUBMIT, - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - }), - ); - }); - - await waitFor(() => { - expect(mockCompleteOnboarding).toHaveBeenCalledWith( - expect.objectContaining({ - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - }), - ); - }); - - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('test-policy-id')); - }); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('should complete onboarding with the joined Submit workspace policyID and open Categories in the admins room', async () => { - jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {}); - jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback()); - - await TestHelper.signInWithTestUser(); - - await act(async () => { - await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, { - hasCompletedGuidedSetupFlow: false, - }); - await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER); - await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]); - await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, { - firstName: 'Test', - lastName: 'User', - }); - await Onyx.set(ONYXKEYS.JOINABLE_POLICIES, { - submitPolicyID: { - policyID: 'submit-policy-id', - policyName: 'Submit Workspace', - policyOwner: 'owner@test.com', - employeeCount: 4, - hasPendingAccess: false, - automaticJoiningEnabled: true, - }, - }); - }); - - const onyxSetSpy = jest.spyOn(Onyx, 'set'); - onyxSetSpy.mockClear(); - - const {unmount} = renderOnboardingWorkspacesPage(SCREENS.ONBOARDING.WORKSPACES, {backTo: ''}); - - await waitForBatchedUpdatesWithAct(); - - fireEvent.press(screen.getByText(TestHelper.translateLocal('workspace.workspaceList.joinNow'))); - - await waitFor(() => { - expect(mockJoinAccessiblePolicy).toHaveBeenCalledWith('submit-policy-id'); - }); - - await waitFor(() => { - expect(mockCompleteOnboarding).toHaveBeenCalledWith( - expect.objectContaining({ - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - onboardingPolicyID: 'submit-policy-id', - }), - ); - }); - - await waitFor(() => { - expect(onyxSetSpy).toHaveBeenCalledWith(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM); - expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('submit-policy-id')); - }); - - onyxSetSpy.mockRestore(); - unmount(); - await waitForBatchedUpdatesWithAct(); - }); }); diff --git a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts deleted file mode 100644 index a01b7002d801..000000000000 --- a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -import {renderHook} from '@testing-library/react-native'; -import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useHasActiveAdminPolicies from '@hooks/useHasActiveAdminPolicies'; -import useLocalize from '@hooks/useLocalize'; -import useOnboardingMessages from '@hooks/useOnboardingMessages'; -import useOnyx from '@hooks/useOnyx'; -import usePreferredPolicy from '@hooks/usePreferredPolicy'; -import * as navigateAfterOnboarding from '@libs/navigateAfterOnboarding'; -import * as Policy from '@userActions/Policy/Policy'; -import * as Report from '@userActions/Report'; -import * as Welcome from '@userActions/Welcome'; -import CONST from '@src/CONST'; - -jest.mock('@hooks/useOnyx', () => { - return {__esModule: true, default: jest.fn(() => [undefined])}; -}); - -jest.mock('@hooks/useCurrentUserPersonalDetails'); -jest.mock('@hooks/useHasActiveAdminPolicies'); -jest.mock('@hooks/useLocalize'); -jest.mock('@hooks/usePreferredPolicy'); -jest.mock('@hooks/useOnboardingMessages'); - -const mockTranslate = jest.fn((key: string) => key); -const mockFormatPhoneNumber = jest.fn((phone: string) => phone); - -const MOCK_SESSION = { - accountID: 12345, - email: 'test@expensify.com', -}; - -const MOCK_POLICY_ID = 'mock-policy-id'; -const MOCK_ADMINS_CHAT_REPORT_ID = 'mock-admins-chat-report-id'; -const MOCK_ONBOARDING_MESSAGE = {message: 'Welcome!', video: undefined, tasks: []}; - -function setupDefaultMocks() { - const mockUseOnyx = useOnyx as jest.Mock; - mockUseOnyx.mockImplementation((key: string) => { - if (key === 'session') { - return [MOCK_SESSION]; - } - if (key === 'betas') { - return [[]]; - } - if (key.startsWith('policy_')) { - return [false]; - } - return [undefined]; - }); - - (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({ - accountID: MOCK_SESSION.accountID, - login: MOCK_SESSION.email, - localCurrencyCode: 'USD', - }); - - (useLocalize as jest.Mock).mockReturnValue({ - translate: mockTranslate, - formatPhoneNumber: mockFormatPhoneNumber, - }); - - (usePreferredPolicy as jest.Mock).mockReturnValue({ - isRestrictedToPreferredPolicy: false, - preferredPolicyID: undefined, - isRestrictedPolicyCreation: false, - }); - - (useHasActiveAdminPolicies as jest.Mock).mockReturnValue(false); - - (useOnboardingMessages as jest.Mock).mockReturnValue({ - onboardingMessages: { - [CONST.ONBOARDING_CHOICES.EMPLOYER]: MOCK_ONBOARDING_MESSAGE, - }, - }); -} - -describe('useAutoCreateSubmitWorkspace', () => { - const createWorkspaceSpy = jest.spyOn(Policy, 'createWorkspace').mockReturnValue({ - policyID: MOCK_POLICY_ID, - adminsChatReportID: MOCK_ADMINS_CHAT_REPORT_ID, - } as ReturnType); - const completeOnboardingSpy = jest.spyOn(Report, 'completeOnboarding').mockImplementation(jest.fn()); - const setOnboardingAdminsChatReportIDSpy = jest.spyOn(Welcome, 'setOnboardingAdminsChatReportID').mockImplementation(jest.fn()); - const setOnboardingPolicyIDSpy = jest.spyOn(Welcome, 'setOnboardingPolicyID').mockImplementation(jest.fn()); - const navigateSpy = jest.spyOn(navigateAfterOnboarding, 'navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue').mockImplementation(jest.fn()); - - beforeEach(() => { - jest.clearAllMocks(); - setupDefaultMocks(); - }); - - it('creates a Submit workspace with the correct parameters for a new EMPLOYER user', () => { - // Given a new user going through onboarding with no existing workspace (onboardingPolicyID is undefined, - // hasEditableGroupPolicy is false, and policy creation is not restricted) - - // When the autoCreateSubmitWorkspace function is invoked during onboarding - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('John', 'Doe'); - - // Then a Submit workspace should be created because EMPLOYER users without an existing - // workspace need one auto-created to land on after onboarding - expect(createWorkspaceSpy).toHaveBeenCalledTimes(1); - expect(createWorkspaceSpy).toHaveBeenCalledWith( - expect.objectContaining({ - makeMeAdmin: true, - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - currency: 'USD', - shouldAddOnboardingTasks: false, - shouldAddGuideWelcomeMessage: false, - type: CONST.POLICY.TYPE.SUBMIT, - currentUserAccountIDParam: MOCK_SESSION.accountID, - currentUserEmailParam: MOCK_SESSION.email, - }), - ); - }); - - it('completes onboarding with the newly created workspace and admins chat IDs', () => { - // Given a new user with no pre-existing onboarding workspace - - // When the hook creates a workspace and finishes the onboarding flow - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('John', 'Doe'); - - // Then completeOnboarding should receive the IDs returned by createWorkspace so the backend - // can associate the guided setup data with the correct workspace and admins chat - expect(completeOnboardingSpy).toHaveBeenCalledTimes(1); - expect(completeOnboardingSpy).toHaveBeenCalledWith( - expect.objectContaining({ - engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, - onboardingMessage: MOCK_ONBOARDING_MESSAGE, - firstName: 'John', - lastName: 'Doe', - adminsChatReportID: MOCK_ADMINS_CHAT_REPORT_ID, - onboardingPolicyID: MOCK_POLICY_ID, - }), - ); - }); - - it('clears onboarding state after the flow completes', () => { - // Given a user completing the onboarding flow - - // When autoCreateSubmitWorkspace finishes - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('John', 'Doe'); - - // Then the transient onboarding Onyx keys should be cleared so the onboarding - // flow is not re-triggered on subsequent app launches - expect(setOnboardingAdminsChatReportIDSpy).toHaveBeenCalledTimes(1); - expect(setOnboardingPolicyIDSpy).toHaveBeenCalledTimes(1); - }); - - it('navigates to the submit workspace page after completing onboarding', () => { - // Given a user completing the EMPLOYER onboarding flow - - // When autoCreateSubmitWorkspace finishes setting up the workspace - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('John', 'Doe'); - - // Then the user should be navigated to the newly created Submit workspace - // so they land on their workspace immediately after onboarding - expect(navigateSpy).toHaveBeenCalledTimes(1); - expect(navigateSpy).toHaveBeenCalledWith(MOCK_POLICY_ID, expect.any(Boolean)); - }); - - it('reuses the existing onboarding workspace instead of creating a new one', () => { - // Given a user who already has an onboardingPolicyID set (e.g. assigned by an admin - // or from a previous partial onboarding attempt) - const existingPolicyID = 'existing-policy-id'; - const existingAdminsReportID = 'existing-admins-report-id'; - - (useOnyx as jest.Mock).mockImplementation((key: string) => { - if (key === 'onboardingPolicyID') { - return [existingPolicyID]; - } - if (key === 'onboardingAdminsChatReportID') { - return [existingAdminsReportID]; - } - if (key === 'session') { - return [MOCK_SESSION]; - } - if (key === 'betas') { - return [[]]; - } - if (key.startsWith('policy_')) { - return [false]; - } - return [undefined]; - }); - - // When the onboarding flow runs - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('Jane', 'Smith'); - - // Then no new workspace should be created, and completeOnboarding should use - // the pre-existing IDs to avoid creating duplicate workspaces - expect(createWorkspaceSpy).not.toHaveBeenCalled(); - expect(completeOnboardingSpy).toHaveBeenCalledWith( - expect.objectContaining({ - adminsChatReportID: existingAdminsReportID, - onboardingPolicyID: existingPolicyID, - }), - ); - }); - - it('skips workspace creation when the user is already a paid group policy admin', () => { - // Given a user who is already an admin of a paid group policy - (useOnyx as jest.Mock).mockImplementation((key: string) => { - if (key === 'session') { - return [MOCK_SESSION]; - } - if (key === 'betas') { - return [[]]; - } - if (key === 'onboardingPolicyID') { - return [undefined]; - } - if (key === 'onboardingAdminsChatReportID') { - return [undefined]; - } - if (key.startsWith('policy_')) { - return [true]; - } - return [undefined]; - }); - - // When onboarding completes - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('Jane', 'Smith'); - - // Then no Submit workspace should be created because the user already has - // a paid group workspace and creating another would be redundant - expect(createWorkspaceSpy).not.toHaveBeenCalled(); - }); - - it('skips workspace creation when the user domain restricts policy creation', () => { - // Given a user whose domain security group has enableRestrictedPolicyCreation set to true - (usePreferredPolicy as jest.Mock).mockReturnValue({ - isRestrictedToPreferredPolicy: false, - preferredPolicyID: undefined, - isRestrictedPolicyCreation: true, - }); - - // When the onboarding flow runs - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('Jane', 'Smith'); - - // Then workspace creation should be skipped because the domain admin has - // restricted users from creating their own policies - expect(createWorkspaceSpy).not.toHaveBeenCalled(); - expect(completeOnboardingSpy).toHaveBeenCalledTimes(1); - }); - - it('still completes onboarding and navigates even when workspace creation is skipped', () => { - // Given a user who cannot create a workspace due to domain restrictions - (usePreferredPolicy as jest.Mock).mockReturnValue({ - isRestrictedToPreferredPolicy: false, - preferredPolicyID: undefined, - isRestrictedPolicyCreation: true, - }); - - // When the onboarding flow runs - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('Jane', 'Smith'); - - // Then onboarding should still be completed and navigation should still occur - // because the user needs to finish onboarding regardless of workspace creation - expect(completeOnboardingSpy).toHaveBeenCalledTimes(1); - expect(setOnboardingAdminsChatReportIDSpy).toHaveBeenCalledTimes(1); - expect(setOnboardingPolicyIDSpy).toHaveBeenCalledTimes(1); - expect(navigateSpy).toHaveBeenCalledTimes(1); - }); - - it('uses the localCurrencyCode from personal details for workspace currency', () => { - // Given a user whose personal details have localCurrencyCode set to GBP - (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({ - accountID: MOCK_SESSION.accountID, - login: MOCK_SESSION.email, - localCurrencyCode: 'GBP', - }); - - // When a workspace is created during onboarding - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('John', 'Doe'); - - // Then the workspace should use GBP as its currency so it matches the user's locale - expect(createWorkspaceSpy).toHaveBeenCalledWith( - expect.objectContaining({ - currency: 'GBP', - }), - ); - }); - - it('falls back to USD when localCurrencyCode is not available', () => { - // Given a user whose personal details do not have a localCurrencyCode set - (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({ - accountID: MOCK_SESSION.accountID, - login: MOCK_SESSION.email, - localCurrencyCode: undefined, - }); - - // When a workspace is created during onboarding - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('John', 'Doe'); - - // Then the workspace should default to USD as a safe fallback currency - expect(createWorkspaceSpy).toHaveBeenCalledWith( - expect.objectContaining({ - currency: CONST.CURRENCY.USD, - }), - ); - }); - - it('forwards firstName and lastName to completeOnboarding for display name setup', () => { - // Given a user providing their name during the onboarding personal details step - - // When the onboarding flow completes - const {result} = renderHook(() => useAutoCreateSubmitWorkspace()); - result.current('Alice', 'Wonderland'); - - // Then the provided name should be passed through to completeOnboarding so the - // backend can set up the user's display name as part of the guided setup - expect(completeOnboardingSpy).toHaveBeenCalledWith( - expect.objectContaining({ - firstName: 'Alice', - lastName: 'Wonderland', - }), - ); - }); -});