Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d8a1487
feat: add consolidated domain billing toggle to DomainAdminsSettingsP…
war-in Dec 12, 2025
720ecad
Merge branch 'war-in/domains/add-admins-settings-page' into war-in/do…
war-in Dec 15, 2025
28497e4
chore: post-merge fixes
war-in Dec 15, 2025
e357d06
Merge branch 'war-in/domains/add-admins-settings-page' into war-in/do…
war-in Dec 15, 2025
766378c
Merge branch 'refs/heads/war-in/domains/add-admins-settings-page' int…
war-in Dec 17, 2025
425e078
chore: post-merge fixes
war-in Dec 17, 2025
c720983
Merge branch 'refs/heads/main' into war-in/domains/add-consolidated-d…
war-in Dec 17, 2025
d313619
feat: add error handling to consolidated domain billing
war-in Dec 17, 2025
8d51a09
chore: fix prettier
war-in Dec 17, 2025
e280f3a
feat: reset error optimistically
war-in Dec 17, 2025
0f31199
chore: add spanish translations
war-in Dec 17, 2025
7286a15
chore: fix prettier
war-in Dec 17, 2025
28af275
chore: update translations
war-in Dec 17, 2025
1aaefbc
Merge branch 'main' into war-in/domains/add-consolidated-domain-billing
war-in Dec 18, 2025
87aee55
fix: get rid of infinite rerenders on DomainAdminsPage.tsx
war-in Dec 19, 2025
30eb49b
chore: fix lint
war-in Dec 19, 2025
895e001
Merge branch 'main' into war-in/domains/add-consolidated-domain-billing
war-in Dec 19, 2025
184543a
refactor: make DomainAdminsPage.tsx compilable again
war-in Dec 19, 2025
a166fe4
feat: fetch domain data after reconnect
war-in Dec 19, 2025
3ce261b
Merge branch 'main' into war-in/domains/add-consolidated-domain-billing
war-in Dec 19, 2025
93df0fb
Merge branch 'refs/heads/main' into war-in/domains/add-consolidated-d…
war-in Dec 22, 2025
f8492e8
refactor: support `strong` in all languages
war-in Dec 22, 2025
146cba8
refactor: rename command
war-in Dec 22, 2025
5e20e45
test: add technicalContactSettingsSelector unit tests
war-in Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Wenn diese Option aktiviert ist, bezahlt der Hauptansprechpartner für alle Workspaces, die Mitgliedern von <strong>${domainName}</strong> gehören, und erhält alle Rechnungsbelege.</muted-text-label></comment>`,
consolidatedDomainBillingError: 'Die konsolidierte Domain-Abrechnung konnte nicht geändert werden. Bitte versuche es später erneut.',
},
},
desktopAppRetiredPage: {
title: 'Desktop-App wurde eingestellt',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7843,6 +7843,10 @@ const translations = {
primaryContact: 'Primary contact',
addPrimaryContact: 'Add primary contact',
settings: 'Settings',
consolidatedDomainBilling: 'Consolidated domain billing',
consolidatedDomainBillingDescription: (domainName: string) =>
`<comment><muted-text-label>When enabled, the primary contact will pay for all workspaces owned by <strong>${domainName}</strong> members and receive all billing receipts.</muted-text-label></comment>`,
consolidatedDomainBillingError: "Consolidated domain billing couldn't be changed. Please try again later.",
},
},
};
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Cuando está habilitada, el contacto principal pagará todos los espacios de trabajo propiedad de los miembros de <strong>${domainName}</strong> y recibirá todos los recibos de facturación.</muted-text-label></comment>`,
consolidatedDomainBillingError: 'No se pudo cambiar la facturación consolidada del dominio. Por favor, inténtalo de nuevo más tarde.',
},
},
};
Expand Down
12 changes: 11 additions & 1 deletion src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Lorsque cette option est activée, le contact principal paiera pour tous les espaces de travail appartenant aux membres de <strong>${domainName}</strong> et recevra tous les reçus de facturation.</muted-text-label></comment>`,
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',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Quando abilitata, il contatto principale pagherà per tutti gli spazi di lavoro di proprietà dei membri di <strong>${domainName}</strong> e riceverà tutte le ricevute di fatturazione.</muted-text-label></comment>`,
consolidatedDomainBillingError: 'La fatturazione dominio consolidata non può essere modificata. Riprova più tardi.',
},
},
desktopAppRetiredPage: {
Expand Down
12 changes: 11 additions & 1 deletion src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7903,7 +7903,17 @@ Expensify の使い方をお見せするための*テストレシート*がこ
subtitle: 'ドメインのメンバーにシングルサインオンでのログインを必須化し、ワークスペースの作成を制限するなど、さらに多くのことができます。',
enable: '有効にする',
},
admins: {title: '管理者', findAdmin: '管理者を検索', primaryContact: '主要連絡先', addPrimaryContact: '主要連絡先を追加', settings: '設定'},
admins: {
title: '管理者',
findAdmin: '管理者を検索',
primaryContact: '主要連絡先',
addPrimaryContact: '主要連絡先を追加',
settings: '設定',
consolidatedDomainBilling: '統合ドメイン請求',
consolidatedDomainBillingDescription: (domainName: string) =>
`<comment><muted-text-label>有効にすると、<strong>${domainName}</strong> メンバーが所有するすべてのワークスペースの支払いを代表連絡先が行い、すべての請求書の領収書を受け取ります。</muted-text-label></comment>`,
consolidatedDomainBillingError: '統合ドメイン請求を変更できませんでした。後でもう一度お試しください。',
},
},
desktopAppRetiredPage: {
title: 'デスクトップアプリは廃止されました',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Indien ingeschakeld, betaalt de primaire contactpersoon voor alle werkruimten die eigendom zijn van leden van <strong>${domainName}</strong> en ontvangt hij/zij alle factuurbewijzen.</muted-text-label></comment>`,
consolidatedDomainBillingError: 'Geconsolideerde domeinfacturering kon niet worden gewijzigd. Probeer het later opnieuw.',
},
},
desktopAppRetiredPage: {
Expand Down
12 changes: 11 additions & 1 deletion src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Gdy ta opcja jest włączona, główny kontakt będzie opłacać wszystkie przestrzenie robocze należące do członków <strong>${domainName}</strong> i otrzymywać wszystkie potwierdzenia rozliczeń.</muted-text-label></comment>`,
consolidatedDomainBillingError: 'Nie udało się zmienić zbiorczego rozliczania domeny. Spróbuj ponownie później.',
},
},
desktopAppRetiredPage: {
title: 'Aplikacja desktopowa została wycofana',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>Quando ativado, o contato principal pagará por todos os espaços de trabalho pertencentes aos membros de <strong>${domainName}</strong> e receberá todos os recibos de cobrança.</muted-text-label></comment>`,
consolidatedDomainBillingError: 'A cobrança de domínio consolidada não pôde ser alterada. Tente novamente mais tarde.',
},
},
desktopAppRetiredPage: {
Expand Down
12 changes: 11 additions & 1 deletion src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
`<comment><muted-text-label>启用后,主要联系人将为<strong>${domainName}</strong>成员拥有的所有工作区付款,并接收所有账单收据。</muted-text-label></comment>`,
consolidatedDomainBillingError: '无法更改合并域账单。请稍后重试。',
},
},
desktopAppRetiredPage: {title: '桌面应用程序已停用', body: '新的 Expensify Mac 桌面应用已停用。今后,请使用网页版应用访问您的账户。', goToWeb: '前往网页'},
};
Expand Down
Comment thread
war-in marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type ToggleConsolidatedDomainBillingParams = {
authToken?: string | null;
domainAccountID: number;
domainName: string;
enabled: boolean;
};

export default ToggleConsolidatedDomainBillingParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof WRITE_COMMANDS>;
Expand Down Expand Up @@ -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 = {
Expand Down
89 changes: 88 additions & 1 deletion src/libs/actions/Domain.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -432,4 +517,6 @@ export {
resetCreateDomainForm,
setPrimaryContact,
clearSetPrimaryContactError,
toggleConsolidatedDomainBilling,
clearToggleConsolidatedDomainBillingErrors,
};
14 changes: 7 additions & 7 deletions src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 ?? '',
Expand Down Expand Up @@ -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));
}}
Expand Down
Loading
Loading