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
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9961,6 +9961,8 @@ const CONST = {
},

HOME: {
// Maximum number of items in TimeSensitiveSection and YourSpendSection. Any extra items are revealed via the expand toggle button.
SECTION_VISIBLE_LIMIT: 5,
ANNOUNCEMENTS: [
{
title: 'Smarter cards, mileage, and approvals',
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: 'In den letzten 30 Tagen zurückgezahlt',
recentTransactions: ({lastFour}: {lastFour: string}) => `Aktuelle Transaktionen • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `${count} weitere anzeigen`,
},
allSettingsScreen: {
subscription: 'Abonnement',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ const translations = {
repaidLast30Days: 'Repaid in the last 30 days',
recentTransactions: ({lastFour}: {lastFour: string}) => `Recent transactions • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `See ${count} more`,
announcements: 'Announcements',
discoverSection: {
title: 'Discover',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ const translations: TranslationDeepObject<typeof en> = {
}),
today: 'Hoy',
},
seeMore: ({count}: {count: number}) => `Ver ${count} más`,
},
allSettingsScreen: {
subscription: 'Suscripcion',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: 'Remboursé au cours des 30 derniers jours',
recentTransactions: ({lastFour}: {lastFour: string}) => `Transactions récentes • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `Voir ${count} de plus`,
},
allSettingsScreen: {
subscription: 'Abonnement',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: 'Rimborsato negli ultimi 30 giorni',
recentTransactions: ({lastFour}: {lastFour: string}) => `Transazioni recenti • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `Vedi altri ${count}`,
},
allSettingsScreen: {
subscription: 'Abbonamento',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: '過去30日間に返済済み',
recentTransactions: ({lastFour}: {lastFour: string}) => `最近の取引 • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `さらに${count}件表示`,
},
allSettingsScreen: {
subscription: 'サブスクリプション',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: 'Terugbetaald in de afgelopen 30 dagen',
recentTransactions: ({lastFour}: {lastFour: string}) => `Recente transacties • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `Bekijk nog ${count}`,
},
allSettingsScreen: {
subscription: 'Abonnement',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: 'Spłacono w ciągu ostatnich 30 dni',
recentTransactions: ({lastFour}: {lastFour: string}) => `Ostatnie transakcje • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `Zobacz jeszcze ${count}`,
},
allSettingsScreen: {
subscription: 'Subskrypcja',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@ const translations: TranslationDeepObject<typeof en> = {
repaidLast30Days: 'Reembolsado nos últimos 30 dias',
recentTransactions: ({lastFour}: {lastFour: string}) => `Transações recentes • ${lastFour}`,
},
seeMore: ({count}: {count: number}) => `Ver mais ${count}`,
},
allSettingsScreen: {
subscription: 'Assinatura',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,7 @@ const translations: TranslationDeepObject<typeof en> = {
inviteAccountant: '邀请你的会计',
},
yourSpend: {title: '您的支出', awaitingApproval: '等待审批', repaidLast30Days: '过去30天内已偿还', recentTransactions: ({lastFour}: {lastFour: string}) => `最近交易 • ${lastFour}`},
seeMore: ({count}: {count: number}) => `再查看 ${count} 个`,
},
allSettingsScreen: {
subscription: '订阅',
Expand Down
59 changes: 59 additions & 0 deletions src/pages/home/HomeSectionExpandToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import Icon from '@components/Icon';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';

type HomeSectionExpandToggleProps = {
/** Whether the section is currently expanded */
isExpanded: boolean;

/** Callback to toggle the expanded state */
onPress: () => void;

/** Label rendered when the section is collapsed (e.g. "See 24 more") */
collapsedLabel: string;

/** Optional override for the wrapper style. Defaults to the standard ph5/ph8 padding used by home sections. */
wrapperStyle?: StyleProp<ViewStyle>;
};

function HomeSectionExpandToggle({isExpanded, onPress, collapsedLabel, wrapperStyle}: HomeSectionExpandToggleProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'UpArrow']);

const label = isExpanded ? translate('common.showLess') : collapsedLabel;

return (
<PressableWithFeedback
onPress={onPress}
role={CONST.ROLE.BUTTON}
accessibilityLabel={label}
sentryLabel="HomeSectionExpandToggle"
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, styles.pv3, shouldUseNarrowLayout ? styles.ph5 : styles.ph8, wrapperStyle]}
>
<View style={[styles.alignItemsCenter, styles.justifyContentCenter, {width: variables.componentSizeNormal, height: variables.componentSizeNormal}]}>
<Icon
src={isExpanded ? icons.UpArrow : icons.DownArrow}
fill={theme.icon}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
/>
</View>
<Text style={styles.textStrong}>{label}</Text>
</PressableWithFeedback>
);
}

export default HomeSectionExpandToggle;
208 changes: 121 additions & 87 deletions src/pages/home/TimeSensitiveSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {useFocusEffect} from '@react-navigation/native';
import {isUserValidatedSelector} from '@selectors/Account';
import {activeAdminPoliciesSelector} from '@selectors/Policy';
import {emailSelector} from '@selectors/Session';
import React, {useCallback} from 'react';
import React, {useCallback, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import WidgetContainer from '@components/WidgetContainer';
Expand All @@ -14,6 +15,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {hasSynchronizationErrorMessage, isConnectionInProgress} from '@libs/actions/connections';
import {isCurrentUserValidated} from '@libs/UserUtils';
import HomeSectionExpandToggle from '@pages/home/HomeSectionExpandToggle';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy';
Expand Down Expand Up @@ -65,6 +68,13 @@ function TimeSensitiveSection() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {login} = useCurrentUserPersonalDetails();
const isAnonymous = useIsAnonymousUser();
const [isExpanded, setIsExpanded] = useState(false);

useFocusEffect(
useCallback(() => {
return () => setIsExpanded(false);
}, []),
);

// Use custom hooks for offers and cards (Release 3)
const {shouldShowAddPaymentCard} = useTimeSensitiveAddPaymentCard();
Expand Down Expand Up @@ -182,96 +192,120 @@ function TimeSensitiveSection() {
// 8. Broken accounting connections
// 9. Expensify card shipping
// 10. Expensify card activation
return (
<WidgetContainer title={translate('homePage.timeSensitiveSection.title')}>
<View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
{/* Priority 1: Validate account */}
{shouldShowValidateAccount && <ValidateAccount />}
{/* Priority 2: Failed billing for existing customers */}
{!!shouldShowFixFailedBilling && <FixFailedBilling />}

{/* Priority 3: Card fraud alerts */}
{shouldShowReviewCardFraud &&
cardsWithFraud.map((card) => {
if (!card.nameValuePairs?.possibleFraud) {
return null;
}
return (
<ReviewCardFraud
key={card.cardID}
possibleFraud={card.nameValuePairs.possibleFraud}
/>
);
})}
const items: React.ReactNode[] = [];

{/* Priority 4: Add payment card (trial ended, no payment card) */}
{shouldShowAddPaymentCard && <AddPaymentCard />}
{/* Priority 5: 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}
policyName={connection.policyName}
/>
);
})}

{/* Priority 6: Broken personal card connections */}
{brokenPersonalCardConnections.map((connection) => {
const card = cardFeedErrors.personalCardsWithBrokenConnection[connection.cardID];
if (!card) {
return null;
}
return (
<FixPersonalCardConnection
key={`card-${connection.cardID}`}
card={card}
/>
);
})}
// Priority 1: Validate account
if (shouldShowValidateAccount) {
items.push(<ValidateAccount key="validate-account" />);
}
// Priority 2: Failed billing for existing customers
if (shouldShowFixFailedBilling) {
items.push(<FixFailedBilling key="fix-failed-billing" />);
}
// Priority 3: Card fraud alerts
if (shouldShowReviewCardFraud) {
for (const card of cardsWithFraud) {
if (!card.nameValuePairs?.possibleFraud) {
continue;
}
items.push(
<ReviewCardFraud
key={`fraud-${card.cardID}`}
possibleFraud={card.nameValuePairs.possibleFraud}
/>,
);
}
}
// Priority 4: Add payment card (trial ended, no payment card)
if (shouldShowAddPaymentCard) {
items.push(<AddPaymentCard key="add-payment-card" />);
}
// Priority 5: Broken company card connections
for (const connection of brokenCompanyCardConnections) {
const card = cardFeedErrors.cardsWithBrokenFeedConnection[connection.cardID];
if (!card) {
continue;
}
items.push(
<FixCompanyCardConnection
key={`company-card-${connection.cardID}`}
card={card}
policyID={connection.policyID}
policyName={connection.policyName}
/>,
);
}
// Priority 6: Broken personal card connections
for (const connection of brokenPersonalCardConnections) {
const card = cardFeedErrors.personalCardsWithBrokenConnection[connection.cardID];
if (!card) {
continue;
}
items.push(
<FixPersonalCardConnection
key={`personal-card-${connection.cardID}`}
card={card}
/>,
);
}
// Priority 7: Locked bank accounts
for (const lockedBankAccount of lockedBankAccounts) {
items.push(
<UnlockBankAccount
key={lockedBankAccount.key}
bankAccountID={lockedBankAccount.bankAccountID}
policyName={lockedBankAccount.policyName}
/>,
);
}
// Priority 8: Broken accounting connections
for (const connection of brokenAccountingConnections) {
items.push(
<FixAccountingConnection
key={`accounting-${connection.policyID}-${connection.connectionName}`}
connectionName={connection.connectionName}
policyID={connection.policyID}
policyName={connection.policyName}
/>,
);
}
// Priority 9: Expensify card shipping
if (shouldShowAddShippingAddress) {
for (const card of cardsNeedingShippingAddress) {
items.push(
<AddShippingAddress
key={`shipping-${card.cardID}`}
card={card}
/>,
);
}
}
// Priority 10: Expensify card activation
if (shouldShowActivateCard) {
for (const card of cardsNeedingActivation) {
items.push(
<ActivateCard
key={`activate-${card.cardID}`}
card={card}
/>,
);
}
}

{/* Priority 7: Locked bank accounts */}
{lockedBankAccounts.map((lockedBankAccount) => (
<UnlockBankAccount
key={lockedBankAccount.key}
bankAccountID={lockedBankAccount.bankAccountID}
policyName={lockedBankAccount.policyName}
/>
))}
const hiddenCount = Math.max(0, items.length - CONST.HOME.SECTION_VISIBLE_LIMIT);
const visibleItems = isExpanded ? items : items.slice(0, CONST.HOME.SECTION_VISIBLE_LIMIT);

{/* Priority 8: Broken accounting connections */}
{brokenAccountingConnections.map((connection) => (
<FixAccountingConnection
key={`accounting-${connection.policyID}-${connection.connectionName}`}
connectionName={connection.connectionName}
policyID={connection.policyID}
policyName={connection.policyName}
return (
<WidgetContainer title={translate('homePage.timeSensitiveSection.title')}>
<View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
{visibleItems}
{hiddenCount > 0 && (
<HomeSectionExpandToggle
isExpanded={isExpanded}
onPress={() => setIsExpanded((prev) => !prev)}
collapsedLabel={translate('homePage.seeMore', {count: hiddenCount})}
/>
))}

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

{/* Priority 10: Expensify card activation */}
{shouldShowActivateCard &&
cardsNeedingActivation.map((card) => (
<ActivateCard
key={card.cardID}
card={card}
/>
))}
)}
</View>
</WidgetContainer>
);
Expand Down
Loading
Loading