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
10 changes: 8 additions & 2 deletions src/components/BaseWidgetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import type {ButtonProps} from './Button';
import Icon from './Icon';
import {PressableWithoutFeedback} from './Pressable';
import Text from './Text';
Expand Down Expand Up @@ -34,9 +35,12 @@ type BaseWidgetItemProps = {

/** Optional: fill color for the icon (defaults to white) */
iconFill?: string;

/** Additional props to pass to the Button component for styling control */
buttonProps?: Partial<ButtonProps>;
};

function BaseWidgetItem({icon, iconBackgroundColor, title, subtitle, ctaText, onCtaPress, iconFill}: BaseWidgetItemProps) {
function BaseWidgetItem({icon, iconBackgroundColor, title, subtitle, ctaText, onCtaPress, iconFill, buttonProps}: BaseWidgetItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {shouldUseNarrowLayout} = useResponsiveLayout();
Expand Down Expand Up @@ -65,9 +69,11 @@ function BaseWidgetItem({icon, iconBackgroundColor, title, subtitle, ctaText, on
<Button
text={ctaText}
onPress={onCtaPress}
success
small
style={styles.widgetItemButton}
// Prop spreading allows parent components to pass additional button styling props (e.g., danger: true, success: true)
// eslint-disable-next-line react/jsx-props-no-spreading
{...buttonProps}
/>
</View>
)}
Expand Down
6 changes: 6 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Validieren Sie Ihre Karte und beginnen Sie mit dem Ausgeben.',
cta: 'Aktivieren',
},
ctaFix: 'Beheben',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `${feedName}-Firmenkartenverbindung reparieren` : 'Firmenkarte reparieren Verbindung der Firmenkarte reparieren'),
subtitle: 'Workspace > Unternehmenskarten',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `${integrationName}-Verbindung reparieren`, subtitle: 'Arbeitsbereich > Buchhaltung'},
},
announcements: 'Ankündigungen',
discoverSection: {
Expand Down
9 changes: 9 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ const translations = {
timeSensitiveSection: {
title: 'Time sensitive',
cta: 'Claim',
ctaFix: 'Fix',
offer50off: {
title: 'Get 50% off your first year!',
subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} remaining`,
Expand All @@ -1006,6 +1007,14 @@ const translations = {
title: 'Get 25% off your first year!',
subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'day' : 'days'} remaining`,
},
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Fix ${feedName} company card connection` : 'Fix company card connection'),
subtitle: 'Workspace > Company cards',
},
fixAccountingConnection: {
title: ({integrationName}: {integrationName: string}) => `Fix ${integrationName} connection`,
subtitle: 'Workspace > Accounting',
},
addShippingAddress: {
title: 'We need your shipping address',
subtitle: 'Provide an address to receive your Expensify Card.',
Expand Down
9 changes: 9 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ const translations: TranslationDeepObject<typeof en> = {
timeSensitiveSection: {
title: 'Requiere atención inmediata',
cta: 'Reclamar',
ctaFix: 'Corrige',
offer50off: {
title: '¡Obtén 50% de descuento en tu primer año!',
subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} restantes`,
Expand All @@ -751,6 +752,14 @@ const translations: TranslationDeepObject<typeof en> = {
title: '¡Obtén 25% de descuento en tu primer año!',
subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'día' : 'días'} restantes`,
},
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Reconectar la tarjeta corporativa de ${feedName}` : 'Reconectar la tarjeta corporativa'),
subtitle: 'Espacio de trabajo > Tarjetas de empresa',
},
fixAccountingConnection: {
title: ({integrationName}: {integrationName: string}) => `Reconectar con ${integrationName}`,
subtitle: 'Espacio de trabajo > Contabilidad',
},
addShippingAddress: {
title: 'Necesitamos tu dirección de envío',
subtitle: 'Proporciona una dirección para recibir tu Tarjeta Expensify.',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Validez votre carte et commencez à dépenser.',
cta: 'Activer',
},
ctaFix: 'Corriger',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Corriger la connexion de la carte d'entreprise ${feedName}` : 'Corriger la connexion de la carte entreprise'),
subtitle: 'Espace de travail > Cartes d’entreprise',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `Corriger la connexion ${integrationName}`, subtitle: 'Espace de travail > Comptabilité'},
},
announcements: 'Annonces',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Convalida la tua carta e inizia a spendere.',
cta: 'Attiva',
},
ctaFix: 'Correggi',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Correggi la connessione della carta aziendale ${feedName}` : 'Correggi connessione carta aziendale'),
subtitle: 'Spazio di lavoro > Carte aziendali',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `Correggi connessione ${integrationName}`, subtitle: 'Spazio di lavoro > Contabilità'},
},
announcements: 'Annunci',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'カードを認証して支出を始めましょう。',
cta: '有効化',
},
ctaFix: '修正',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `${feedName} 会社カード接続を修正` : '法人クレジットカードの接続を修正'),
subtitle: 'ワークスペース > 会社カード',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `${integrationName} 接続を修正`, subtitle: 'ワークスペース > 会計'},
},
announcements: 'お知らせ',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Valideer je kaart en begin met uitgeven.',
cta: 'Activeren',
},
ctaFix: 'Repareren',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Verbinding bedrijfskaart ${feedName} herstellen` : 'Verbinding van bedrijfskaart repareren'),
subtitle: 'Werkruimte > Bedrijfspassen',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `Verbinding met ${integrationName} repareren`, subtitle: 'Werkruimte > Boekhouding'},
},
announcements: 'Aankondigingen',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Zatwierdź swoją kartę i zacznij wydawać.',
cta: 'Aktywuj',
},
ctaFix: 'Napraw',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Napraw połączenie karty firmowej ${feedName}` : 'Napraw połączenie karty firmowej'),
subtitle: 'Workspace > Karty firmowe',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `Napraw połączenie ${integrationName}`, subtitle: 'Przestrzeń robocza > Księgowość'},
},
announcements: 'Ogłoszenia',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Valide seu cartão e comece a gastar.',
cta: 'Ativar',
},
ctaFix: 'Corrigir',
fixCompanyCardConnection: {
title: ({feedName}: {feedName: string}) => (feedName ? `Corrigir conexão do cartão corporativo ${feedName}` : 'Corrigir conexão do cartão corporativo'),
subtitle: 'Área de trabalho > Cartões corporativos',
},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `Corrigir conexão com ${integrationName}`, subtitle: 'Espaço de trabalho > Contabilidade'},
},
announcements: 'Comunicados',
discoverSection: {
Expand Down
3 changes: 3 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,9 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: '验证您的银行卡并开始消费。',
cta: '启用',
},
ctaFix: '修复',
fixCompanyCardConnection: {title: ({feedName}: {feedName: string}) => (feedName ? `修复 ${feedName} 公司卡连接` : '修复公司卡连接'), subtitle: '工作区 > 公司卡片'},
fixAccountingConnection: {title: ({integrationName}: {integrationName: string}) => `修复 ${integrationName} 连接`, subtitle: '工作区 > 会计'},
},
announcements: '公告',
discoverSection: {
Expand Down
1 change: 1 addition & 0 deletions src/pages/home/ForYouSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function ForYouSection() {
title={translate(translationKey, {count})}
ctaText={translate('homePage.forYouSection.begin')}
onCtaPress={handler}
buttonProps={{success: true}}
/>
))}
</View>
Expand Down
130 changes: 126 additions & 4 deletions src/pages/home/TimeSensitiveSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,128 @@
import React from 'react';
import {activeAdminPoliciesSelector} from '@selectors/Policy';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import WidgetContainer from '@components/WidgetContainer';
import useCardFeedErrors from '@hooks/useCardFeedErrors';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {hasSynchronizationErrorMessage, isConnectionInProgress} from '@libs/actions/connections';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy';
import useTimeSensitiveCards from './hooks/useTimeSensitiveCards';
import useTimeSensitiveOffers from './hooks/useTimeSensitiveOffers';
import ActivateCard from './items/ActivateCard';
import AddShippingAddress from './items/AddShippingAddress';
import FixAccountingConnection from './items/FixAccountingConnection';
import FixCompanyCardConnection from './items/FixCompanyCardConnection';
import Offer25off from './items/Offer25off';
import Offer50off from './items/Offer50off';

type BrokenAccountingConnection = {
/** The policy ID associated with this connection */
policyID: string;

/** The connection name that has an error */
connectionName: PolicyConnectionName;
};

type BrokenCompanyCardConnection = {
/** The policy ID associated with this connection */
policyID: string;

/** The card ID associated with this connection */
cardID: string;
};

function TimeSensitiveSection() {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const);
const icons = useMemoizedLazyExpensifyIcons(['Stopwatch']);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {login} = useCurrentUserPersonalDetails();

// Use custom hooks for offers and cards (Release 3)
const {shouldShow50off, shouldShow25off, firstDayFreeTrial, discountInfo} = useTimeSensitiveOffers();
const {shouldShowAddShippingAddress, shouldShowActivateCard, cardsNeedingShippingAddress, cardsNeedingActivation} = useTimeSensitiveCards();

const hasAnyItemToShow = shouldShow50off || shouldShow25off || shouldShowAddShippingAddress || shouldShowActivateCard;
// Selector for filtering admin policies (Release 4)
const adminPoliciesSelectorWrapper = useCallback((policies: OnyxCollection<Policy>) => activeAdminPoliciesSelector(policies, login ?? ''), [login]);
const [adminPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true, selector: adminPoliciesSelectorWrapper});
const [connectionSyncProgress] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS, {canBeMissing: true});

// Get card feed errors for company card connections (Release 4)
const cardFeedErrors = useCardFeedErrors();

// Find policies with broken accounting connections (only for admins)
const brokenAccountingConnections: BrokenAccountingConnection[] = [];
for (const policy of adminPolicies ?? []) {
const policyConnections = policy.connections;
if (!policyConnections) {
continue;
}

// Check if there's a sync in progress for this policy using the proper check that handles JOB_DONE and timeout
const syncProgress = connectionSyncProgress?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy.id}`];
const isSyncInProgress = isConnectionInProgress(syncProgress, policy);

for (const connectionName of Object.keys(policyConnections) as ConnectionName[]) {
if (hasSynchronizationErrorMessage(policy, connectionName, isSyncInProgress)) {
brokenAccountingConnections.push({
policyID: policy.id,
connectionName,
});
}
}
}

// Get company cards with broken connections (for admins)
const brokenCompanyCardConnections: BrokenCompanyCardConnection[] = [];
const cardsWithBrokenConnection = cardFeedErrors.cardsWithBrokenFeedConnection;
if (cardsWithBrokenConnection && adminPolicies) {
for (const card of Object.values(cardsWithBrokenConnection)) {
if (!card?.fundID) {
continue;
}

// Find the policy associated with this card's fundID (workspaceAccountID)
const cardFundID = Number(card.fundID);
const matchingPolicy = adminPolicies.find((policy) => policy.workspaceAccountID === cardFundID);

if (!hasAnyItemToShow) {
if (!matchingPolicy) {
continue;
}

brokenCompanyCardConnections.push({
policyID: matchingPolicy.id,
cardID: String(card.cardID),
});
}
}

const hasBrokenCompanyCards = brokenCompanyCardConnections.length > 0;
const hasBrokenAccountingConnections = brokenAccountingConnections.length > 0;
const hasAnyTimeSensitiveContent =
shouldShow50off || shouldShow25off || hasBrokenCompanyCards || hasBrokenAccountingConnections || shouldShowAddShippingAddress || shouldShowActivateCard;

if (!hasAnyTimeSensitiveContent) {
return null;
}

// Priority order:
// 1. Potential card fraud ( not implemented here)
// 2. Broken bank connections (company cards)
// 3. Broken accounting connections
// 4. Early adoption discount (50% or 25%)
// 5. Expensify card shipping
// 6. Expensify card activation
return (
<WidgetContainer
icon={icons.Stopwatch}
Expand All @@ -40,15 +133,44 @@ function TimeSensitiveSection() {
titleColor={theme.danger}
>
<View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
{/* Priority 2: Broken company card connections */}
{brokenCompanyCardConnections.map((connection) => {
const card = cardFeedErrors.cardsWithBrokenFeedConnection[connection.cardID];
if (!card) {
return null;
}
return (
<FixCompanyCardConnection
key={`card-${connection.cardID}`}
card={card}
policyID={connection.policyID}
/>
);
})}

{/* Priority 3: Broken accounting connections */}
{brokenAccountingConnections.map((connection) => (
<FixAccountingConnection
key={`accounting-${connection.policyID}-${connection.connectionName}`}
connectionName={connection.connectionName}
policyID={connection.policyID}
/>
))}

{/* Priority 4: Early adoption discount offers */}
{shouldShow50off && <Offer50off firstDayFreeTrial={firstDayFreeTrial} />}
{shouldShow25off && !!discountInfo && <Offer25off days={discountInfo.days} />}

{/* Priority 5: Expensify card shipping */}
{shouldShowAddShippingAddress &&
cardsNeedingShippingAddress.map((card) => (
<AddShippingAddress
key={card.cardID}
card={card}
/>
))}

{/* Priority 6: Expensify card activation */}
{shouldShowActivateCard &&
cardsNeedingActivation.map((card) => (
<ActivateCard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function ActivateCard({card}: ActivateCardProps) {
subtitle={translate('homePage.timeSensitiveSection.activateCard.subtitle')}
ctaText={translate('homePage.timeSensitiveSection.activateCard.cta')}
onCtaPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(String(card.cardID)))}
buttonProps={{success: true}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mountiny we need to add this button prop styling to to ForYouSection

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image (Should be green)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function AddShippingAddress({card}: AddShippingAddressProps) {
subtitle={translate('homePage.timeSensitiveSection.addShippingAddress.subtitle')}
ctaText={translate('homePage.timeSensitiveSection.addShippingAddress.cta')}
onCtaPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(card.cardID)))}
buttonProps={{success: true}}
/>
);
}
Expand Down
Loading
Loading