diff --git a/src/languages/de.ts b/src/languages/de.ts
index f1ad016d4cd7..180fd3eb5b3e 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7984,7 +7984,17 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
subtitle: 'Erzwingen Sie für Mitglieder Ihrer Domain die Anmeldung per Single Sign-On, schränken Sie die Erstellung von Workspaces ein und vieles mehr.',
enable: 'Aktivieren',
},
- admins: {title: 'Admins', findAdmin: 'Admin finden', primaryContact: 'Hauptansprechpartner', addPrimaryContact: 'Primären Kontakt hinzufügen', settings: 'Einstellungen'},
+ admins: {
+ title: 'Admins',
+ findAdmin: 'Admin finden',
+ primaryContact: 'Hauptansprechpartner',
+ addPrimaryContact: 'Primären Kontakt hinzufügen',
+ settings: 'Einstellungen',
+ consolidatedDomainBilling: 'Konsolidierte Domain-Abrechnung',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Wenn diese Option aktiviert ist, bezahlt der Hauptansprechpartner für alle Workspaces, die Mitgliedern von ${domainName} gehören, und erhält alle Rechnungsbelege.`,
+ consolidatedDomainBillingError: 'Die konsolidierte Domain-Abrechnung konnte nicht geändert werden. Bitte versuche es später erneut.',
+ },
},
desktopAppRetiredPage: {
title: 'Desktop-App wurde eingestellt',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 3872c29d93eb..2c112324da6b 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7843,6 +7843,10 @@ const translations = {
primaryContact: 'Primary contact',
addPrimaryContact: 'Add primary contact',
settings: 'Settings',
+ consolidatedDomainBilling: 'Consolidated domain billing',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `When enabled, the primary contact will pay for all workspaces owned by ${domainName} members and receive all billing receipts.`,
+ consolidatedDomainBillingError: "Consolidated domain billing couldn't be changed. Please try again later.",
},
},
};
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c226ae1f0b42..dcfbd2e11717 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -8013,6 +8013,10 @@ ${amount} para ${merchant} - ${date}`,
primaryContact: 'Contacto principal',
addPrimaryContact: 'Añadir contacto principal',
settings: 'Configuración',
+ consolidatedDomainBilling: 'Facturación consolidada del dominio',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Cuando está habilitada, el contacto principal pagará todos los espacios de trabajo propiedad de los miembros de ${domainName} y recibirá todos los recibos de facturación.`,
+ consolidatedDomainBillingError: 'No se pudo cambiar la facturación consolidada del dominio. Por favor, inténtalo de nuevo más tarde.',
},
},
};
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index af035a665cbe..7269eb74c0a7 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7988,7 +7988,17 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
subtitle: "Exiger que les membres de votre domaine se connectent via l'authentification unique, restreindre la création d'espaces de travail, et plus encore.",
enable: 'Activer',
},
- admins: {title: 'Admins', findAdmin: 'Trouver un admin', primaryContact: 'Contact principal', addPrimaryContact: 'Ajouter un contact principal', settings: 'Paramètres'},
+ admins: {
+ title: 'Admins',
+ findAdmin: 'Trouver un admin',
+ primaryContact: 'Contact principal',
+ addPrimaryContact: 'Ajouter un contact principal',
+ settings: 'Paramètres',
+ consolidatedDomainBilling: 'Facturation consolidée du domaine',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Lorsque cette option est activée, le contact principal paiera pour tous les espaces de travail appartenant aux membres de ${domainName} et recevra tous les reçus de facturation.`,
+ consolidatedDomainBillingError: 'La facturation de domaine consolidée n’a pas pu être modifiée. Veuillez réessayer plus tard.',
+ },
},
desktopAppRetiredPage: {
title: 'L’application de bureau a été retirée',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 852b6046e76c..bf34454d4c01 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7971,6 +7971,10 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
primaryContact: 'Contatto principale',
addPrimaryContact: 'Aggiungi contatto principale',
settings: 'Impostazioni',
+ consolidatedDomainBilling: 'Fatturazione consolidata del dominio',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Quando abilitata, il contatto principale pagherà per tutti gli spazi di lavoro di proprietà dei membri di ${domainName} e riceverà tutte le ricevute di fatturazione.`,
+ consolidatedDomainBillingError: 'La fatturazione dominio consolidata non può essere modificata. Riprova più tardi.',
},
},
desktopAppRetiredPage: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 502acbe488db..ca1e2cb72d1b 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7903,7 +7903,17 @@ Expensify の使い方をお見せするための*テストレシート*がこ
subtitle: 'ドメインのメンバーにシングルサインオンでのログインを必須化し、ワークスペースの作成を制限するなど、さらに多くのことができます。',
enable: '有効にする',
},
- admins: {title: '管理者', findAdmin: '管理者を検索', primaryContact: '主要連絡先', addPrimaryContact: '主要連絡先を追加', settings: '設定'},
+ admins: {
+ title: '管理者',
+ findAdmin: '管理者を検索',
+ primaryContact: '主要連絡先',
+ addPrimaryContact: '主要連絡先を追加',
+ settings: '設定',
+ consolidatedDomainBilling: '統合ドメイン請求',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `有効にすると、${domainName} メンバーが所有するすべてのワークスペースの支払いを代表連絡先が行い、すべての請求書の領収書を受け取ります。`,
+ consolidatedDomainBillingError: '統合ドメイン請求を変更できませんでした。後でもう一度お試しください。',
+ },
},
desktopAppRetiredPage: {
title: 'デスクトップアプリは廃止されました',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ad597b75837b..8625581cbe29 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7957,6 +7957,10 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`,
primaryContact: 'Primair contactpersoon',
addPrimaryContact: 'Primair contactpersoon toevoegen',
settings: 'Instellingen',
+ consolidatedDomainBilling: 'Geconsolideerde domeinfacturering',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Indien ingeschakeld, betaalt de primaire contactpersoon voor alle werkruimten die eigendom zijn van leden van ${domainName} en ontvangt hij/zij alle factuurbewijzen.`,
+ consolidatedDomainBillingError: 'Geconsolideerde domeinfacturering kon niet worden gewijzigd. Probeer het later opnieuw.',
},
},
desktopAppRetiredPage: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 82c2c30f5887..198eb6aa1c9b 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7936,7 +7936,17 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`,
subtitle: 'Wymagaj, aby członkowie Twojej domeny logowali się przez Single Sign-On (SSO), ograniczaj tworzenie obszarów roboczych i nie tylko.',
enable: 'Włącz',
},
- admins: {title: 'Administratorzy', findAdmin: 'Znajdź administratora', primaryContact: 'Główny kontakt', addPrimaryContact: 'Dodaj główny kontakt', settings: 'Ustawienia'},
+ admins: {
+ title: 'Administratorzy',
+ findAdmin: 'Znajdź administratora',
+ primaryContact: 'Główny kontakt',
+ addPrimaryContact: 'Dodaj główny kontakt',
+ settings: 'Ustawienia',
+ consolidatedDomainBilling: 'Skonsolidowane rozliczanie domen',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Gdy ta opcja jest włączona, główny kontakt będzie opłacać wszystkie przestrzenie robocze należące do członków ${domainName} i otrzymywać wszystkie potwierdzenia rozliczeń.`,
+ consolidatedDomainBillingError: 'Nie udało się zmienić zbiorczego rozliczania domeny. Spróbuj ponownie później.',
+ },
},
desktopAppRetiredPage: {
title: 'Aplikacja desktopowa została wycofana',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 1a4b3bda74a9..9398e92783fa 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7948,6 +7948,10 @@ Aqui está um *recibo de teste* para mostrar como funciona:`,
primaryContact: 'Contato principal',
addPrimaryContact: 'Adicionar contato principal',
settings: 'Configurações',
+ consolidatedDomainBilling: 'Cobrança consolidada de domínio',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `Quando ativado, o contato principal pagará por todos os espaços de trabalho pertencentes aos membros de ${domainName} e receberá todos os recibos de cobrança.`,
+ consolidatedDomainBillingError: 'A cobrança de domínio consolidada não pôde ser alterada. Tente novamente mais tarde.',
},
},
desktopAppRetiredPage: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index f77152ea8019..f53f73574eb6 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -7766,7 +7766,17 @@ ${reportName}
addDomain: {title: '添加域', subtitle: '请输入您想访问的私有域名(例如:expensify.com)。', domainName: '域名', newDomain: '新域名'},
domainAdded: {title: '已添加域名', description: '接下来,您需要验证域名的所有权并调整您的安全设置。', configure: '配置'},
enhancedSecurity: {title: '增强的安全性', subtitle: '要求您域内的成员使用单点登录登录、限制工作区创建等。', enable: '启用'},
- admins: {title: '管理员', findAdmin: '查找管理员', primaryContact: '主要联系人', addPrimaryContact: '添加主要联系人', settings: '设置'},
+ admins: {
+ title: '管理员',
+ findAdmin: '查找管理员',
+ primaryContact: '主要联系人',
+ addPrimaryContact: '添加主要联系人',
+ settings: '设置',
+ consolidatedDomainBilling: '合并域名结算',
+ consolidatedDomainBillingDescription: (domainName: string) =>
+ `启用后,主要联系人将为${domainName}成员拥有的所有工作区付款,并接收所有账单收据。`,
+ consolidatedDomainBillingError: '无法更改合并域账单。请稍后重试。',
+ },
},
desktopAppRetiredPage: {title: '桌面应用程序已停用', body: '新的 Expensify Mac 桌面应用已停用。今后,请使用网页版应用访问您的账户。', goToWeb: '前往网页'},
};
diff --git a/src/libs/API/parameters/ToggleConsolidatedDomainBillingParams.ts b/src/libs/API/parameters/ToggleConsolidatedDomainBillingParams.ts
new file mode 100644
index 000000000000..ced9e6aff56f
--- /dev/null
+++ b/src/libs/API/parameters/ToggleConsolidatedDomainBillingParams.ts
@@ -0,0 +1,8 @@
+type ToggleConsolidatedDomainBillingParams = {
+ authToken?: string | null;
+ domainAccountID: number;
+ domainName: string;
+ enabled: boolean;
+};
+
+export default ToggleConsolidatedDomainBillingParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 7694afe03367..b3c1fbf3ec48 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -450,3 +450,4 @@ export type {default as UpdateSamlEnabledParams} from './UpdateSamlEnabledParams
export type {default as UpdateSamlRequiredParams} from './UpdateSamlRequiredParams';
export type {default as SetPolicyRequireCompanyCardsEnabledParams} from './SetPolicyRequireCompanyCardsEnabled';
export type {default as SetTechnicalContactEmailParams} from './SetTechnicalContactEmailParams';
+export type {default as ToggleConsolidatedDomainBillingParams} from './ToggleConsolidatedDomainBillingParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 114286123757..b162c99622f4 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -528,6 +528,7 @@ const WRITE_COMMANDS = {
UPDATE_SAML_REQUIRED: 'UpdateSAMLRequired',
CREATE_DOMAIN: 'CreateDomain',
SET_TECHNICAL_CONTACT_EMAIL: 'SetTechnicalContactEmail',
+ TOGGLE_CONSOLIDATED_DOMAIN_BILLING: 'ToggleConsolidatedDomainBilling',
} as const;
type WriteCommand = ValueOf;
@@ -1077,6 +1078,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_SAML_REQUIRED]: Parameters.UpdateSamlRequiredParams;
[WRITE_COMMANDS.CREATE_DOMAIN]: Parameters.DomainParams;
[WRITE_COMMANDS.SET_TECHNICAL_CONTACT_EMAIL]: Parameters.SetTechnicalContactEmailParams;
+ [WRITE_COMMANDS.TOGGLE_CONSOLIDATED_DOMAIN_BILLING]: Parameters.ToggleConsolidatedDomainBillingParams;
};
const READ_COMMANDS = {
diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts
index 51d553f1d841..6ffe3610cdef 100644
--- a/src/libs/actions/Domain.ts
+++ b/src/libs/actions/Domain.ts
@@ -1,7 +1,7 @@
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
-import type {SetTechnicalContactEmailParams} from '@libs/API/parameters';
+import type {SetTechnicalContactEmailParams, ToggleConsolidatedDomainBillingParams} from '@libs/API/parameters';
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils';
import {getAuthToken} from '@libs/Network/NetworkStore';
@@ -416,6 +416,91 @@ function clearSetPrimaryContactError(domainAccountID: number) {
});
}
+function toggleConsolidatedDomainBilling(domainAccountID: number, domainName: string, useTechnicalContactBillingCard: boolean) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
+ value: {
+ settings: {
+ useTechnicalContactBillingCard,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
+ value: {
+ useTechnicalContactBillingCard: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`,
+ value: {
+ useTechnicalContactBillingCardErrors: null,
+ },
+ },
+ ];
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
+ value: {
+ useTechnicalContactBillingCard: null,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`,
+ value: {
+ useTechnicalContactBillingCardErrors: null,
+ },
+ },
+ ];
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
+ value: {
+ settings: {
+ useTechnicalContactBillingCard: !useTechnicalContactBillingCard,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`,
+ value: {
+ useTechnicalContactBillingCardErrors: getMicroSecondOnyxErrorWithTranslationKey('domain.admins.consolidatedDomainBillingError'),
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
+ value: {
+ useTechnicalContactBillingCard: null,
+ },
+ },
+ ];
+
+ const authToken = getAuthToken();
+ const params: ToggleConsolidatedDomainBillingParams = {
+ authToken,
+ domainAccountID,
+ domainName,
+ enabled: useTechnicalContactBillingCard,
+ };
+
+ API.write(WRITE_COMMANDS.TOGGLE_CONSOLIDATED_DOMAIN_BILLING, params, {optimisticData, failureData, successData});
+}
+
+function clearToggleConsolidatedDomainBillingErrors(domainAccountID: number) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, {
+ useTechnicalContactBillingCardErrors: null,
+ });
+}
+
export {
getDomainValidationCode,
validateDomain,
@@ -432,4 +517,6 @@ export {
resetCreateDomainForm,
setPrimaryContact,
clearSetPrimaryContactError,
+ toggleConsolidatedDomainBilling,
+ clearToggleConsolidatedDomainBillingErrors,
};
diff --git a/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx b/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx
index 9aa5a0c7506a..fd8271c1747e 100644
--- a/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx
+++ b/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx
@@ -1,4 +1,4 @@
-import {adminAccountIDsSelector, technicalContactEmailSelector} from '@selectors/Domain';
+import {adminAccountIDsSelector, technicalContactSettingsSelector} from '@selectors/Domain';
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -54,20 +54,20 @@ function DomainAddPrimaryContactPage({route}: DomainAddPrimaryContactPageProps)
});
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
- const [technicalContactEmail] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
+ const [technicalContactSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
canBeMissing: false,
- selector: technicalContactEmailSelector,
+ selector: technicalContactSettingsSelector,
});
let technicalContactEmailKey: string | undefined;
const data: AdminOption[] = [];
for (const accountID of adminAccountIDs ?? []) {
const details = personalDetails?.[accountID];
- if (details?.login === technicalContactEmail) {
+ if (details?.login === technicalContactSettings?.technicalContactEmail) {
technicalContactEmailKey = String(accountID);
}
data.push({
- isSelected: details?.login === technicalContactEmail,
+ isSelected: details?.login === technicalContactSettings?.technicalContactEmail,
keyForList: String(accountID),
accountID,
login: details?.login ?? '',
@@ -104,8 +104,8 @@ function DomainAddPrimaryContactPage({route}: DomainAddPrimaryContactPageProps)
if (!option.login || !option.accountID) {
return;
}
- if (option.login !== technicalContactEmail) {
- setPrimaryContact(domainAccountID, option.accountID, option.login, technicalContactEmail);
+ if (option.login !== technicalContactSettings?.technicalContactEmail) {
+ setPrimaryContact(domainAccountID, option.accountID, option.login, technicalContactSettings?.technicalContactEmail);
}
Navigation.goBack(ROUTES.DOMAIN_ADMINS_SETTINGS.getRoute(domainAccountID));
}}
diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx
index 297e92e7d0a3..2a8e1675ba43 100644
--- a/src/pages/domain/Admins/DomainAdminsPage.tsx
+++ b/src/pages/domain/Admins/DomainAdminsPage.tsx
@@ -1,4 +1,4 @@
-import {adminAccountIDsSelector, technicalContactEmailSelector} from '@selectors/Domain';
+import {adminAccountIDsSelector, technicalContactSettingsSelector} from '@selectors/Domain';
import React from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
@@ -53,9 +53,9 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) {
selector: adminAccountIDsSelector,
});
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true});
- const [technicalContactEmail] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
+ const [technicalContactSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
canBeMissing: false,
- selector: technicalContactEmailSelector,
+ selector: technicalContactSettingsSelector,
});
const currentUserAccountID = getCurrentUserAccountID();
@@ -64,6 +64,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) {
const data: AdminOption[] = [];
for (const accountID of adminAccountIDs ?? []) {
const details = personalDetails?.[accountID];
+ const isPrimaryContact = technicalContactSettings?.technicalContactEmail === details?.login;
data.push({
keyForList: String(accountID),
accountID,
@@ -78,7 +79,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) {
id: accountID,
},
],
- rightElement: technicalContactEmail === details?.login && ,
+ rightElement: isPrimaryContact && ,
});
}
diff --git a/src/pages/domain/Admins/DomainAdminsSettingsPage.tsx b/src/pages/domain/Admins/DomainAdminsSettingsPage.tsx
index 9506a2e796dc..d2ca4b2a5daf 100644
--- a/src/pages/domain/Admins/DomainAdminsSettingsPage.tsx
+++ b/src/pages/domain/Admins/DomainAdminsSettingsPage.tsx
@@ -1,17 +1,22 @@
-import {technicalContactEmailSelector} from '@selectors/Domain';
+import {technicalContactSettingsSelector} from '@selectors/Domain';
+import {Str} from 'expensify-common';
import React from 'react';
+import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import RenderHTML from '@components/RenderHTML';
import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
import {getLatestError} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@navigation/types';
import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper';
-import {clearSetPrimaryContactError} from '@userActions/Domain';
+import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import {clearSetPrimaryContactError, clearToggleConsolidatedDomainBillingErrors, toggleConsolidatedDomainBilling} from '@userActions/Domain';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -22,6 +27,7 @@ function DomainAdminsSettingsPage({route}: DomainAdminsSettingsPageProps) {
const {domainAccountID} = route.params;
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const [domainPendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, {
canBeMissing: true,
@@ -29,10 +35,11 @@ function DomainAdminsSettingsPage({route}: DomainAdminsSettingsPageProps) {
const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, {
canBeMissing: true,
});
- const [technicalContactEmail] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
+ const [technicalContactSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
canBeMissing: false,
- selector: technicalContactEmailSelector,
+ selector: technicalContactSettingsSelector,
});
+ const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true});
return (
@@ -55,11 +62,33 @@ function DomainAdminsSettingsPage({route}: DomainAdminsSettingsPageProps) {
>
Navigation.navigate(ROUTES.DOMAIN_ADD_PRIMARY_CONTACT.getRoute(domainAccountID))}
/>
+ {
+ if (!domain?.email) {
+ return;
+ }
+ toggleConsolidatedDomainBilling(domainAccountID, Str.extractEmailDomain(domain.email), value);
+ }}
+ title={translate('domain.admins.consolidatedDomainBilling')}
+ subtitle={
+
+
+
+ }
+ shouldPlaceSubtitleBelowSwitch
+ pendingAction={domainPendingActions?.useTechnicalContactBillingCard}
+ errors={getLatestError(domainErrors?.useTechnicalContactBillingCardErrors)}
+ onCloseError={() => clearToggleConsolidatedDomainBillingErrors(domainAccountID)}
+ />
);
diff --git a/src/pages/domain/DomainInitialPage.tsx b/src/pages/domain/DomainInitialPage.tsx
index ef2cb6080cc0..a46df07b1440 100644
--- a/src/pages/domain/DomainInitialPage.tsx
+++ b/src/pages/domain/DomainInitialPage.tsx
@@ -1,6 +1,6 @@
import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
import {Str} from 'expensify-common';
-import React, {useEffect, useMemo} from 'react';
+import React, {useCallback, useEffect, useMemo} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -12,6 +12,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSingleExecution from '@hooks/useSingleExecution';
@@ -72,13 +73,19 @@ function DomainInitialPage({route}: DomainInitialPageProps) {
return menuItems;
}, [domainAccountID, singleExecution, waitForNavigate, icons.UserLock]);
- useEffect(() => {
+ const fetchDomainData = useCallback(() => {
if (!domainName) {
return;
}
openDomainInitialPage(domainName);
}, [domainName]);
+ useEffect(() => {
+ fetchDomainData();
+ }, [fetchDomainData]);
+
+ useNetwork({onReconnect: fetchDomainData});
+
useEffect(() => {
confirmReadyToOpenApp();
}, []);
diff --git a/src/selectors/Domain.ts b/src/selectors/Domain.ts
index 536d7ebf06d0..6a177f251a09 100644
--- a/src/selectors/Domain.ts
+++ b/src/selectors/Domain.ts
@@ -46,6 +46,11 @@ function adminAccountIDsSelector(domain: OnyxEntry): number[] {
);
}
-const technicalContactEmailSelector = (domainMemberSharedNVP: OnyxEntry) => domainMemberSharedNVP?.settings?.technicalContactEmail;
-
-export {domainMemberSamlSettingsSelector, domainSamlSettingsStateSelector, domainNameSelector, metaIdentitySelector, adminAccountIDsSelector, technicalContactEmailSelector};
+const technicalContactSettingsSelector = (domainMemberSharedNVP: OnyxEntry) => {
+ return {
+ technicalContactEmail: domainMemberSharedNVP?.settings?.technicalContactEmail,
+ useTechnicalContactBillingCard: domainMemberSharedNVP?.settings?.useTechnicalContactBillingCard,
+ };
+};
+
+export {domainMemberSamlSettingsSelector, domainSamlSettingsStateSelector, domainNameSelector, metaIdentitySelector, adminAccountIDsSelector, technicalContactSettingsSelector};
diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts
index 678c3d06dba4..5c0b70783870 100644
--- a/src/types/onyx/CardFeeds.ts
+++ b/src/types/onyx/CardFeeds.ts
@@ -166,6 +166,9 @@ type CardFeeds = {
/** Email address of the technical contact for the domain */
technicalContactEmail?: string;
+
+ /** Whether to use the technical contact's billing card */
+ useTechnicalContactBillingCard?: boolean;
};
/** Whether we are loading the data via the API */
diff --git a/src/types/onyx/DomainErrors.ts b/src/types/onyx/DomainErrors.ts
index f4d4f59f7f79..740f3947e329 100644
--- a/src/types/onyx/DomainErrors.ts
+++ b/src/types/onyx/DomainErrors.ts
@@ -8,6 +8,11 @@ type DomainErrors = {
* Errors related to the technical contact email
*/
technicalContactEmailErrors?: OnyxCommon.Errors;
+
+ /**
+ * Errors related to the "use technical contact billing card" setting
+ */
+ useTechnicalContactBillingCardErrors?: OnyxCommon.Errors;
};
export default DomainErrors;
diff --git a/src/types/onyx/DomainPendingActions.ts b/src/types/onyx/DomainPendingActions.ts
index fd5edbab9f0a..ab632687e3fd 100644
--- a/src/types/onyx/DomainPendingActions.ts
+++ b/src/types/onyx/DomainPendingActions.ts
@@ -8,6 +8,11 @@ type DomainPendingAction = {
* Pending action for the technical contact email
*/
technicalContactEmail?: OnyxCommon.PendingAction;
+
+ /**
+ * Pending action for the "use technical contact billing card" setting
+ */
+ useTechnicalContactBillingCard?: OnyxCommon.PendingAction;
};
export default DomainPendingAction;
diff --git a/tests/unit/DomainSelectorsTest.ts b/tests/unit/DomainSelectorsTest.ts
index 3be6dcac3227..18be3f394c04 100644
--- a/tests/unit/DomainSelectorsTest.ts
+++ b/tests/unit/DomainSelectorsTest.ts
@@ -1,7 +1,7 @@
-import {adminAccountIDsSelector} from '@selectors/Domain';
+import {adminAccountIDsSelector, technicalContactSettingsSelector} from '@selectors/Domain';
import type {OnyxEntry} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Domain} from '@src/types/onyx';
+import type {CardFeeds, Domain} from '@src/types/onyx';
describe('domainSelectors', () => {
describe('adminAccountIDsSelector', () => {
@@ -42,4 +42,60 @@ describe('domainSelectors', () => {
expect(adminAccountIDsSelector(domain)).toEqual([]);
});
});
+
+ describe('technicalContactSettingsSelector', () => {
+ it('Should return undefined values if the domain object is undefined', () => {
+ expect(technicalContactSettingsSelector(undefined)).toEqual({
+ technicalContactEmail: undefined,
+ useTechnicalContactBillingCard: undefined,
+ });
+ });
+
+ it('Should return undefined values if shared NVP is empty', () => {
+ const domainMemberSharedNVP = {} as OnyxEntry;
+
+ expect(technicalContactSettingsSelector(domainMemberSharedNVP)).toEqual({
+ technicalContactEmail: undefined,
+ useTechnicalContactBillingCard: undefined,
+ });
+ });
+
+ it('Should return technical contact settings when present', () => {
+ const domainMemberSharedNVP = {
+ settings: {
+ technicalContactEmail: 'tech@example.com',
+ useTechnicalContactBillingCard: true,
+ },
+ } as OnyxEntry;
+
+ expect(technicalContactSettingsSelector(domainMemberSharedNVP)).toEqual({
+ technicalContactEmail: 'tech@example.com',
+ useTechnicalContactBillingCard: true,
+ });
+ });
+
+ it('Should handle partial settings correctly', () => {
+ const domainMemberSharedNVP = {
+ settings: {
+ technicalContactEmail: 'tech@example.com',
+ },
+ } as OnyxEntry;
+
+ expect(technicalContactSettingsSelector(domainMemberSharedNVP)).toEqual({
+ technicalContactEmail: 'tech@example.com',
+ useTechnicalContactBillingCard: undefined,
+ });
+ });
+
+ it('Should return undefined values if settings are empty', () => {
+ const domainMemberSharedNVP = {
+ settings: {},
+ } as OnyxEntry;
+
+ expect(technicalContactSettingsSelector(domainMemberSharedNVP)).toEqual({
+ technicalContactEmail: undefined,
+ useTechnicalContactBillingCard: undefined,
+ });
+ });
+ });
});