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: 3 additions & 3 deletions src/hooks/useAccountTabIndicatorStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function useAccountTabIndicatorStatus(): AccountTabIndicatorStatusResult {
const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(allCards, CONST.EXPENSIFY_CARD.BANK);
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
const [stripeCustomerId] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, {canBeMissing: true});

const [retryBillingSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {canBeMissing: true});
// All of the error & info-checking methods are put into an array. This is so that using _.some() will return
// early as soon as the first error / info condition is returned. This makes the checks very efficient since
// we only care if a single error / info condition exists anywhere.
Expand All @@ -41,12 +41,12 @@ function useAccountTabIndicatorStatus(): AccountTabIndicatorStatusResult {
[CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
[CONST.INDICATOR_STATUS.HAS_CARD_CONNECTION_ERROR]: hasBrokenFeedConnection,
[CONST.INDICATOR_STATUS.HAS_PHONE_NUMBER_ERROR]: !!privatePersonalDetails?.errorFields?.phoneNumber,
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(stripeCustomerId),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(stripeCustomerId, retryBillingSuccessful),
};

const infoChecking: Partial<Record<AccountTabIndicatorStatus, boolean>> = {
[CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && hasLoginListInfo(loginList, session?.email),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(stripeCustomerId),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(stripeCustomerId, retryBillingSuccessful),
};

const [error] = Object.entries(errorChecking).find(([, value]) => value) ?? [];
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useIndicatorStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function useIndicatorStatus(): IndicatorStatusResult {
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true});
const [allCards] = useOnyx(`${ONYXKEYS.CARD_LIST}`, {canBeMissing: true});
const [stripeCustomerId] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, {canBeMissing: true});
const [retryBillingSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {canBeMissing: true});
const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(allCards, CONST.EXPENSIFY_CARD.BANK);
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});

Expand All @@ -57,7 +58,7 @@ function useIndicatorStatus(): IndicatorStatusResult {
[CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0,
[CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList),
...(Object.fromEntries(Object.entries(policyErrors).map(([error, policy]) => [error, !!policy])) as Record<keyof typeof policyErrors, boolean>),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(stripeCustomerId),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(stripeCustomerId, retryBillingSuccessful),
[CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
[CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && hasLoginListError(loginList),
// Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
Expand All @@ -68,7 +69,7 @@ function useIndicatorStatus(): IndicatorStatusResult {

const infoChecking: Partial<Record<IndicatorStatus, boolean>> = {
[CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && hasLoginListInfo(loginList, session?.email),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(stripeCustomerId),
[CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(stripeCustomerId, retryBillingSuccessful),
};

const [error] = Object.entries(errorChecking).find(([, value]) => value) ?? [];
Expand Down
27 changes: 7 additions & 20 deletions src/libs/SubscriptionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,6 @@ Onyx.connect({
},
});

let retryBillingSuccessful: OnyxEntry<boolean>;
Onyx.connect({
key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL,
initWithStoredValues: false,
callback: (value) => {
if (value === undefined) {
return;
}

retryBillingSuccessful = value;
},
});

let retryBillingFailed: OnyxEntry<boolean>;
Onyx.connect({
key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED,
Expand Down Expand Up @@ -343,7 +330,7 @@ function hasRetryBillingError(): boolean {
/**
* @returns Whether the retry billing was successful.
*/
function isRetryBillingSuccessful(): boolean {
function isRetryBillingSuccessful(retryBillingSuccessful: boolean | undefined): boolean {
return !!retryBillingSuccessful;
}

Expand All @@ -355,7 +342,7 @@ type SubscriptionStatus = {
/**
* @returns The subscription status.
*/
function getSubscriptionStatus(stripeCustomerId: OnyxEntry<StripeCustomerID>): SubscriptionStatus | undefined {
function getSubscriptionStatus(stripeCustomerId: OnyxEntry<StripeCustomerID>, retryBillingSuccessful: boolean | undefined): SubscriptionStatus | undefined {
if (hasOverdueGracePeriod()) {
if (hasAmountOwed()) {
// 1. Policy owner with amount owed, within grace period
Expand Down Expand Up @@ -431,7 +418,7 @@ function getSubscriptionStatus(stripeCustomerId: OnyxEntry<StripeCustomerID>): S
}

// 10. Retry billing success
if (isRetryBillingSuccessful()) {
if (isRetryBillingSuccessful(retryBillingSuccessful)) {
return {
status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS,
isError: false,
Expand All @@ -452,15 +439,15 @@ function getSubscriptionStatus(stripeCustomerId: OnyxEntry<StripeCustomerID>): S
/**
* @returns Whether there is a subscription red dot error.
*/
function hasSubscriptionRedDotError(stripeCustomerId: OnyxEntry<StripeCustomerID>): boolean {
return getSubscriptionStatus(stripeCustomerId)?.isError ?? false;
function hasSubscriptionRedDotError(stripeCustomerId: OnyxEntry<StripeCustomerID>, retryBillingSuccessful: boolean | undefined): boolean {
return getSubscriptionStatus(stripeCustomerId, retryBillingSuccessful)?.isError ?? false;
}

/**
* @returns Whether there is a subscription green dot info.
*/
function hasSubscriptionGreenDotInfo(stripeCustomerId: OnyxEntry<StripeCustomerID>): boolean {
return getSubscriptionStatus(stripeCustomerId)?.isError === false;
function hasSubscriptionGreenDotInfo(stripeCustomerId: OnyxEntry<StripeCustomerID>, retryBillingSuccessful: boolean | undefined): boolean {
return getSubscriptionStatus(stripeCustomerId, retryBillingSuccessful)?.isError === false;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
const [stripeCustomerId] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, {canBeMissing: true});
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const [retryBillingSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {canBeMissing: true});

const {shouldUseNarrowLayout} = useResponsiveLayout();
const network = useNetwork();
Expand Down Expand Up @@ -197,7 +198,8 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
translationKey: 'allSettingsScreen.subscription',
icon: Expensicons.CreditCard,
screenName: SCREENS.SETTINGS.SUBSCRIPTION.ROOT,
brickRoadIndicator: !!privateSubscription?.errors || hasSubscriptionRedDotError(stripeCustomerId) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
brickRoadIndicator:
!!privateSubscription?.errors || hasSubscriptionRedDotError(stripeCustomerId, retryBillingSuccessful) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
badgeText: freeTrialText,
badgeStyle: freeTrialText ? styles.badgeSuccess : undefined,
action: () => Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION.route),
Expand All @@ -223,6 +225,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
privateSubscription?.errors,
stripeCustomerId,
freeTrialText,
retryBillingSuccessful,
]);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ function CardSection() {
}, []);

const [billingStatus, setBillingStatus] = useState<BillingStatusResult | undefined>(() =>
CardSectionUtils.getBillingStatus({translate, stripeCustomerId: privateStripeCustomerID, accountData: defaultCard?.accountData ?? {}, purchase: purchaseList?.[0]}),
CardSectionUtils.getBillingStatus({
translate,
stripeCustomerId: privateStripeCustomerID,
accountData: defaultCard?.accountData ?? {},
purchase: purchaseList?.[0],
retryBillingSuccessful: subscriptionRetryBillingStatusSuccessful,
}),
);

const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined;
Expand All @@ -96,6 +102,7 @@ function CardSection() {
stripeCustomerId: privateStripeCustomerID,
accountData: defaultCard?.accountData ?? {},
purchase: purchaseList?.[0],
retryBillingSuccessful: subscriptionRetryBillingStatusSuccessful,
}),
);
}, [
Expand Down
5 changes: 3 additions & 2 deletions src/pages/settings/Subscription/CardSection/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ type GetBillingStatusProps = {
stripeCustomerId: OnyxEntry<StripeCustomerID>;
accountData?: AccountData;
purchase?: Purchase;
retryBillingSuccessful: OnyxEntry<boolean>;
};

function getBillingStatus({translate, stripeCustomerId, accountData, purchase}: GetBillingStatusProps): BillingStatusResult | undefined {
function getBillingStatus({translate, stripeCustomerId, accountData, purchase, retryBillingSuccessful}: GetBillingStatusProps): BillingStatusResult | undefined {
const cardEnding = (accountData?.cardNumber ?? '')?.slice(-4);

const amountOwed = getAmountOwed();

const subscriptionStatus = getSubscriptionStatus(stripeCustomerId);
const subscriptionStatus = getSubscriptionStatus(stripeCustomerId, retryBillingSuccessful);

const endDate = getOverdueGracePeriodDate();

Expand Down
29 changes: 14 additions & 15 deletions tests/unit/CardsSectionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,14 @@ describe('CardSectionUtils', () => {
});

it('should return undefined by default', () => {
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toBeUndefined();
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toBeUndefined();
});

it('should return POLICY_OWNER_WITH_AMOUNT_OWED variant', () => {
mockGetSubscriptionStatus.mockReturnValue({
status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.policyOwnerAmountOwed.title',
subtitle: 'subscription.billingBanner.policyOwnerAmountOwed.subtitle',
isError: true,
Expand All @@ -119,7 +118,7 @@ describe('CardSectionUtils', () => {
purchaseID: 12345,
} as Purchase;

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, purchase: mockPurchase})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, purchase: mockPurchase, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title',
subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle',
isError: true,
Expand All @@ -132,7 +131,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: undefined})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: undefined, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title',
subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle',
isError: true,
Expand All @@ -145,7 +144,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.policyOwnerUnderInvoicing.title',
subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicing.subtitle',
isError: true,
Expand All @@ -158,7 +157,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.title',
subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.subtitle',
isError: true,
Expand All @@ -171,7 +170,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.BILLING_DISPUTE_PENDING,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.billingDisputePending.title',
subtitle: 'subscription.billingBanner.billingDisputePending.subtitle',
isError: true,
Expand All @@ -184,7 +183,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.cardAuthenticationRequired.title',
subtitle: 'subscription.billingBanner.cardAuthenticationRequired.subtitle',
isError: true,
Expand All @@ -197,7 +196,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.INSUFFICIENT_FUNDS,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.insufficientFunds.title',
subtitle: 'subscription.billingBanner.insufficientFunds.subtitle',
isError: true,
Expand All @@ -210,14 +209,14 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.CARD_EXPIRED,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: {...ACCOUNT_DATA, cardYear: 2023}})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: {...ACCOUNT_DATA, cardYear: 2023}, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.cardExpired.title',
subtitle: 'subscription.billingBanner.cardExpired.subtitle',
isError: true,
isRetryAvailable: false,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.cardExpired.title',
subtitle: 'subscription.billingBanner.cardExpired.subtitle',
isError: true,
Expand All @@ -230,7 +229,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.CARD_EXPIRE_SOON,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.cardExpireSoon.title',
subtitle: 'subscription.billingBanner.cardExpireSoon.subtitle',
isError: false,
Expand All @@ -243,7 +242,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.retryBillingSuccess.title',
subtitle: 'subscription.billingBanner.retryBillingSuccess.subtitle',
isError: false,
Expand All @@ -256,7 +255,7 @@ describe('CardSectionUtils', () => {
status: PAYMENT_STATUS.RETRY_BILLING_ERROR,
});

expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA})).toEqual({
expect(CardSectionUtils.getBillingStatus({translate: translateMock, stripeCustomerId, accountData: ACCOUNT_DATA, retryBillingSuccessful: false})).toEqual({
title: 'subscription.billingBanner.retryBillingError.title',
subtitle: 'subscription.billingBanner.retryBillingError.subtitle',
isError: true,
Expand Down
Loading
Loading