diff --git a/assets/images/simple-illustrations/simple-illustration__syncusers.svg b/assets/images/simple-illustrations/simple-illustration__syncusers.svg
new file mode 100644
index 000000000000..6611d1d7c46b
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__syncusers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/GustoSyncResultsModal.tsx b/src/components/GustoSyncResultsModal.tsx
new file mode 100644
index 000000000000..7f771b4bdb3c
--- /dev/null
+++ b/src/components/GustoSyncResultsModal.tsx
@@ -0,0 +1,112 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {GustoSyncResult} from '@libs/API/GustoSyncResult';
+import CONST from '@src/CONST';
+import Button from './Button';
+import FixedFooter from './FixedFooter';
+import HeaderWithBackButton from './HeaderWithBackButton';
+import Icon from './Icon';
+import Modal from './Modal';
+import type {ModalProps} from './Modal/Global/ModalContext';
+import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
+import ScrollView from './ScrollView';
+import Text from './Text';
+
+type GustoSyncResultsModalProps = ModalProps & {
+ /** Sync result returned by the completed Gusto sync job */
+ result: GustoSyncResult;
+};
+
+function GustoSyncResultsModal({result, closeModal}: GustoSyncResultsModalProps) {
+ const {translate} = useLocalize();
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const icons = useMemoizedLazyExpensifyIcons(['DownArrow']);
+ const illustrations = useMemoizedLazyIllustrations(['SyncUsers']);
+ const [isSkippedSectionExpanded, setIsSkippedSectionExpanded] = useState(false);
+
+ const addedCount = result.addedEmployeesCount ?? 0;
+ const removedCount = result.removedEmployeesCount ?? 0;
+ const skippedCount = result.skippedEmployees?.length ?? 0;
+ const closeResultsModal = () => closeModal();
+
+ const renderResultSummary = (label: string, count: number) => (
+
+ {label}
+ {translate('workspace.hr.gusto.syncResults.employeeCount', {count})}
+
+ );
+
+ return (
+
+
+
+
+
+
+
+ {translate('workspace.hr.gusto.syncResults.successTitle')}
+ {renderResultSummary(translate('workspace.hr.gusto.syncResults.added'), addedCount)}
+ {renderResultSummary(translate('workspace.hr.gusto.syncResults.removed'), removedCount)}
+ setIsSkippedSectionExpanded((isExpanded) => !isExpanded)}
+ style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}
+ >
+
+ {translate('workspace.hr.gusto.syncResults.skipped')}
+ {translate('workspace.hr.gusto.syncResults.employeeCount', {count: skippedCount})}
+
+
+
+ {isSkippedSectionExpanded &&
+ result.skippedEmployees?.map((employee) => (
+
+ {employee.name}
+ {employee.reason}
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+export default GustoSyncResultsModal;
diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts
index d44be8710bc9..f3d5ed5ccb6e 100644
--- a/src/components/Icon/chunks/illustrations.chunk.ts
+++ b/src/components/Icon/chunks/illustrations.chunk.ts
@@ -186,6 +186,7 @@ import Stopwatch from '@assets/images/simple-illustrations/simple-illustration__
import SubscriptionAnnual from '@assets/images/simple-illustrations/simple-illustration__subscription-annual.svg';
import SubscriptionPPU from '@assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg';
import Sunglasses from '@assets/images/simple-illustrations/simple-illustration__sunglasses.svg';
+import SyncUsers from '@assets/images/simple-illustrations/simple-illustration__syncusers.svg';
import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg';
import Target from '@assets/images/simple-illustrations/simple-illustration__target.svg';
import TeachersUnite from '@assets/images/simple-illustrations/simple-illustration__teachers-unite.svg';
@@ -391,6 +392,7 @@ const Illustrations = {
LockOpen,
Mailbox,
NewUser,
+ SyncUsers,
ShieldYellow,
Clock,
Members,
diff --git a/src/hooks/useGustoSyncResultsModal.ts b/src/hooks/useGustoSyncResultsModal.ts
new file mode 100644
index 000000000000..35d9fc61b134
--- /dev/null
+++ b/src/hooks/useGustoSyncResultsModal.ts
@@ -0,0 +1,45 @@
+import {useEffect} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import GustoSyncResultsModal from '@components/GustoSyncResultsModal';
+import {useModal} from '@components/Modal/Global/ModalContext';
+import CONST from '@src/CONST';
+import type {PolicyConnectionSyncProgress} from '@src/types/onyx/Policy';
+import usePrevious from './usePrevious';
+
+function useGustoSyncResultsModal(policyID: string, connectionSyncProgress: OnyxEntry, isFocused: boolean) {
+ const modal = useModal();
+ const previousSyncProgress = usePrevious(connectionSyncProgress);
+
+ useEffect(() => {
+ const syncResult = connectionSyncProgress?.result;
+ const isGustoSyncDoneWithResult =
+ connectionSyncProgress?.connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO &&
+ connectionSyncProgress?.stageInProgress === CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE &&
+ !!syncResult;
+ const didTransitionToJobDone =
+ previousSyncProgress?.connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO && previousSyncProgress?.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE;
+ const didGustoSyncComplete = isFocused && isGustoSyncDoneWithResult && didTransitionToJobDone;
+
+ if (!didGustoSyncComplete || !syncResult) {
+ return;
+ }
+
+ modal.showModal({
+ component: GustoSyncResultsModal,
+ props: {result: syncResult},
+ id: `gusto-sync-results-${policyID}`,
+ });
+ }, [
+ connectionSyncProgress?.connectionName,
+ connectionSyncProgress?.result,
+ connectionSyncProgress?.stageInProgress,
+ connectionSyncProgress?.timestamp,
+ isFocused,
+ policyID,
+ previousSyncProgress?.connectionName,
+ previousSyncProgress?.stageInProgress,
+ modal,
+ ]);
+}
+
+export default useGustoSyncResultsModal;
diff --git a/src/languages/de.ts b/src/languages/de.ts
index c1686e06c257..6c8b8bb46c71 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7106,6 +7106,17 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
syncError: 'Verbindung zu Gusto nicht möglich',
disconnectTitle: 'Gusto trennen',
disconnectPrompt: 'Bist du sicher, dass du Gusto trennen möchtest?',
+ syncResults: {
+ title: 'Gusto-Synchronisierungsergebnisse',
+ successTitle: 'Ihre Gusto-Verbindung wurde erfolgreich synchronisiert!',
+ added: 'Hinzugefügt',
+ removed: 'Entfernt',
+ skipped: 'Übersprungen',
+ employeeCount: () => ({
+ one: '1 Mitarbeiter',
+ other: (count: number) => `${count} Mitarbeitende`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index ace5662b11f6..d0b09ada8af4 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -6465,6 +6465,17 @@ const translations = {
description: 'I’ll manually setup approval workflows in Expensify.',
},
},
+ syncResults: {
+ title: 'Gusto sync results',
+ successTitle: 'Successfully synced your Gusto connection!',
+ added: 'Added',
+ removed: 'Removed',
+ skipped: 'Skipped',
+ employeeCount: () => ({
+ one: '1 employee',
+ other: (count: number) => `${count} employees`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 05cd02bcd091..6c76b7bbe3a4 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -6287,6 +6287,17 @@ ${amount} para ${merchant} - ${date}`,
description: 'Configuraré manualmente los flujos de aprobación en Expensify.',
},
},
+ syncResults: {
+ title: 'Resultados de la sincronización de Gusto',
+ successTitle: '¡Se sincronizó correctamente tu conexión con Gusto!',
+ added: 'Añadido',
+ removed: 'Eliminado',
+ skipped: 'Omitido',
+ employeeCount: () => ({
+ one: '1 empleado',
+ other: (count: number) => `${count} empleados`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index bc007f558a55..127c39394cf0 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7128,6 +7128,17 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
syncError: 'Impossible de se connecter à Gusto',
disconnectTitle: 'Déconnecter Gusto',
disconnectPrompt: 'Voulez-vous vraiment déconnecter Gusto ?',
+ syncResults: {
+ title: 'Résultats de la synchronisation Gusto',
+ successTitle: 'Connexion Gusto synchronisée avec succès !',
+ added: 'Ajouté',
+ removed: 'Supprimé',
+ skipped: 'Ignoré',
+ employeeCount: () => ({
+ one: '1 employé',
+ other: (count: number) => `${count} employés`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 64ecc83a7d75..70b07a0e3ad0 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7092,6 +7092,17 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
syncError: 'Impossibile connettersi a Gusto',
disconnectTitle: 'Disconnetti Gusto',
disconnectPrompt: 'Sei sicuro di voler disconnettere Gusto?',
+ syncResults: {
+ title: 'Risultati sincronizzazione Gusto',
+ successTitle: 'Connessione a Gusto sincronizzata con successo!',
+ added: 'Aggiunto',
+ removed: 'Rimosso',
+ skipped: 'Saltato',
+ employeeCount: () => ({
+ one: '1 dipendente',
+ other: (count: number) => `${count} dipendenti`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index d98ad07a35ca..21ca2e04db81 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7013,6 +7013,17 @@ ${reportName}
syncError: 'Gusto に接続できません',
disconnectTitle: 'Gusto の接続を解除',
disconnectPrompt: 'Gusto との接続を本当に解除しますか?',
+ syncResults: {
+ title: 'Gusto 同期結果',
+ successTitle: 'Gusto との連携が正常に同期されました!',
+ added: '追加済み',
+ removed: '削除済み',
+ skipped: 'スキップ済み',
+ employeeCount: () => ({
+ one: '1 従業員',
+ other: (count: number) => `${count} 従業員`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 6afa4c4fcabf..3faa1dbc7b34 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7068,6 +7068,17 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
syncError: 'Kan geen verbinding maken met Gusto',
disconnectTitle: 'Verbinding met Gusto verbreken',
disconnectPrompt: 'Weet je zeker dat je de verbinding met Gusto wilt verbreken?',
+ syncResults: {
+ title: 'Gusto-synchronisatieresultaten',
+ successTitle: 'Je Gusto-verbinding is succesvol gesynchroniseerd!',
+ added: 'Toegevoegd',
+ removed: 'Verwijderd',
+ skipped: 'Overgeslagen',
+ employeeCount: () => ({
+ one: '1 werknemer',
+ other: (count: number) => `${count} medewerkers`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 5c255612c907..3ec574604795 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7063,6 +7063,17 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
syncError: 'Nie można połączyć się z Gusto',
disconnectTitle: 'Odłącz Gusto',
disconnectPrompt: 'Czy na pewno chcesz odłączyć Gusto?',
+ syncResults: {
+ title: 'Wyniki synchronizacji Gusto',
+ successTitle: 'Pomyślnie zsynchronizowano Twoje połączenie z Gusto!',
+ added: 'Dodano',
+ removed: 'Usunięto',
+ skipped: 'Pominięto',
+ employeeCount: () => ({
+ one: '1 pracownik',
+ other: (count: number) => `${count} pracownicy`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 2cdf87c7a691..87375026294c 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7069,6 +7069,17 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
syncError: 'Não é possível conectar ao Gusto',
disconnectTitle: 'Desconectar Gusto',
disconnectPrompt: 'Tem certeza de que deseja desconectar o Gusto?',
+ syncResults: {
+ title: 'Resultados da sincronização com Gusto',
+ successTitle: 'Sua conexão com o Gusto foi sincronizada com sucesso!',
+ added: 'Adicionado',
+ removed: 'Removido',
+ skipped: 'Ignorado',
+ employeeCount: () => ({
+ one: '1 funcionário',
+ other: (count: number) => `${count} funcionários`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index d3ffe6ab78e6..dd6746f0e5fd 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6890,6 +6890,17 @@ ${reportName}
syncError: '无法连接到 Gusto',
disconnectTitle: '断开 Gusto',
disconnectPrompt: '确定要断开与 Gusto 的连接吗?',
+ syncResults: {
+ title: 'Gusto 同步结果',
+ successTitle: '已成功同步你的 Gusto 连接!',
+ added: '已添加',
+ removed: '已移除',
+ skipped: '已跳过',
+ employeeCount: () => ({
+ one: '1 员工',
+ other: (count: number) => `${count} 员工`,
+ }),
+ },
},
zenefits: {
title: 'TriNet',
diff --git a/src/libs/API/GustoSyncResult.ts b/src/libs/API/GustoSyncResult.ts
index 363162d21ca8..79c497f0fcc9 100644
--- a/src/libs/API/GustoSyncResult.ts
+++ b/src/libs/API/GustoSyncResult.ts
@@ -1,16 +1,13 @@
-type GustoSyncResultUser = {
- email: string;
- displayName?: string;
-};
-
-type GustoSyncSkippedResultUser = GustoSyncResultUser & {
+type GustoSyncSkippedEmployee = {
+ name: string;
+ id: string;
reason: string;
};
type GustoSyncResult = {
- added?: GustoSyncResultUser[];
- removed?: GustoSyncResultUser[];
- skipped?: GustoSyncSkippedResultUser[];
+ addedEmployeesCount?: number;
+ removedEmployeesCount?: number;
+ skippedEmployees?: GustoSyncSkippedEmployee[];
};
-export type {GustoSyncResult, GustoSyncResultUser};
+export type {GustoSyncResult, GustoSyncSkippedEmployee};
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index d5034e7ee3db..d5fea3a0ad46 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -28,6 +28,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement';
import useDebouncedValue from '@hooks/useDebouncedValue';
import useFilteredSelection from '@hooks/useFilteredSelection';
+import useGustoSyncResultsModal from '@hooks/useGustoSyncResultsModal';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -616,6 +617,8 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers
clearInviteDraft(route.params.policyID);
}, [invitedEmailsToAccountIDsDraft, isFocused, accountIDs, prevAccountIDs, route.params.policyID]);
+ useGustoSyncResultsModal(policyID, connectionSyncProgress, isFocused);
+
const headerMessage = useMemo(() => {
if (isOfflineAndNoMemberDataAvailable) {
return translate('workspace.common.mustBeOnlineToViewMembers');
diff --git a/src/pages/workspace/hr/WorkspaceHRPage.tsx b/src/pages/workspace/hr/WorkspaceHRPage.tsx
index 2e35466ee60c..00d332e6b887 100644
--- a/src/pages/workspace/hr/WorkspaceHRPage.tsx
+++ b/src/pages/workspace/hr/WorkspaceHRPage.tsx
@@ -1,3 +1,4 @@
+import {useIsFocused} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
@@ -15,6 +16,7 @@ import Section from '@components/Section';
import Text from '@components/Text';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types';
+import useGustoSyncResultsModal from '@hooks/useGustoSyncResultsModal';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -46,6 +48,7 @@ function WorkspaceHRPage({
},
}: WorkspaceHRPageProps) {
const {translate, datetimeToRelative, getLocalDateFromDatetime} = useLocalize();
+ const isFocused = useIsFocused();
const {isBetaEnabled} = usePermissions();
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -91,6 +94,8 @@ function WorkspaceHRPage({
fetchPolicyHRPage();
}, [fetchPolicyHRPage]);
+ useGustoSyncResultsModal(policyID, connectionSyncProgress, isFocused);
+
const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo(
() => [
{