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, + }); + }); + }); });