From 99d0dd814ac20c9c6e90e158fd0b9f3df6153b07 Mon Sep 17 00:00:00 2001 From: Stevie LaFortune Date: Wed, 2 Oct 2024 16:09:35 -0500 Subject: [PATCH 001/264] Update Billing-page-coming-soon.md Added page details related to billing and discounts coming from https://github.com/Expensify/Expensify/issues/420340 --- .../Billing-page-coming-soon.md | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md index 2ae2fcd2426d..02edd95f845b 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md @@ -1,6 +1,49 @@ --- title: Billing and Subscriptions -description: Coming soon +description: Billing Page Overview --- -# Coming Soon +# Billing Overview + +At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. +Your Expensify bill is determined by: +The number of active members in your workspace +- Whether you have a Collect or Control plan +- Whether you’re on pay-per-use or an annual subscription +- Whether you’re using the Expensify Visa® Commercial Card +- Active members + +An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automated settings. + +## Collect vs Control plan + +Control plan coming soon, comparison to follow. + +## Pay-per-use vs annual subscription + +**Pay-per-use** + +With the pay-per-use rate, you pay the full rate per active member. +- **Collect plan:** $20 per active member +- **Control plan:** $36 per active member + +**Annual** + +With the annual rate, you set your monthly active member count at the beginning of your subscription and get 50% off your monthly active member cost. +- **Collect plan:** $10 per active member +- **Control plan:** $18 per active member + +If you have any additional active members above the number included in your set member count, they will be billed at the pay-per-use rate. You can also choose to increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. For questions, contact Concierge or your account manager. + +## The Expensify Card + +Bundling the Expensify Card with an annual subscription provides you with the lowest monthly price for Expensify. And the more you spend with the Expensify Cards, the lower your bill will be. + +If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per active member when paired with the annual subscription. +- **Collect plan:** $5 per active member +- **Control plan:** $9 per active member + +Additionally, you receive cash back every month that is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. +- 1% cash back on all Expensify Card purchases +- 2% cash back if the amount spent across your Expensify Cards is $250k or more (for U.S. purchases only) + From 30335e4905c557ee9a5e7f96f3b01c85e978ab5a Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Thu, 10 Oct 2024 14:23:09 +0200 Subject: [PATCH 002/264] fix(ci): remove `fetch-depth` paramater --- .github/workflows/reassurePerformanceTests.yml | 18 ------------------ .github/workflows/sendReassurePerfData.yml | 2 -- src/App.tsx | 2 ++ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index fb7a34d6fa01..ed5803c35b42 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -13,8 +13,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode @@ -24,22 +22,6 @@ jobs: git config --global user.email "test@test.com" git config --global user.name "Test" - - name: Get common ancestor commit - run: | - git fetch origin main - common_ancestor=$(git merge-base "${{ github.sha }}" origin/main) - echo "COMMIT_HASH=$common_ancestor" >> "$GITHUB_ENV" - - - name: Clean up deleted files - run: | - DELETED_FILES=$(git diff --name-only --diff-filter=D "$COMMIT_HASH" "${{ github.sha }}") - for file in $DELETED_FILES; do - if [ -n "$file" ]; then - rm -f "$file" - echo "Deleted file: $file" - fi - done - - name: Run performance testing script shell: bash run: | diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 884182bfc896..6ae528557faf 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -12,8 +12,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode diff --git a/src/App.tsx b/src/App.tsx index 177cc00c7dee..37d1875233d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,6 +52,8 @@ LogBox.ignoreLogs([ 'Setting a timer for a long period of time', ]); +// trigger action comment + const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; From 57cbb28278fdf353fc4d3067b2618c2f0e1f5c58 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Thu, 10 Oct 2024 14:45:02 +0200 Subject: [PATCH 003/264] fix: remove trigger action comment --- src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 37d1875233d2..177cc00c7dee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,8 +52,6 @@ LogBox.ignoreLogs([ 'Setting a timer for a long period of time', ]); -// trigger action comment - const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; From 40424321137ae3db8e1151c6fb2112a12fd90c2b Mon Sep 17 00:00:00 2001 From: Stevie LaFortune Date: Wed, 16 Oct 2024 14:04:31 -0500 Subject: [PATCH 004/264] Update docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md Co-authored-by: Carlos Alvarez --- .../billing-and-subscriptions/Billing-page-coming-soon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md index 02edd95f845b..03e6fb42a6da 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md @@ -7,7 +7,7 @@ description: Billing Page Overview At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. Your Expensify bill is determined by: -The number of active members in your workspace +- The number of active members in your workspace - Whether you have a Collect or Control plan - Whether you’re on pay-per-use or an annual subscription - Whether you’re using the Expensify Visa® Commercial Card From 9fd4343b4ca3451180f1c5477edf84da0d289bd2 Mon Sep 17 00:00:00 2001 From: Stevie LaFortune Date: Wed, 16 Oct 2024 15:52:42 -0500 Subject: [PATCH 005/264] Update Billing-page-coming-soon.md --- .../billing-and-subscriptions/Billing-page-coming-soon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md index 03e6fb42a6da..63c44247658e 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md @@ -13,7 +13,7 @@ Your Expensify bill is determined by: - Whether you’re using the Expensify Visa® Commercial Card - Active members -An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automated settings. +An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automatic actions by Concierge. ## Collect vs Control plan From 02d6985b9d4c2d49659a8dd22511ab6ce2432a66 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 30 Oct 2024 10:10:27 +0530 Subject: [PATCH 006/264] fix: Provide education/confirmation before creating workspaces in New Workspace flows. Signed-off-by: krishna2323 --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 1 + src/SCREENS.ts | 3 + src/components/CurrencyPicker.tsx | 84 +++++++++ src/components/ValuePicker/types.ts | 2 + src/libs/CurrencyUtils.ts | 7 + .../ModalStackNavigators/index.tsx | 6 + .../Navigators/RightModalNavigator.tsx | 6 + src/libs/Navigation/linkingConfig/config.ts | 7 + src/libs/Navigation/types.ts | 6 + src/libs/Permissions.ts | 1 + src/libs/SubscriptionUtils.ts | 1 + src/libs/actions/App.ts | 14 +- src/libs/actions/Policy/Policy.ts | 12 +- .../workspace/WorkspaceConfirmationPage.tsx | 169 ++++++++++++++++++ src/pages/workspace/WorkspacesListPage.tsx | 7 +- src/types/form/WorkspaceConfirmationForm.ts | 20 +++ 17 files changed, 338 insertions(+), 11 deletions(-) create mode 100644 src/components/CurrencyPicker.tsx create mode 100644 src/pages/workspace/WorkspaceConfirmationPage.tsx create mode 100644 src/types/form/WorkspaceConfirmationForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e14e536154a3..9f6776454464 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -3,6 +3,7 @@ import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; +import type {WorkspaceConfirmationForm} from './types/form/WorkspaceConfirmationForm'; import type * as OnyxTypes from './types/onyx'; import type {Attendee} from './types/onyx/IOU'; import type Onboarding from './types/onyx/Onboarding'; @@ -539,6 +540,7 @@ const ONYXKEYS = { ADD_PAYMENT_CARD_FORM_DRAFT: 'addPaymentCardFormDraft', WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', + WORKSPACE_CONFIRMATION_FORM: 'workspaceConfirmationForm', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft', @@ -722,6 +724,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM]: FormTypes.AddPaymentCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm; + [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: WorkspaceConfirmationForm; [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; [ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2e895537eaac..bd114bf2fb09 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1335,6 +1335,7 @@ const ROUTES = { }, WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', EXPLANATION_MODAL_ROOT: 'onboarding/explanation', + WORKSPACE_CONFIRMATION: 'workspace/confirmation', TRANSACTION_RECEIPT: { route: 'r/:reportID/transaction/:transactionID/receipt', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index feded7c81a47..7d9b8f20d4d5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -150,6 +150,7 @@ const SCREENS = { DETAILS: 'Details', PROFILE: 'Profile', REPORT_DETAILS: 'Report_Details', + WORKSPACE_CONFIRMATION: 'Workspace_Confirmation', REPORT_SETTINGS: 'Report_Settings', REPORT_DESCRIPTION: 'Report_Description', PARTICIPANTS: 'Participants', @@ -310,6 +311,8 @@ const SCREENS = { EXPORT: 'Report_Details_Export', }, + WORKSPACE_CONFIRMATION: {ROOT: 'Workspace_Confirmation_Root'}, + WORKSPACE: { ACCOUNTING: { ROOT: 'Policy_Accounting', diff --git a/src/components/CurrencyPicker.tsx b/src/components/CurrencyPicker.tsx new file mode 100644 index 000000000000..eae2633425b1 --- /dev/null +++ b/src/components/CurrencyPicker.tsx @@ -0,0 +1,84 @@ +import React, {forwardRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import CurrencySelectionListWithOnyx from './CurrencySelectionList'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import Modal from './Modal'; +import ScreenWrapper from './ScreenWrapper'; +import type {ValuePickerItem, ValuePickerProps} from './ValuePicker/types'; + +function CurrencyPicker({selectedCurrency, label, errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps, forwardedRef: ForwardedRef) { + const StyleUtils = useStyleUtils(); + const styles = useThemeStyles(); + const [isPickerVisible, setIsPickerVisible] = useState(false); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateInput = (item: ValuePickerItem) => { + if (item.value !== selectedCurrency) { + onInputChange?.(item.value); + } + hidePickerModal(); + }; + + const descStyle = !selectedCurrency || selectedCurrency.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null; + + return ( + + + + hidePickerModal} + onModalHide={hidePickerModal} + hideModalContentWhileAnimating + useNativeDriver + onBackdropPress={hidePickerModal} + > + + + updateInput({value: item.currencyCode})} + searchInputLabel="Currency" + initiallySelectedCurrencyCode={selectedCurrency} + /> + + + + ); +} + +CurrencyPicker.displayName = 'CurrencyPicker'; + +export default forwardRef(CurrencyPicker); diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts index b57c9d32061a..3f1eb101f879 100644 --- a/src/components/ValuePicker/types.ts +++ b/src/components/ValuePicker/types.ts @@ -60,6 +60,8 @@ type ValuePickerProps = { /** Whether to show the tooltip text */ shouldShowTooltips?: boolean; + + selectedCurrency?: string; }; export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps, ValuePickerListItem}; diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index f9ac681cb468..dfa6bae4412a 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Currency} from '@src/types/onyx'; import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; import * as NumberFormatUtils from './NumberFormatUtils'; @@ -30,6 +31,11 @@ function getCurrencyDecimals(currency: string = CONST.CURRENCY.USD): number { return decimals ?? 2; } +function getCurrency(currency: string = CONST.CURRENCY.USD): Currency | null { + const currencyItem = currencyList?.[currency]; + return currencyItem; +} + /** * Returns the currency's minor unit quantity * e.g. Cent in USD @@ -211,4 +217,5 @@ export { convertToDisplayStringWithoutCurrency, isValidCurrencyCode, convertToShortDisplayString, + getCurrency, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8a64424c8f7d..83786661cbfe 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -32,6 +32,7 @@ import type { TransactionDuplicateNavigatorParamList, TravelNavigatorParamList, WalletStatementNavigatorParamList, + WorkspaceConfirmationNavigatorParamList, } from '@navigation/types'; import type {ThemeStyles} from '@styles/index'; import type {Screen} from '@src/SCREENS'; @@ -121,6 +122,10 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator require('../../../../pages/home/report/ReportDetailsExportPage').default, }); +const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({ + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default, +}); + const ReportSettingsModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default, [SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default, @@ -700,4 +705,5 @@ export { SearchSavedSearchModalStackNavigator, MissingPersonalDetailsModalStackNavigator, DebugModalStackNavigator, + WorkspaceConfirmationModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index da1ce32bf747..a75b8a712b5e 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -79,6 +79,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.NEW_CHAT} component={ModalStackNavigators.NewChatModalStackNavigator} /> + + + ['config'] = { path: ROUTES.KEYBOARD_SHORTCUTS, }, [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route, + // [SCREENS.WORKSPACE.CONFIRMATION]: {path: ROUTES.WORKSPACE_CONFIRMATION}, [SCREENS.SETTINGS.SHARE_CODE]: { path: ROUTES.SETTINGS_SHARE_CODE, }, @@ -947,6 +948,11 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DETAILS.EXPORT]: ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.route, }, }, + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: { + screens: { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION, + }, + }, [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: { screens: { [SCREENS.REPORT_SETTINGS.ROOT]: { @@ -1093,6 +1099,7 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DESCRIPTION_ROOT]: ROUTES.REPORT_DESCRIPTION.route, }, }, + [SCREENS.RIGHT_MODAL.NEW_CHAT]: { screens: { [SCREENS.NEW_CHAT.ROOT]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3de07f2c801f..59e677e8575c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -901,6 +901,10 @@ type NewChatNavigatorParamList = { [SCREENS.NEW_CHAT.NEW_CHAT_EDIT_NAME]: undefined; }; +type WorkspaceConfirmationNavigatorParamList = { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: undefined; +}; + type DetailsNavigatorParamList = { [SCREENS.DETAILS_ROOT]: { login: string; @@ -1344,6 +1348,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.EXPENSIFY_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DOMAIN_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; @@ -1701,4 +1706,5 @@ export type { RestrictedActionParamList, MissingPersonalDetailsParamList, DebugParamList, + WorkspaceConfirmationNavigatorParamList, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 45203c1db5b6..a619420d92c5 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,6 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { + return true return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index f2ceef9069fa..6cbf6d5b0d9e 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -435,6 +435,7 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { + return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 5a594a19e15a..903d0bf08b17 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -366,10 +366,12 @@ function endSignOnTransition() { * @param [transitionFromOldDot] Optional, if the user is transitioning from old dot * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy * @param [backTo] An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. + * @param [policyID] Optional, Policy id. + * @param [file],file */ -function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '') { - const policyID = Policy.generatePolicyID(); - Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID, makeMeAdmin); +function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '', policyID = '', file?: File) { + const genereatedPolicyID = Policy.generatePolicyID(); + Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID || genereatedPolicyID, makeMeAdmin, file); Navigation.isNavigationReady() .then(() => { @@ -377,7 +379,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po // We must call goBack() to remove the /transition route from history Navigation.goBack(); } - savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin); + savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin, file); Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, backTo)); }) .then(endSignOnTransition); @@ -391,8 +393,8 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po * @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false) { - Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID); +function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, file?: File) { + Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', file); } /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index b419431bbbb3..94fe15f19e16 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1538,7 +1538,7 @@ function buildOptimisticCustomUnits(reportCurrency?: string): OptimisticCustomUn * @param [policyID] custom policy id we will use for created workspace * @param [makeMeAdmin] leave the calling account as an admin on the policy */ -function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false) { +function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, file?: File) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, outputCurrency} = buildOptimisticCustomUnits(); @@ -1561,6 +1561,8 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol makeMeAdmin, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + avatarURL: file?.uri ?? null, + originalFileName: file?.name, employeeList: { [sessionEmail]: { role: CONST.POLICY.ROLE.ADMIN, @@ -1592,7 +1594,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol * @param [policyID] custom policy id we will use for created workspace * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace */ -function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string) { +function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string, file?: File) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); @@ -1653,6 +1655,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, + avatarURL: file?.uri, + originalFileName: file?.name, }, }, { @@ -1824,8 +1828,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName * @param [policyID] custom policy id we will use for created workspace * @param [engagementChoice] Purpose of using application selected by user in guided setup flow */ -function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = ''): CreateWorkspaceParams { - const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice); +function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = '', file?: File): CreateWorkspaceParams { + const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, file); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return params; diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx new file mode 100644 index 000000000000..054a62b15916 --- /dev/null +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -0,0 +1,169 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Avatar from '@components/Avatar'; +import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; +import CurrencyPicker from '@components/CurrencyPicker'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import {getCurrency} from '@libs/CurrencyUtils'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import getCurrentUrl from '@libs/Navigation/currentUrl'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import * as App from '@userActions/App'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; +import withPolicy from './withPolicy'; + +function WorkspaceNamePage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const name = values.name.trim(); + + if (!ValidationUtils.isRequiredFulfilled(name)) { + errors.name = translate('workspace.editor.nameIsRequiredError'); + } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 + // code units. + ErrorUtils.addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } + + return errors; + }, + [translate], + ); + + const currentUrl = getCurrentUrl(); + const policyID = Policy.generatePolicyID(); + const [session] = useOnyx(ONYXKEYS.SESSION); + const url = new URL(currentUrl); + // Approved Accountants and Guides can enter a flow where they make a workspace for other users, + // and those are passed as a search parameter when using transition links + const policyOwnerEmail = url.searchParams.get('ownerEmail') ?? ''; + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const defaultWorkspaceName = Policy.generateDefaultWorkspaceName(policyOwnerEmail); + + const userCurrency = allPersonalDetails?.[session?.accountID ?? 0]?.localCurrencyCode ?? CONST.CURRENCY.USD; + const [currencyCode, setCurrencyCode] = useState(userCurrency); + + const currency = getCurrency(currencyCode); + const [workspaceAvatar, setWorkspaceAvatar] = useState({avatarUri: null, avatarFileName: null, avatarFileType: null}); + const [avatarFile, setAvatarFile] = useState(); + + const stashedLocalAvatarImage = workspaceAvatar?.avatarUri; + + const DefaultAvatar = useCallback( + () => ( + + ), + [workspaceAvatar?.avatarUri, defaultWorkspaceName, styles.alignSelfCenter, styles.avatarXLarge, policyID], + ); + + return ( + + Navigation.goBack()} + /> + + + {translate('workspace.emptyWorkspace.subtitle')} + + { + setAvatarFile(image); + // setWorkspaceAvatar({avatarUri: image.uri ?? '', avatarFileName: image.name ?? '', avatarFileType: image.type}); + }} + onImageRemoved={() => { + setAvatarFile(undefined); + // setWorkspaceAvatar({avatarUri: null, avatarFileName: null, avatarFileType: null}); + }} + size={CONST.AVATAR_SIZE.XLARGE} + avatarStyle={[styles.avatarXLarge, styles.alignSelfCenter]} + shouldDisableViewPhoto + editIcon={Expensicons.Camera} + editIconStyle={styles.smallEditIconAccount} + shouldUseStyleUtilityForAnchorPosition + // style={styles.w100} + type={CONST.ICON_TYPE_WORKSPACE} + style={[styles.w100, styles.alignItemsCenter, styles.mv4, styles.mb6, styles.alignSelfCenter]} + DefaultAvatar={DefaultAvatar} + /> + { + App.createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, avatarFile as File); + }} + enabledWhenOffline + > + + + + + { + setCurrencyCode(val as string); + }} + /> + + + + + ); +} + +WorkspaceNamePage.displayName = 'WorkspaceNamePage'; + +export default withPolicy(WorkspaceNamePage); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 82503134b09e..9a55ebac2a49 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -396,7 +396,12 @@ function WorkspacesListPage() { subtitle={translate('workspace.emptyWorkspace.subtitle')} ctaText={translate('workspace.new.newWorkspace')} ctaAccessibilityLabel={translate('workspace.new.newWorkspace')} - onCtaPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())} + onCtaPress={() => + interceptAnonymousUser( + () => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION), + // App.createWorkspaceWithPolicyDraftAndNavigateToIt() + ) + } illustration={LottieAnimations.WorkspacePlanet} // We use this style to vertically center the illustration, as the original illustration is not centered illustrationStyle={styles.emptyWorkspaceIllustrationStyle} diff --git a/src/types/form/WorkspaceConfirmationForm.ts b/src/types/form/WorkspaceConfirmationForm.ts new file mode 100644 index 000000000000..8ae2261d018a --- /dev/null +++ b/src/types/form/WorkspaceConfirmationForm.ts @@ -0,0 +1,20 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + NAME: 'name', + CURRENCY: 'currency', +} as const; + +type InputID = ValueOf; + +type WorkspaceConfirmationForm = Form< + InputID, + { + [INPUT_IDS.NAME]: string; + [INPUT_IDS.CURRENCY]: string; + } +>; + +export type {WorkspaceConfirmationForm}; +export default INPUT_IDS; From 96a77e8df1b3905be8f1c3985dc9583e23a74442 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 30 Oct 2024 10:23:52 +0530 Subject: [PATCH 007/264] minor updates. Signed-off-by: krishna2323 --- src/libs/actions/App.ts | 19 ++++++++--- src/libs/actions/Policy/Policy.ts | 33 ++++++++++++++----- .../workspace/WorkspaceConfirmationPage.tsx | 10 +++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 903d0bf08b17..4329386406df 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -369,9 +369,18 @@ function endSignOnTransition() { * @param [policyID] Optional, Policy id. * @param [file],file */ -function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '', policyID = '', file?: File) { +function createWorkspaceWithPolicyDraftAndNavigateToIt( + policyOwnerEmail = '', + policyName = '', + transitionFromOldDot = false, + makeMeAdmin = false, + backTo = '', + policyID = '', + currency?: string, + file?: File, +) { const genereatedPolicyID = Policy.generatePolicyID(); - Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID || genereatedPolicyID, makeMeAdmin, file); + Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID || genereatedPolicyID, makeMeAdmin, currency, file); Navigation.isNavigationReady() .then(() => { @@ -379,7 +388,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po // We must call goBack() to remove the /transition route from history Navigation.goBack(); } - savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin, file); + savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin, currency, file); Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, backTo)); }) .then(endSignOnTransition); @@ -393,8 +402,8 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po * @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, file?: File) { - Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', file); +function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency?: '', file?: File) { + Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', currency, file); } /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 94fe15f19e16..c757cccab100 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1538,9 +1538,9 @@ function buildOptimisticCustomUnits(reportCurrency?: string): OptimisticCustomUn * @param [policyID] custom policy id we will use for created workspace * @param [makeMeAdmin] leave the calling account as an admin on the policy */ -function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, file?: File) { +function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, currency = '', file?: File) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const {customUnits, outputCurrency} = buildOptimisticCustomUnits(); + const {customUnits, outputCurrency} = buildOptimisticCustomUnits(currency); const optimisticData: OnyxUpdate[] = [ { @@ -1555,7 +1555,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol ownerAccountID: sessionAccountID, isPolicyExpenseChatEnabled: true, areCategoriesEnabled: true, - outputCurrency, + outputCurrency: currency || outputCurrency, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, customUnits, makeMeAdmin, @@ -1594,10 +1594,19 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol * @param [policyID] custom policy id we will use for created workspace * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace */ -function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string, file?: File) { +function buildPolicyData( + policyOwnerEmail = '', + makeMeAdmin = false, + policyName = '', + policyID = generatePolicyID(), + expenseReportId?: string, + engagementChoice?: string, + currency?: '', + file?: File, +) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); + const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(currency); const { adminsChatReportID, @@ -1624,7 +1633,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName owner: sessionEmail, ownerAccountID: sessionAccountID, isPolicyExpenseChatEnabled: true, - outputCurrency, + outputCurrency: currency || outputCurrency, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, @@ -1828,8 +1837,16 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName * @param [policyID] custom policy id we will use for created workspace * @param [engagementChoice] Purpose of using application selected by user in guided setup flow */ -function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = '', file?: File): CreateWorkspaceParams { - const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, file); +function createWorkspace( + policyOwnerEmail = '', + makeMeAdmin = false, + policyName = '', + policyID = generatePolicyID(), + engagementChoice = '', + currency?: '', + file?: File, +): CreateWorkspaceParams { + const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, currency, file); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return params; diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx index 054a62b15916..b105998ba22a 100644 --- a/src/pages/workspace/WorkspaceConfirmationPage.tsx +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -28,7 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; import withPolicy from './withPolicy'; -function WorkspaceNamePage() { +function WorkspaceConfirmationPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -90,7 +90,7 @@ function WorkspaceNamePage() { { - App.createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, avatarFile as File); + App.createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, currencyCode, avatarFile as File); }} enabledWhenOffline > @@ -164,6 +164,6 @@ function WorkspaceNamePage() { ); } -WorkspaceNamePage.displayName = 'WorkspaceNamePage'; +WorkspaceConfirmationPage.displayName = 'WorkspaceConfirmationPage'; -export default withPolicy(WorkspaceNamePage); +export default withPolicy(WorkspaceConfirmationPage); From 879bd6d62510b1a13f05f465752db59767cb70e8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 30 Oct 2024 12:40:16 -0700 Subject: [PATCH 008/264] Updating billing page --- .../Billing-page-coming-soon.md | 49 ------------------- .../billing-and-subscriptions/Billing-page.md | 47 +++++++++++++++++- 2 files changed, 45 insertions(+), 51 deletions(-) delete mode 100644 docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md deleted file mode 100644 index 63c44247658e..000000000000 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Billing and Subscriptions -description: Billing Page Overview ---- - -# Billing Overview - -At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. -Your Expensify bill is determined by: -- The number of active members in your workspace -- Whether you have a Collect or Control plan -- Whether you’re on pay-per-use or an annual subscription -- Whether you’re using the Expensify Visa® Commercial Card -- Active members - -An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automatic actions by Concierge. - -## Collect vs Control plan - -Control plan coming soon, comparison to follow. - -## Pay-per-use vs annual subscription - -**Pay-per-use** - -With the pay-per-use rate, you pay the full rate per active member. -- **Collect plan:** $20 per active member -- **Control plan:** $36 per active member - -**Annual** - -With the annual rate, you set your monthly active member count at the beginning of your subscription and get 50% off your monthly active member cost. -- **Collect plan:** $10 per active member -- **Control plan:** $18 per active member - -If you have any additional active members above the number included in your set member count, they will be billed at the pay-per-use rate. You can also choose to increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. For questions, contact Concierge or your account manager. - -## The Expensify Card - -Bundling the Expensify Card with an annual subscription provides you with the lowest monthly price for Expensify. And the more you spend with the Expensify Cards, the lower your bill will be. - -If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per active member when paired with the annual subscription. -- **Collect plan:** $5 per active member -- **Control plan:** $9 per active member - -Additionally, you receive cash back every month that is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. -- 1% cash back on all Expensify Card purchases -- 2% cash back if the amount spent across your Expensify Cards is $250k or more (for U.S. purchases only) - diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md index f945840d65da..63c44247658e 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md @@ -1,6 +1,49 @@ --- title: Billing and Subscriptions -description: An overview of how billing works in Expensify. +description: Billing Page Overview --- -# Coming Soon +# Billing Overview + +At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. +Your Expensify bill is determined by: +- The number of active members in your workspace +- Whether you have a Collect or Control plan +- Whether you’re on pay-per-use or an annual subscription +- Whether you’re using the Expensify Visa® Commercial Card +- Active members + +An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automatic actions by Concierge. + +## Collect vs Control plan + +Control plan coming soon, comparison to follow. + +## Pay-per-use vs annual subscription + +**Pay-per-use** + +With the pay-per-use rate, you pay the full rate per active member. +- **Collect plan:** $20 per active member +- **Control plan:** $36 per active member + +**Annual** + +With the annual rate, you set your monthly active member count at the beginning of your subscription and get 50% off your monthly active member cost. +- **Collect plan:** $10 per active member +- **Control plan:** $18 per active member + +If you have any additional active members above the number included in your set member count, they will be billed at the pay-per-use rate. You can also choose to increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. For questions, contact Concierge or your account manager. + +## The Expensify Card + +Bundling the Expensify Card with an annual subscription provides you with the lowest monthly price for Expensify. And the more you spend with the Expensify Cards, the lower your bill will be. + +If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per active member when paired with the annual subscription. +- **Collect plan:** $5 per active member +- **Control plan:** $9 per active member + +Additionally, you receive cash back every month that is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. +- 1% cash back on all Expensify Card purchases +- 2% cash back if the amount spent across your Expensify Cards is $250k or more (for U.S. purchases only) + From e55f5a84c57a9bda3344964197e38cebf4ee4ec6 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 5 Nov 2024 17:44:15 +0300 Subject: [PATCH 009/264] updated EmptySearchView to show generic nothing to show message for empty search results --- src/components/Search/index.tsx | 6 ++++- src/pages/Search/EmptySearchView.tsx | 40 ++++++++++++++++------------ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9238488361b0..8a54b1d14ed1 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -33,6 +33,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {useSearchContext} from './SearchContext'; import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types'; @@ -307,7 +308,10 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr if (shouldShowEmptyState) { return ( - + ); } diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 8e61978c169e..e274c981aa21 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -26,6 +26,7 @@ import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; type EmptySearchViewProps = { type: SearchDataTypes; + noExpensesCreatedYet?: boolean; }; const tripsFeatures: FeatureListItem[] = [ @@ -39,7 +40,7 @@ const tripsFeatures: FeatureListItem[] = [ }, ]; -function EmptySearchView({type}: EmptySearchViewProps) { +function EmptySearchView({type, noExpensesCreatedYet = false}: EmptySearchViewProps) { const theme = useTheme(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -114,21 +115,26 @@ function EmptySearchView({type}: EmptySearchViewProps) { ], }; case CONST.SEARCH.DATA_TYPES.EXPENSE: - return { - headerMedia: LottieAnimations.GenericEmptyState, - headerStyles: [StyleUtils.getBackgroundColorStyle(theme.emptyFolderBG)], - title: translate('search.searchResults.emptyExpenseResults.title'), - subtitle: translate('search.searchResults.emptyExpenseResults.subtitle'), - buttons: [ - {buttonText: translate('emptySearchView.takeATour'), buttonAction: () => Link.openExternalLink(navatticLink)}, - { - buttonText: translate('iou.createExpense'), - buttonAction: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.CREATE, ReportUtils.generateReportID())), - success: true, - }, - ], - headerContentStyles: styles.emptyStateFolderWebStyles, - }; + if (noExpensesCreatedYet) { + return { + headerMedia: LottieAnimations.GenericEmptyState, + headerStyles: [StyleUtils.getBackgroundColorStyle(theme.emptyFolderBG)], + title: translate('search.searchResults.emptyExpenseResults.title'), + subtitle: translate('search.searchResults.emptyExpenseResults.subtitle'), + buttons: [ + {buttonText: translate('emptySearchView.takeATour'), buttonAction: () => Link.openExternalLink(navatticLink)}, + { + buttonText: translate('iou.createExpense'), + buttonAction: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.CREATE, ReportUtils.generateReportID())), + success: true, + }, + ], + headerContentStyles: styles.emptyStateFolderWebStyles, + }; + } + // We want to display the default nothing to show message if the user has expenses created + // but the current search filter result is empty. + // eslint-disable-next-line no-fallthrough case CONST.SEARCH.DATA_TYPES.CHAT: case CONST.SEARCH.DATA_TYPES.INVOICE: default: @@ -140,7 +146,7 @@ function EmptySearchView({type}: EmptySearchViewProps) { headerContentStyles: styles.emptyStateFolderWebStyles, }; } - }, [type, StyleUtils, translate, theme, styles, subtitleComponent, ctaErrorMessage, navatticLink]); + }, [type, StyleUtils, translate, theme, styles, subtitleComponent, ctaErrorMessage, navatticLink, noExpensesCreatedYet]); return ( Date: Wed, 6 Nov 2024 17:08:00 +0530 Subject: [PATCH 010/264] show DelegateNoAccessModal for restricted delegate actions --- src/CONST.ts | 6 ++ src/components/AddressForm.tsx | 6 +- src/components/DelegateNoAccessModal.tsx | 8 +-- src/components/DelegateNoAccessWrapper.tsx | 71 +++++++++++++++++++ src/components/Form/FormProvider.tsx | 2 +- src/pages/AddressPage.tsx | 21 +++++- src/pages/settings/InitialSettingsPage.tsx | 13 ++++ .../Profile/Contacts/ContactMethodsPage.tsx | 2 - .../Profile/Contacts/NewContactMethodPage.tsx | 7 +- .../PersonalDetails/DateOfBirthPage.tsx | 21 ++++-- .../Profile/PersonalDetails/LegalNamePage.tsx | 25 +++++-- .../PersonalDetails/PhoneNumberPage.tsx | 21 +++++- .../Security/SecuritySettingsPage.tsx | 18 ++++- .../settings/Wallet/WalletPage/WalletPage.tsx | 19 +++++ 14 files changed, 212 insertions(+), 28 deletions(-) create mode 100644 src/components/DelegateNoAccessWrapper.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 8a1e9cfbf67c..5c5745f7cfe6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4500,6 +4500,12 @@ const CONST = { ALL: 'all', SUBMITTER: 'submitter', }, + DELEGATE: { + DENIED_ACCESS_VARIANTS: { + DELEGATE: 'delegate', + SUBMITTER: 'submitter', + }, + }, DELEGATE_ROLE_HELPDOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/', STRIPE_GBP_AUTH_STATUSES: { SUCCEEDED: 'succeeded', diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 4470481d2be6..709770617fdc 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -56,6 +56,9 @@ type AddressFormProps = { /** A unique Onyx key identifying the form */ formID: typeof ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM; + + /** Whether or not validation should be skipped */ + skipValidation?: boolean; }; function AddressForm({ @@ -70,6 +73,7 @@ function AddressForm({ street2 = '', submitButtonText = '', zip = '', + skipValidation = false, }: AddressFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -139,7 +143,7 @@ function AddressForm({ void; - delegatorEmail: string; }; -export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose, delegatorEmail = ''}: DelegateNoAccessModalProps) { +export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose}: DelegateNoAccessModalProps) { const {translate} = useLocalize(); - const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail}); + const {delegatorEmail} = useDelegateUserDetails(); + const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail ?? ''}); const noDelegateAccessHyperLinked = translate('delegate.notAllowedMessageHyperLinked'); - const delegateNoAccessPrompt = ( {noDelegateAccessPromptStart} diff --git a/src/components/DelegateNoAccessWrapper.tsx b/src/components/DelegateNoAccessWrapper.tsx new file mode 100644 index 000000000000..36e675547d7e --- /dev/null +++ b/src/components/DelegateNoAccessWrapper.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import {OnyxEntry, useOnyx} from 'react-native-onyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import AccountUtils from '@libs/AccountUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {Account} from '@src/types/onyx'; +import callOrReturn from '@src/types/utils/callOrReturn'; +import {FullPageNotFoundViewProps} from './BlockingViews/FullPageNotFoundView'; + +const DENIED_ACCESS_VARIANTS = { + [CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]: (account: OnyxEntry) => isDelegate(account), + [CONST.DELEGATE.DENIED_ACCESS_VARIANTS.SUBMITTER]: (account: OnyxEntry) => isSubmitter(account), +} as const satisfies Record) => boolean>; + +type AccessDeniedVariants = keyof typeof DENIED_ACCESS_VARIANTS; + +type DelegateNoAccessWrapperProps = { + accessDeniedVariants?: AccessDeniedVariants[]; + FullPageNotFoundViewProps?: FullPageNotFoundViewProps; + shouldShowFullScreenFallback?: boolean; + children: (() => React.ReactNode) | React.ReactNode; +}; + +type PageNotFoundFallbackProps = { + shouldShowFullScreenFallback?: boolean; +}; + +function isDelegate(account: OnyxEntry) { + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + return isActingAsDelegate; +} + +function isSubmitter(account: OnyxEntry) { + const isDelegateOnlySubmitter = AccountUtils.isDelegateOnlySubmitter(account); + return isDelegateOnlySubmitter; +} + +function PageNotFoundFallback({shouldShowFullScreenFallback}: PageNotFoundFallbackProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + return ( + { + if (shouldShowFullScreenFallback) { + Navigation.dismissModal(); + return; + } + Navigation.goBack(); + }} + shouldShowBackButton={!shouldShowFullScreenFallback ? shouldUseNarrowLayout : undefined} + /> + ); +} + +function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldShowFullScreenFallback, ...props}: DelegateNoAccessWrapperProps) { + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isPageAccessDenied = accessDeniedVariants.reduce((acc, variant) => { + const accessDeniedFunction = DENIED_ACCESS_VARIANTS[variant]; + return acc || accessDeniedFunction(account); + }, false); + if (isPageAccessDenied) { + return ; + } + return callOrReturn(props.children); +} + +export default DelegateNoAccessWrapper; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 1d66953c1070..d1b45e1d9327 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -44,7 +44,7 @@ type FormProviderProps = FormProps}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: FormOnyxValues) => FormInputErrors; + validate?: (values: FormOnyxValues) => FormInputErrors | undefined; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; diff --git a/src/pages/AddressPage.tsx b/src/pages/AddressPage.tsx index 88e52409751b..670b582e08a2 100644 --- a/src/pages/AddressPage.tsx +++ b/src/pages/AddressPage.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import {type OnyxEntry, useOnyx} from 'react-native-onyx'; import AddressForm from '@components/AddressForm'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -37,6 +38,17 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true, backTo const [city, setCity] = useState(address?.city); const [zipcode, setZipcode] = useState(address?.zip); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + + // For delegates, modifying legal address is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + const skipValidation = isActingAsDelegate; + const handleSubmit = (values: FormOnyxValues) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : updateAddress(values); + }; + useEffect(() => { if (!address) { return; @@ -91,7 +103,7 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true, backTo ) : ( )} + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 8c1d68e0a95b..3256a6dbc500 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -8,6 +8,7 @@ import type {ValueOf} from 'type-fest'; import AccountSwitcher from '@components/AccountSwitcher'; import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView'; import ConfirmModal from '@components/ConfirmModal'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {InitialURLContext} from '@components/InitialURLContextProvider'; @@ -83,6 +84,10 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const network = useNetwork(); const theme = useTheme(); const styles = useThemeStyles(); @@ -242,6 +247,10 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr } : { action() { + if (isActingAsDelegate) { + setIsNoDelegateAccessMenuVisible(true); + return; + } resetExitSurveyForm(() => Navigation.navigate(ROUTES.SETTINGS_EXIT_SURVEY_REASON)); }, }), @@ -435,6 +444,10 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr onCancel={() => toggleSignoutConfirmModal(false)} /> + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 92a246949c53..dac36a15cefd 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -36,7 +36,6 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { const [account] = useOnyx(ONYXKEYS.ACCOUNT); const isActingAsDelegate = !!account?.delegatedAccess?.delegate; const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const {delegatorEmail} = useDelegateUserDetails(); // Sort the login names by placing the one corresponding to the default contact method as the first item before displaying the contact methods. // The default contact method is determined by checking against the session email (the current login). @@ -132,7 +131,6 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { setIsNoDelegateAccessMenuVisible(false)} - delegatorEmail={delegatorEmail ?? ''} /> ); diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index c2a7e1b6712c..d23af0371ef3 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -3,6 +3,7 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -110,7 +111,8 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { }, [navigateBackTo]); return ( - + // + loginInputRef.current?.focus()} includeSafeAreaPaddingBottom={false} @@ -174,7 +176,8 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { description={translate('contacts.enterMagicCode', {contactMethod})} /> - + + // ); } diff --git a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx index e91093731c03..882f29d51b99 100644 --- a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx @@ -1,8 +1,9 @@ import {subYears} from 'date-fns'; -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -16,7 +17,7 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/DateOfBirthForm'; +import INPUT_IDS, {DateOfBirthForm} from '@src/types/form/DateOfBirthForm'; import type {PrivatePersonalDetails} from '@src/types/onyx'; type DateOfBirthPageOnyxProps = { @@ -30,6 +31,9 @@ type DateOfBirthPageProps = DateOfBirthPageOnyxProps; function DateOfBirthPage({privatePersonalDetails, isLoadingApp = true}: DateOfBirthPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); /** * @returns An object containing the errors for each inputID @@ -48,6 +52,13 @@ function DateOfBirthPage({privatePersonalDetails, isLoadingApp = true}: DateOfBi return errors; }, []); + // For delegates, modifying legal DOB is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + + const skipValidation = isActingAsDelegate; + const handleSubmit = (DOB: DateOfBirthForm) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : PersonalDetails.updateDateOfBirth(DOB); + }; return ( diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx index 99e9c910cbdf..cf9a8ec398a3 100644 --- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx @@ -1,7 +1,8 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -33,12 +34,14 @@ type LegalNamePageProps = LegalNamePageOnyxProps; const updateLegalName = (values: PrivatePersonalDetails) => { PersonalDetails.updateLegalName(values.legalFirstName?.trim() ?? '', values.legalLastName?.trim() ?? ''); }; - function LegalNamePage({privatePersonalDetails, isLoadingApp = true}: LegalNamePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const legalFirstName = privatePersonalDetails?.legalFirstName ?? ''; const legalLastName = privatePersonalDetails?.legalLastName ?? ''; + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); const validate = useCallback( (values: FormOnyxValues) => { @@ -83,6 +86,14 @@ function LegalNamePage({privatePersonalDetails, isLoadingApp = true}: LegalNameP [translate], ); + // For delegates, modifying legal Name is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + + const skipValidation = isActingAsDelegate; + const handleSubmit = (values: PrivatePersonalDetails) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : updateLegalName(values); + }; + return ( @@ -130,6 +141,10 @@ function LegalNamePage({privatePersonalDetails, isLoadingApp = true}: LegalNameP )} + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx index 12c6f011c6a2..536fa510ef54 100644 --- a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -32,6 +33,10 @@ function PhoneNumberPage() { const validateLoginError = ErrorUtils.getEarliestErrorField(privatePersonalDetails, 'phoneNumber'); const currenPhoneNumber = privatePersonalDetails?.phoneNumber ?? ''; + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const updatePhoneNumber = (values: PrivatePersonalDetails) => { // Clear the error when the user tries to submit the form if (validateLoginError) { @@ -66,6 +71,12 @@ function PhoneNumberPage() { }, [translate, validateLoginError], ); + // For delegates, modifying Phone Number is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + const skipValidation = isActingAsDelegate; + const handleSubmit = (values: PrivatePersonalDetails) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : updatePhoneNumber(values); + }; return ( @@ -112,6 +123,10 @@ function PhoneNumberPage() { )} + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index cd8e7c14d882..f646725319b3 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -5,6 +5,7 @@ import {Dimensions, View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import {FallbackAvatar} from '@components/Icon/Expensicons'; @@ -20,6 +21,7 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -63,6 +65,10 @@ function SecuritySettingsPage() { anchorPositionRight: 0, }); + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + + // const [account] = useOnyx(ONYXKEYS.ACCOUNT); + // const isActingAsDelegate = !!account?.delegatedAccess?.delegate; const setMenuPosition = useCallback(() => { if (!delegateButtonRef.current) { return; @@ -92,7 +98,9 @@ function SecuritySettingsPage() { setShouldShowDelegatePopoverMenu(true); setSelectedDelegate(delegate); }; - + const showDelegateNoAccessMenu = () => { + setIsNoDelegateAccessMenuVisible(true); + }; useLayoutEffect(() => { const popoverPositionListener = Dimensions.addEventListener('change', () => { debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); @@ -111,12 +119,12 @@ function SecuritySettingsPage() { { translationKey: 'twoFactorAuth.headerTitle', icon: Expensicons.Shield, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute())), + action: isActingAsDelegate ? showDelegateNoAccessMenu : waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute())), }, { translationKey: 'closeAccountPage.closeAccount', icon: Expensicons.ClosedSign, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), + action: isActingAsDelegate ? showDelegateNoAccessMenu : waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), }, ]; @@ -333,6 +341,10 @@ function SecuritySettingsPage() { /> + setIsNoDelegateAccessMenuVisible(false)} + /> )} diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 7b9366370349..79b0a434a5ad 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -7,6 +7,7 @@ import {ActivityIndicator, Dimensions, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import ConfirmModal from '@components/ConfirmModal'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -22,6 +23,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; +import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePaymentMethodState from '@hooks/usePaymentMethodState'; @@ -59,6 +61,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {initialValue: {}}); const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -179,6 +185,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { setMenuPosition(); return; } + if (isActingAsDelegate) { + setIsNoDelegateAccessMenuVisible(true); + return; + } setShouldShowAddPaymentMenu(true); setMenuPosition(); }; @@ -499,6 +509,11 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { } onPress={() => { + if (isActingAsDelegate) { + setIsNoDelegateAccessMenuVisible(true); + return; + } + if (!isUserValidated) { Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ENABLE_PAYMENTS)); return; @@ -595,6 +610,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { anchorRef={addPaymentMethodAnchorRef} shouldShowPersonalBankAccountOption /> + setIsNoDelegateAccessMenuVisible(false)} + /> ); } From f283a185438b2e4addea67acc03ac10bb6bf8055 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 14:15:14 +0530 Subject: [PATCH 011/264] updated ONYXKEYS file. Signed-off-by: krishna2323 --- src/ONYXKEYS.ts | 4 ++-- src/components/CurrencyPicker.tsx | 5 ++++- src/components/ValuePicker/types.ts | 2 -- .../AppNavigator/ModalStackNavigators/index.tsx | 8 ++++---- .../AppNavigator/Navigators/RightModalNavigator.tsx | 11 +++++------ src/libs/Navigation/linkingConfig/config.ts | 12 +++++------- src/libs/Navigation/types.ts | 10 +++++----- src/libs/Permissions.ts | 1 - src/libs/SubscriptionUtils.ts | 1 - src/libs/actions/App.ts | 2 +- src/libs/actions/Policy/Policy.ts | 2 +- src/types/form/index.ts | 1 + 12 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f15337b22e75..441f85b01a16 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -3,7 +3,6 @@ import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; -import type {WorkspaceConfirmationForm} from './types/form/WorkspaceConfirmationForm'; import type * as OnyxTypes from './types/onyx'; import type {Attendee} from './types/onyx/IOU'; import type Onboarding from './types/onyx/Onboarding'; @@ -547,6 +546,7 @@ const ONYXKEYS = { WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CONFIRMATION_FORM: 'workspaceConfirmationForm', + WORKSPACE_CONFIRMATION_FORM_DRAFT: 'workspaceConfirmationFormDraft', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft', @@ -730,7 +730,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM]: FormTypes.AddPaymentCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm; - [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: WorkspaceConfirmationForm; + [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: FormTypes.WorkspaceConfirmationForm; [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; [ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName; diff --git a/src/components/CurrencyPicker.tsx b/src/components/CurrencyPicker.tsx index eae2633425b1..7f60eb94fc34 100644 --- a/src/components/CurrencyPicker.tsx +++ b/src/components/CurrencyPicker.tsx @@ -12,7 +12,10 @@ import Modal from './Modal'; import ScreenWrapper from './ScreenWrapper'; import type {ValuePickerItem, ValuePickerProps} from './ValuePicker/types'; -function CurrencyPicker({selectedCurrency, label, errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps, forwardedRef: ForwardedRef) { +type CurrencyPickerProps = { + selectedCurrency?: string; +}; +function CurrencyPicker({selectedCurrency, label, errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps & CurrencyPickerProps, forwardedRef: ForwardedRef) { const StyleUtils = useStyleUtils(); const styles = useThemeStyles(); const [isPickerVisible, setIsPickerVisible] = useState(false); diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts index 3f1eb101f879..b57c9d32061a 100644 --- a/src/components/ValuePicker/types.ts +++ b/src/components/ValuePicker/types.ts @@ -60,8 +60,6 @@ type ValuePickerProps = { /** Whether to show the tooltip text */ shouldShowTooltips?: boolean; - - selectedCurrency?: string; }; export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps, ValuePickerListItem}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 83786661cbfe..1a91fa55ee91 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -122,10 +122,6 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator require('../../../../pages/home/report/ReportDetailsExportPage').default, }); -const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({ - [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default, -}); - const ReportSettingsModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default, [SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default, @@ -134,6 +130,10 @@ const ReportSettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Report/VisibilityPage').default, }); +const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({ + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default, +}); + const TaskModalStackNavigator = createModalStackNavigator({ [SCREENS.TASK.TITLE]: () => require('../../../../pages/tasks/TaskTitlePage').default, [SCREENS.TASK.ASSIGNEE]: () => require('../../../../pages/tasks/TaskAssigneeSelectorModal').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index a75b8a712b5e..a39a87bbc496 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -79,7 +79,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.NEW_CHAT} component={ModalStackNavigators.NewChatModalStackNavigator} /> - - + - + ['config'] = { path: ROUTES.KEYBOARD_SHORTCUTS, }, [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route, - // [SCREENS.WORKSPACE.CONFIRMATION]: {path: ROUTES.WORKSPACE_CONFIRMATION}, [SCREENS.SETTINGS.SHARE_CODE]: { path: ROUTES.SETTINGS_SHARE_CODE, }, @@ -951,11 +950,6 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DETAILS.EXPORT]: ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.route, }, }, - [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: { - screens: { - [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION, - }, - }, [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: { screens: { [SCREENS.REPORT_SETTINGS.ROOT]: { @@ -1102,7 +1096,6 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DESCRIPTION_ROOT]: ROUTES.REPORT_DESCRIPTION.route, }, }, - [SCREENS.RIGHT_MODAL.NEW_CHAT]: { screens: { [SCREENS.NEW_CHAT.ROOT]: { @@ -1129,6 +1122,11 @@ const config: LinkingOptions['config'] = { }, }, }, + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: { + screens: { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION, + }, + }, [SCREENS.RIGHT_MODAL.NEW_TASK]: { screens: { [SCREENS.NEW_TASK.ROOT]: ROUTES.NEW_TASK.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4b5844c764c0..87e2509c7ae2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -904,10 +904,6 @@ type NewChatNavigatorParamList = { [SCREENS.NEW_CHAT.NEW_CHAT_EDIT_NAME]: undefined; }; -type WorkspaceConfirmationNavigatorParamList = { - [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: undefined; -}; - type DetailsNavigatorParamList = { [SCREENS.DETAILS_ROOT]: { login: string; @@ -1178,6 +1174,10 @@ type MoneyRequestNavigatorParamList = { }; }; +type WorkspaceConfirmationNavigatorParamList = { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: undefined; +}; + type NewTaskNavigatorParamList = { [SCREENS.NEW_TASK.ROOT]: { backTo?: Routes; @@ -1351,10 +1351,10 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.EXPENSIFY_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DOMAIN_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1396c2cd8219..b0591d1ad42b 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -3,7 +3,6 @@ import CONST from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 6cbf6d5b0d9e..f2ceef9069fa 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -435,7 +435,6 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { - return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 4329386406df..7ac7abc278e9 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -402,7 +402,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt( * @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency?: '', file?: File) { +function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency = '', file?: File) { Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', currency, file); } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d5b9713782b1..12bb63e60a71 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1853,7 +1853,7 @@ function createWorkspace( policyName = '', policyID = generatePolicyID(), engagementChoice = '', - currency?: '', + currency = '', file?: File, ): CreateWorkspaceParams { const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, currency, file); diff --git a/src/types/form/index.ts b/src/types/form/index.ts index ddecc5cd634e..6e29c6b08784 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -85,3 +85,4 @@ export type {WorkspaceCompanyCardFeedName} from './WorkspaceCompanyCardFeedName' export type {SearchSavedSearchRenameForm} from './SearchSavedSearchRenameForm'; export type {WorkspaceCompanyCardEditName} from './WorkspaceCompanyCardEditName'; export type {PersonalDetailsForm} from './PersonalDetailsForm'; +export type {WorkspaceConfirmationForm} from './WorkspaceConfirmationForm'; From d442d2f825b590f64d137ca96adc258d2cfd7e12 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 14:24:35 +0530 Subject: [PATCH 012/264] minor updates. Signed-off-by: krishna2323 --- .../WorkspaceCardCreateAWorkspace.tsx | 5 +++-- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 7 +------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx b/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx index ecfdb0d83ace..bf250a063582 100644 --- a/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx +++ b/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx @@ -4,7 +4,8 @@ import * as Illustrations from '@components/Icon/Illustrations'; import Section, {CARD_LAYOUT} from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as App from '@userActions/App'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; function WorkspaceCardCreateAWorkspace() { const styles = useThemeStyles(); @@ -21,7 +22,7 @@ function WorkspaceCardCreateAWorkspace() { >