Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7222,6 +7222,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
alreadyConnectedPrompt: 'Sie müssen Ihre aktuelle HR-Plattform trennen, bevor Sie eine andere verbinden.',
lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`,
syncError: (providerName: string) => `Verbindung zu ${providerName} nicht möglich`,
authenticationError: (providerName: string) => `Verbindung zu ${providerName} aufgrund falscher Anmeldedaten nicht möglich.`,
reconnect: 'Erneut verbinden',
connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`,
approvalMode: 'Genehmigungsmodus',
providerApprovalMode: (providerName: string) => `${providerName}-Genehmigungsmodus`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6509,6 +6509,8 @@ const translations = {
alreadyConnectedPrompt: 'You must disconnect your current HR platform before connecting another.',
lastSync: (relativeDate: string) => `Last synced ${relativeDate}`,
syncError: (providerName: string) => `Can't connect to ${providerName}`,
authenticationError: (providerName: string) => `Couldn't connect to ${providerName} due to incorrect credentials.`,
reconnect: 'Reconnect',
connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`,
approvalMode: 'Approval mode',
providerApprovalMode: (providerName: string) => `${providerName} approval mode`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6321,6 +6321,8 @@ ${amount} para ${merchant} - ${date}`,
alreadyConnectedPrompt: 'Debes desconectar tu plataforma de RR. HH. actual antes de conectar otra.',
lastSync: (relativeDate: string) => `Última sincronización ${relativeDate}`,
syncError: (providerName: string) => `No se puede conectar con ${providerName}`,
authenticationError: (providerName: string) => `No se pudo conectar con ${providerName} debido a credenciales incorrectas.`,
reconnect: 'Volver a conectar',
connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`,
approvalMode: 'Modo de aprobación',
providerApprovalMode: (providerName: string) => `Modo de aprobación de ${providerName}`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7246,6 +7246,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
alreadyConnectedPrompt: 'Vous devez déconnecter votre plateforme RH actuelle avant d’en connecter une autre.',
lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`,
syncError: (providerName: string) => `Impossible de se connecter à ${providerName}`,
authenticationError: (providerName: string) => `Impossible de se connecter à ${providerName} en raison d’identifiants incorrects.`,
reconnect: 'Reconnecter',
connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`,
approvalMode: "Mode d'approbation",
providerApprovalMode: (providerName: string) => `Mode d'approbation ${providerName}`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7207,6 +7207,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
alreadyConnectedPrompt: 'Devi disconnettere la tua attuale piattaforma HR prima di collegarne un’altra.',
lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`,
syncError: (providerName: string) => `Impossibile connettersi a ${providerName}`,
authenticationError: (providerName: string) => `Impossibile connettersi a ${providerName} a causa di credenziali errate.`,
reconnect: 'Riconnetti',
connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`,
approvalMode: 'Modalità di approvazione',
providerApprovalMode: (providerName: string) => `Modalità di approvazione ${providerName}`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7125,6 +7125,8 @@ ${reportName}
alreadyConnectedPrompt: '別の人事プラットフォームに接続する前に、現在の人事プラットフォームとの接続を解除する必要があります。',
lastSync: (relativeDate: string) => `最終同期: ${relativeDate}`,
syncError: (providerName: string) => `${providerName}に接続できません`,
authenticationError: (providerName: string) => `認証情報が正しくないため、${providerName}に接続できませんでした。`,
reconnect: '再接続',
connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`,
approvalMode: '承認モード',
providerApprovalMode: (providerName: string) => `${providerName} 承認モード`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7181,6 +7181,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
alreadyConnectedPrompt: 'Je moet je huidige HR-platform loskoppelen voordat je een ander kunt verbinden.',
lastSync: (relativeDate: string) => `Laatst gesynchroniseerd ${relativeDate}`,
syncError: (providerName: string) => `Kan geen verbinding maken met ${providerName}`,
authenticationError: (providerName: string) => `Kan geen verbinding maken met ${providerName} vanwege onjuiste inloggegevens.`,
reconnect: 'Opnieuw verbinden',
connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`,
approvalMode: 'Goedkeuringsmodus',
providerApprovalMode: (providerName: string) => `${providerName}-goedkeuringsmodus`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7174,6 +7174,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
alreadyConnectedPrompt: 'Musisz odłączyć swoją obecną platformę HR, zanim podłączysz inną.',
lastSync: (relativeDate: string) => `Ostatnia synchronizacja ${relativeDate}`,
syncError: (providerName: string) => `Nie można połączyć z ${providerName}`,
authenticationError: (providerName: string) => `Nie można połączyć z ${providerName} z powodu nieprawidłowych danych logowania.`,
reconnect: 'Połącz ponownie',
connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`,
approvalMode: 'Tryb zatwierdzania',
providerApprovalMode: (providerName: string) => `Tryb zatwierdzania ${providerName}`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7181,6 +7181,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
alreadyConnectedPrompt: 'Você precisa desconectar sua plataforma de RH atual antes de conectar outra.',
lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`,
syncError: (providerName: string) => `Não é possível conectar ao ${providerName}`,
authenticationError: (providerName: string) => `Não foi possível conectar ao ${providerName} devido a credenciais incorretas.`,
reconnect: 'Reconectar',
connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`,
approvalMode: 'Modo de aprovação',
providerApprovalMode: (providerName: string) => `Modo de aprovação do ${providerName}`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6992,6 +6992,8 @@ ${reportName}
alreadyConnectedPrompt: '在连接其他人力资源平台之前,您必须先断开当前的人力资源平台。',
lastSync: (relativeDate: string) => `上次同步 ${relativeDate}`,
syncError: (providerName: string) => `无法连接到 ${providerName}`,
authenticationError: (providerName: string) => `由于凭据不正确,无法连接到 ${providerName}。`,
reconnect: '重新连接',
connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`,
approvalMode: '审批模式',
providerApprovalMode: (providerName: string) => `${providerName} 审批模式`,
Expand Down
25 changes: 21 additions & 4 deletions src/pages/workspace/hr/HRProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types';
import useConfirmModal from '@hooks/useConfirmModal';
Expand All @@ -30,9 +32,12 @@ type HRProviderCardProps = {

/** Callback invoked when the user taps the "Connect" button for an unconnected provider. */
handleConnect: () => void;

/** Callback invoked when the user taps "Reconnect" on a connected provider with an auth error. */
onReconnect: () => void;
};

function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
function HRProviderCard({card, policy, handleConnect, onReconnect}: HRProviderCardProps) {
const {translate, datetimeToRelative} = useLocalize();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
Expand All @@ -46,12 +51,24 @@ function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
let connectionDescription: string | undefined;
if (card.isSyncInProgress) {
connectionDescription = card.syncStageInProgress ? translate('workspace.hr.syncStageName', {stage: card.syncStageInProgress}) : translate('workspace.hr.syncing');
} else if (card.successfulDate && !card.hasError) {
} else if (card.successfulDate && !card.hasError && !card.hasAuthenticationError) {
connectionDescription = translate('workspace.hr.lastSync', datetimeToRelative(card.successfulDate));
}

let lastSyncErrorMessage: string | undefined;
if (card.hasError) {
let lastSyncErrorMessage: string | React.ReactNode | undefined;
if (card.hasAuthenticationError) {
lastSyncErrorMessage = (
<Text style={styles.formError}>
{translate('workspace.hr.authenticationError', card.displayName)}{' '}
<TextLink
style={[styles.link, styles.fontSizeLabel]}
onPress={onReconnect}
>
{translate('workspace.hr.reconnect')}
</TextLink>
</Text>
);
} else if (card.hasError) {
const genericError = translate('workspace.hr.syncError', card.displayName);
lastSyncErrorMessage = card.lastSyncErrorMessage ? `${genericError} ("${card.lastSyncErrorMessage}")` : genericError;
}
Expand Down
12 changes: 12 additions & 0 deletions src/pages/workspace/hr/WorkspaceHRPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ function WorkspaceHRPage({
setActiveHRFlow({setupLink, key: Math.random()});
};

const handleReconnect = (setupLink: string | undefined) => {
if (!setupLink) {
return;
}

// eslint-disable-next-line react-hooks/purity -- random key forces remount on every press, even for the same provider
setActiveHRFlow({setupLink, key: Math.random()});
};

return (
<AccessOrNotFoundWrapper
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
Expand Down Expand Up @@ -152,6 +161,7 @@ function WorkspaceHRPage({
card={card}
policy={policy}
handleConnect={() => handleConnect(card.setupLink)}
onReconnect={() => handleReconnect(card.setupLink)}
/>
))}
{connectedCards.length === 0 &&
Expand All @@ -161,6 +171,7 @@ function WorkspaceHRPage({
card={card}
policy={policy}
handleConnect={() => handleConnect(card.setupLink)}
onReconnect={() => handleReconnect(card.setupLink)}
/>
))}
</View>
Expand All @@ -178,6 +189,7 @@ function WorkspaceHRPage({
card={card}
policy={policy}
handleConnect={() => handleConnect(card.setupLink)}
onReconnect={() => handleReconnect(card.setupLink)}
/>
))}
</CollapsibleSection>
Expand Down
14 changes: 11 additions & 3 deletions src/pages/workspace/hr/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {OnyxEntry} from 'react-native-onyx';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import {hasSynchronizationErrorMessage, isConnectionInProgress} from '@libs/actions/connections';
import {hasSynchronizationErrorMessage, isAuthenticationError, isConnectionInProgress} from '@libs/actions/connections';
import getGustoSetupLink from '@libs/actions/connections/Gusto';
import getMergeHRSetupLink from '@libs/actions/connections/MergeHR';
import getZenefitsSetupLink from '@libs/actions/connections/Zenefits';
Expand Down Expand Up @@ -50,6 +50,9 @@ type HRCardDescriptor = {
/** Whether the last sync resulted in an error. */
hasError: boolean;

/** Whether the last sync failed due to an authentication error. */
hasAuthenticationError: boolean;

/** Human-readable error message from the last failed sync attempt. */
lastSyncErrorMessage?: string;

Expand Down Expand Up @@ -92,10 +95,14 @@ type GetHRCardStateParams = {
function getMergeHRSyncState(policy: OnyxEntry<Policy>) {
const lastSync = policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]?.lastSync;
const isSyncInProgress = lastSync?.syncStatus === CONST.MERGE_HR.SYNC_STATUS.SYNCING;
const hasAuthenticationError = isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.MERGE_HR);
const hasFailedSync = lastSync?.syncStatus === CONST.MERGE_HR.SYNC_STATUS.FAILED;

return {
isSyncInProgress,
isInitialSyncInProgress: isSyncInProgress && lastSync?.syncType === CONST.MERGE_HR.SYNC_TYPE.INITIAL,
hasError: lastSync?.syncStatus === CONST.MERGE_HR.SYNC_STATUS.FAILED,
hasAuthenticationError,
hasError: hasFailedSync && !hasAuthenticationError,
syncStageInProgress: undefined,
successfulDate: lastSync?.successfulDate,
};
Expand All @@ -113,6 +120,7 @@ function getHRSyncState(
return {
isSyncInProgress,
isInitialSyncInProgress: undefined,
hasAuthenticationError: false,
hasError: hasSynchronizationErrorMessage(policy, connectionName, isSyncInProgress),
syncStageInProgress: isSyncInProgress && syncProgress?.stageInProgress ? syncProgress.stageInProgress : undefined,
successfulDate: getIntegrationLastSuccessfulDate(getLocalDateFromDatetime, connection, syncProgress),
Expand Down Expand Up @@ -261,7 +269,7 @@ function getHRCards({policy, connectionSyncProgress, isBetaEnabled, getLocalDate

if (isBetaEnabled(CONST.BETAS.MERGE_HR)) {
const mergeConnectionName = CONST.POLICY.CONNECTIONS.NAME.MERGE_HR;
const disconnectedState = {isConnected: false, isSyncInProgress: false, isInitialSyncInProgress: false, hasError: false} as const;
const disconnectedState = {isConnected: false, isSyncInProgress: false, isInitialSyncInProgress: false, hasError: false, hasAuthenticationError: false} as const;

for (const [slug, providerEntry] of Object.entries(MERGE_HR_PROVIDERS) as Array<[MergeHRProviderSlug, (typeof MERGE_HR_PROVIDERS)[MergeHRProviderSlug]]>) {
const state = getHRCardState({policy, connectionName: mergeConnectionName, connectionSyncProgress, getLocalDateFromDatetime, mergeSlug: slug});
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/HrUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,60 @@ describe('getHRCardState', () => {
expect(state.isSyncInProgress).toBe(false);
expect(state.syncStageInProgress).toBeUndefined();
});

it('detects authentication error and suppresses generic sync error', () => {
const policy = makePolicy({
connections: {
// eslint-disable-next-line @typescript-eslint/naming-convention
merge_hris: {
config: {integration: 'bamboohr'},
data: {},
lastSync: {
syncStatus: CONST.MERGE_HR.SYNC_STATUS.FAILED,
isAuthenticationError: true,
errorMessage: 'Invalid credentials',
},
},
} as unknown as Policy['connections'],
});
const state = getHRCardState({
policy,
connectionName: MERGE_HR,
connectionSyncProgress: undefined,
getLocalDateFromDatetime: stubGetLocalDateFromDatetime,
mergeSlug: 'bamboohr',
});
expect(state.hasAuthenticationError).toBe(true);
expect(state.hasError).toBe(false);
expect(state.lastSyncErrorMessage).toBeUndefined();
});

it('reports generic sync error when failed without authentication error', () => {
const policy = makePolicy({
connections: {
// eslint-disable-next-line @typescript-eslint/naming-convention
merge_hris: {
config: {integration: 'bamboohr'},
data: {},
lastSync: {
syncStatus: CONST.MERGE_HR.SYNC_STATUS.FAILED,
isAuthenticationError: false,
errorMessage: 'Sync failed',
},
},
} as unknown as Policy['connections'],
});
const state = getHRCardState({
policy,
connectionName: MERGE_HR,
connectionSyncProgress: undefined,
getLocalDateFromDatetime: stubGetLocalDateFromDatetime,
mergeSlug: 'bamboohr',
});
expect(state.hasAuthenticationError).toBe(false);
expect(state.hasError).toBe(true);
expect(state.lastSyncErrorMessage).toBe('Sync failed');
});
});
});

Expand Down
Loading