Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4944,6 +4944,12 @@ const CONST = {
BUSINESS_CENTRAL_SYNC_IMPORT_DATA: 'businessCentralSyncImportData',
},
SYNC_STAGE_TIMEOUT_MINUTES: 20,
/** How many days before a QuickBooks Online refresh token expires the workspace starts warning admins to reconnect */
QBO_REFRESH_TOKEN_EXPIRY_WARNING_DAYS: 7,
QBO_REFRESH_TOKEN_EXPIRY_STATUS: {
EXPIRING_SOON: 'expiringSoon',
EXPIRED: 'expired',
},
},
ACCESS_VARIANTS: {
PAID: 'paid',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';

import {isConnectionInProgress} from '@libs/actions/connections';
import {getPolicyBrickRoadIndicatorStatus, getUberConnectionErrorDirectlyFromPolicy, isMergeHRCompleteSetupNeededSelector, shouldShowEmployeeListError} from '@libs/PolicyUtils';
import {
getPolicyBrickRoadIndicatorStatus,
getUberConnectionErrorDirectlyFromPolicy,
isMergeHRCompleteSetupNeededSelector,
isQBORefreshTokenExpiringSoonSelector,
shouldShowEmployeeListError,
} from '@libs/PolicyUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -48,12 +54,13 @@ function WorkspaceRowBrickRoadIndicator({policyID}: WorkspaceRowBrickRoadIndicat
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
const [hasPolicyErrors] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {selector: createPolicyErrorsSelector(connectionSyncProgress)});
const [isHRCompleteSetupNeeded] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {selector: isMergeHRCompleteSetupNeededSelector});
const [isQBORefreshTokenExpiringSoon] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {selector: isQBORefreshTokenExpiringSoonSelector});

// All of the error sources resolve to ERROR or undefined; the HR setup check is the only INFO source
// and every error takes precedence over it.
// All of the error sources resolve to ERROR or undefined. The HR setup check and the QBO token expiry
// warning are the only INFO sources, and every error takes precedence over them.
const hasError = !!hasReimbursementAccountErrors || !!hasCardFeedErrors || !!hasPolicyErrors;

if (!hasError && !isHRCompleteSetupNeeded) {
if (!hasError && !isHRCompleteSetupNeeded && !isQBORefreshTokenExpiringSoon) {
return null;
}

Expand Down
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7514,6 +7514,8 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Zum Entsperren automatische Synchronisierung aktivieren.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Ihre QuickBooks Online-Verbindung läuft am ${date} ab.`,
qboConnectionExpired: ({date}: {date: string}) => `Ihre QuickBooks Online-Verbindung ist am ${date} abgelaufen.`,
},
export: {
notReadyHeading: 'Nicht bereit zum Export',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7649,6 +7649,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Για να το ξεκλειδώσετε, ενεργοποιήστε τον αυτόματο συγχρονισμό.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Η σύνδεσή σας με το QuickBooks Online λήγει στις ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Η σύνδεσή σας με το QuickBooks Online έληξε στις ${date}.`,
},
hr: {
title: 'HR',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7461,6 +7461,8 @@ const translations = {
reconnect: 'Reconnect',
enterCredentials: 'Enter your credentials',
updateCredentials: 'Update credentials',
qboConnectionExpiring: ({date}: {date: string}) => `Your QuickBooks Online connection expires on ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Your QuickBooks Online connection expired on ${date}.`,
claimOffer: {
badgeText: 'Offer available!',
xero: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7322,6 +7322,8 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Para desbloquear, habilita la sincronización automática.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Tu conexión con QuickBooks Online caduca el ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Tu conexión con QuickBooks Online caducó el ${date}.`,
},
card: {
issueCard: 'Emitir tarjeta',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7535,6 +7535,8 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Pour le déverrouiller, activez la synchronisation automatique.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Votre connexion QuickBooks Online expire le ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Votre connexion à QuickBooks Online a expiré le ${date}.`,
},
export: {
notReadyHeading: 'Pas prêt à être exporté',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7473,6 +7473,8 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Per sbloccare, abilita la sincronizzazione automatica.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `La tua connessione a QuickBooks Online scade il ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `La tua connessione a QuickBooks Online è scaduta il ${date}.`,
},
export: {
notReadyHeading: 'Non pronto per l’esportazione',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7385,6 +7385,8 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'ロックを解除するには、自動同期を有効にしてください。',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `QuickBooks Online との接続は ${date} に有効期限が切れます。`,
qboConnectionExpired: ({date}: {date: string}) => `QuickBooks Online との接続は ${date} に期限切れになりました。`,
},
export: {
notReadyHeading: 'エクスポートの準備ができていません',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7457,6 +7457,8 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Schakel automatisch synchroniseren in om dit te ontgrendelen.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Je QuickBooks Online-verbinding verloopt op ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Je QuickBooks Online-verbinding is op ${date} verlopen.`,
},
export: {
notReadyHeading: 'Niet klaar om te exporteren',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7482,6 +7482,8 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Aby odblokować, włącz automatyczną synchronizację.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Twoje połączenie z QuickBooks Online wygaśnie ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Twoje połączenie z QuickBooks Online wygasło ${date}.`,
},
export: {
notReadyHeading: 'Niegotowe do eksportu',
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 @@ -7460,6 +7460,8 @@ O plano Control começa em US$ 9 por membro ativo por mês.`,
syncTravelInvoicingSettlementsNoAutoSyncTooltip: 'Para desbloquear, ative a sincronização automática.',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `Sua conexão com o QuickBooks Online expira em ${date}.`,
qboConnectionExpired: ({date}: {date: string}) => `Sua conexão com o QuickBooks Online expirou em ${date}.`,
},
export: {
notReadyHeading: 'Não está pronto para exportar',
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 @@ -7201,6 +7201,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
syncTravelInvoicingSettlementsNoAutoSyncTooltip: '若要解锁,请启用自动同步。',
campfire: 'Campfire',
businessCentral: 'Dynamics 365 Business Central',
qboConnectionExpiring: ({date}: {date: string}) => `您的 QuickBooks Online 连接将于 ${date} 过期。`,
qboConnectionExpired: ({date}: {date: string}) => `您与 QuickBooks Online 的连接已于 ${date} 过期。`,
},
export: {
notReadyHeading: '尚未准备好导出',
Expand Down
36 changes: 36 additions & 0 deletions src/libs/AccountingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy'
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';

import {differenceInCalendarDays, fromUnixTime} from 'date-fns';

const ROUTE_NAME_MAPPING = {
[CONST.POLICY.CONNECTIONS.ROUTE.QBO]: CONST.POLICY.CONNECTIONS.NAME.QBO,
[CONST.POLICY.CONNECTIONS.ROUTE.XERO]: CONST.POLICY.CONNECTIONS.NAME.XERO,
Expand Down Expand Up @@ -77,6 +79,37 @@ function isIntuitEnterpriseSuiteConnection(policy: OnyxEntry<Policy>): boolean {
return !!policy?.connections?.quickbooksOnline?.config?.credentials?.scope?.includes(CONST.POLICY.CONNECTIONS.INTUIT_ENTERPRISE_SUITE_SCOPE);
}

/** The moment the QuickBooks Online refresh token stops working. Intuit restarts its 100-day lifetime on every refresh, so this only gets close on idle connections. */
function getQBORefreshTokenExpiryDate(policy: OnyxEntry<Policy>): Date | undefined {
const refreshTokenExpiresAt = policy?.connections?.quickbooksOnline?.config?.credentials?.refreshTokenExpiresAt;
if (!refreshTokenExpiresAt) {
return undefined;
}
return fromUnixTime(refreshTokenExpiresAt);
}

/**
* Whether the QuickBooks Online refresh token expires within the warning window or already has, so the workspace should ask its admins to reconnect
* before exports start failing. A connection that already reports an authentication error is excluded since it shows the reconnect error state instead.
*/
function getQBORefreshTokenExpiryStatus(policy: OnyxEntry<Policy>, now: Date = new Date()): ValueOf<typeof CONST.POLICY.CONNECTIONS.QBO_REFRESH_TOKEN_EXPIRY_STATUS> | undefined {
const expiryDate = getQBORefreshTokenExpiryDate(policy);
if (!expiryDate || policy?.connections?.quickbooksOnline?.lastSync?.isAuthenticationError === true) {
return undefined;
}
if (expiryDate.getTime() <= now.getTime()) {
return CONST.POLICY.CONNECTIONS.QBO_REFRESH_TOKEN_EXPIRY_STATUS.EXPIRED;
}
if (differenceInCalendarDays(expiryDate, now) <= CONST.POLICY.CONNECTIONS.QBO_REFRESH_TOKEN_EXPIRY_WARNING_DAYS) {
return CONST.POLICY.CONNECTIONS.QBO_REFRESH_TOKEN_EXPIRY_STATUS.EXPIRING_SOON;
}
return undefined;
}

function isQBORefreshTokenExpiringSoon(policy: OnyxEntry<Policy>, now: Date = new Date()): boolean {
return !!getQBORefreshTokenExpiryStatus(policy, now);
}

function getQuickbooksOnlineIntegrationName(policy: OnyxEntry<Policy>, translate: LocaleContextProps['translate']): string {
return translate(isIntuitEnterpriseSuiteConnection(policy) ? 'workspace.accounting.intuitEnterpriseSuite' : 'workspace.accounting.qbo');
}
Expand Down Expand Up @@ -107,10 +140,13 @@ export {
getConnectionNameFromRouteParam,
getExportLabelForConnection,
getExportLabelsForConnection,
getQBORefreshTokenExpiryDate,
getQBORefreshTokenExpiryStatus,
getQuickbooksOnlineIntegrationName,
getRouteParamForConnection,
getStandardExportTemplateDisplayName,
isStandardExportTemplate,
isStandardExportTemplateLabel,
isIntuitEnterpriseSuiteConnection,
isQBORefreshTokenExpiringSoon,
};
8 changes: 7 additions & 1 deletion src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import type {TupleToUnion, ValueOf} from 'type-fest';

import {Str} from 'expensify-common';

import {getQuickbooksOnlineIntegrationName} from './AccountingUtils';
import {getQuickbooksOnlineIntegrationName, isQBORefreshTokenExpiringSoon} from './AccountingUtils';
import {getBankAccountFromID} from './actions/BankAccounts';
import {hasSynchronizationErrorMessage, isConnectionUnverified} from './actions/connections';
import {shouldShowQBOReimbursableExportDestinationAccountError} from './actions/connections/QuickbooksOnline';
Expand Down Expand Up @@ -642,6 +642,11 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry<Policy>, isConnecti
*/
const isMergeHRCompleteSetupNeededSelector = (policy: OnyxEntry<Policy>) => isMergeHRCompleteSetupNeeded(policy);

/**
* Returns whether an admin should be warned that the workspace's QuickBooks Online connection is about to expire.
*/
const isQBORefreshTokenExpiringSoonSelector = (policy: OnyxEntry<Policy>) => isPolicyAdmin(policy) && isQBORefreshTokenExpiringSoon(policy);

function getPolicyRole(policy: OnyxInputOrEntry<Policy>, currentUserLogin?: string, shouldCheckGlobalPolicyRole = true): string | undefined {
if (shouldCheckGlobalPolicyRole && policy?.role) {
return policy.role;
Expand Down Expand Up @@ -3982,6 +3987,7 @@ export {
hasAnyPaidPolicy,
isTaxCodeCustomized,
isMergeHRCompleteSetupNeededSelector,
isQBORefreshTokenExpiringSoonSelector,
};

export type {MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess};
Loading
Loading