Skip to content
Open
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6448,6 +6448,11 @@ const CONST = {
},
ONBOARDING_JOINABLE_WORKSPACES_LIMIT: 5,
ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?',
TRIAL_REMINDER_VARIANT: {
BASIC: 'basic',
NEAR_END: 'nearEnd',
COUNTDOWN: 'countdown',
},
ONBOARDING_ACCOUNTING_MAPPING,

REPORT_FIELD_TITLE_FIELD_ID: 'text_title',
Expand Down
4 changes: 4 additions & 0 deletions src/GlobalModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const LazyPopoverReportActionContextMenu = React.lazy(() => import('./pages/inbo
const LazyUpdateAppModal = React.lazy(() => import('./components/UpdateAppModal'));
const LazyScreenShareRequestModal = React.lazy(() => import('./components/ScreenShareRequestModal'));
const LazyProactiveAppReviewModalManager = React.lazy(() => import('./components/ProactiveAppReviewModalManager'));
const LazyTrialPaymentReminderModalManager = React.lazy(() => import('./components/TrialPaymentReminderModalManager'));

// Maximum time (ms) the context menu mount can stay deferred before requestIdleCallback forces it to run,
// guaranteeing mount even if the main thread never becomes idle.
Expand Down Expand Up @@ -68,6 +69,9 @@ function GlobalModals() {
{/* Proactive app review modal shown when user has completed a trigger action */}
<LazyProactiveAppReviewModalManager />
</LazyModalSlot>
<LazyModalSlot>
<LazyTrialPaymentReminderModalManager />
</LazyModalSlot>
<LazyModalSlot>
<LazyScreenShareRequestModal />
</LazyModalSlot>
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ const ONYXKEYS = {
/** ID associated with the payment card added by the user. */
NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID',

/** ISO timestamp of the last time the trial payment reminder was dismissed */
NVP_DISMISSED_TRIAL_PAYMENT_REMINDER: 'nvp_dismissedTrialPaymentReminder',

/** The user's freebie credits balance (in cents). */
NVP_PRIVATE_FREEBIE_CREDITS: 'nvp_private_freebieCredits',

Expand Down Expand Up @@ -1659,6 +1662,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string;
[ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string;
[ONYXKEYS.NVP_BILLING_FUND_ID]: number;
[ONYXKEYS.NVP_DISMISSED_TRIAL_PAYMENT_REMINDER]: string;
[ONYXKEYS.NVP_PRIVATE_FREEBIE_CREDITS]: number;
[ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number;
[ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number;
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/chunks/illustrations.chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import RunOutOfTime from '@assets/images/multifactorAuthentication/running-out-o
import PendingTravel from '@assets/images/pending-travel.svg';
// Product Illustrations
import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
import ArmWithCardPos from '@assets/images/product-illustrations/arm_with_card_pos.svg';
import BigVault from '@assets/images/product-illustrations/big-vault.svg';
import BrokenCompanyCardBankConnection from '@assets/images/product-illustrations/broken-humpty-dumpty.svg';
import BrokenMagnifyingGlass from '@assets/images/product-illustrations/broken-magnifying-glass.svg';
Expand Down Expand Up @@ -270,6 +271,7 @@ const Illustrations = {

// Product Illustrations
Abracadabra,
ArmWithCardPos,
BigVault,
BrokenCompanyCardBankConnection,
BrokenMagnifyingGlass,
Expand Down
98 changes: 98 additions & 0 deletions src/components/TrialPaymentReminderModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import type {CountdownTime, TrialReminderVariant} from '@hooks/useTrialPaymentReminder';

import CONST from '@src/CONST';

import React from 'react';
import {View} from 'react-native';

import Button from './Button';
import ImageSVG from './ImageSVG';
import Modal from './Modal';
import Text from './Text';

type TrialPaymentReminderModalProps = {
/** Whether the modal is visible */
isVisible: boolean;

/** The variant of the modal to display */
variant: TrialReminderVariant;

/** Number of days remaining for 'nearEnd' variant */
daysRemaining?: number;

/** Countdown time for 'countdown' variant */
countdownTime?: CountdownTime;

/** Called when user presses Close */
onClose: () => void;

/** Called when user presses Add payment card */
onAddPaymentCard: () => void;
};

function padZero(num: number): string {
return num.toString().padStart(2, '0');
}

function TrialPaymentReminderModal({isVisible, variant, daysRemaining, countdownTime, onClose, onAddPaymentCard}: TrialPaymentReminderModalProps) {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const styles = useThemeStyles();
const illustrations = useMemoizedLazyIllustrations(['ArmWithCardPos']);
const {translate} = useLocalize();

return (
<Modal
onClose={onClose}
onBackdropPress={() => {}}
isVisible={isVisible}
type={shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
innerContainerStyle={styles.pv0}
shouldHandleNavigationBack
>
<View style={[styles.alignItemsCenter, styles.wAuto, styles.trialReminderIllustrationContainer, styles.pb7]}>
<ImageSVG
src={illustrations.ArmWithCardPos}
contentFit="contain"
/>
</View>
<View style={[styles.m5]}>
{variant === CONST.TRIAL_REMINDER_VARIANT.NEAR_END && daysRemaining !== undefined && (
<Text style={[styles.textSuccess, styles.textStrong, styles.mb2]}>{translate('trialPaymentReminder.trialEndsInDays', {count: daysRemaining})}</Text>
)}
{variant === CONST.TRIAL_REMINDER_VARIANT.COUNTDOWN && !!countdownTime && (
<Text style={[styles.textSuccess, styles.textStrong, styles.mb2]}>
{translate('trialPaymentReminder.trialEndsCountdown', {
hours: padZero(countdownTime.hours),
minutes: padZero(countdownTime.minutes),
seconds: padZero(countdownTime.seconds),
})}
</Text>
)}

<Text style={[styles.textHeadlineH1, styles.mb3]}>{translate('trialPaymentReminder.title')}</Text>
<Text style={[styles.textSupporting]}>{translate('trialPaymentReminder.subtitle')}</Text>

<Button
success
style={[styles.mt5]}
onPress={onAddPaymentCard}
pressOnEnter
text={translate('trialPaymentReminder.addPaymentCardButton')}
large
/>
<Button
style={[styles.mt3]}
onPress={onClose}
text={translate('trialPaymentReminder.closeButton')}
large
/>
</View>
</Modal>
);
}

export default TrialPaymentReminderModal;
59 changes: 59 additions & 0 deletions src/components/TrialPaymentReminderModalManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import useOnyx from '@hooks/useOnyx';
import useRootNavigationState from '@hooks/useRootNavigationState';
import useTrialPaymentReminder from '@hooks/useTrialPaymentReminder';

import {isModalNavigatorName} from '@libs/Navigation/helpers/isNavigatorName';

import navigateToSubscriptionPayment from '@pages/home/common/navigateToSubscriptionPayment';

import ONYXKEYS from '@src/ONYXKEYS';

import React, {useCallback, useState} from 'react';

import TrialPaymentReminderModal from './TrialPaymentReminderModal';

function TrialPaymentReminderModalManager() {
const {isEligibleToShow, currentVariation, countdownTime, dismiss} = useTrialPaymentReminder();
const [modal] = useOnyx(ONYXKEYS.MODAL);
const [isModalOpen, setIsModalOpen] = useState(false);

const isModalNavigatorActive = useRootNavigationState((state) => isModalNavigatorName(state?.routes?.at(-1)?.name));

const isOtherModalActive = !!modal?.isVisible || !!modal?.willAlertModalBecomeVisible || isModalNavigatorActive;

if (isEligibleToShow && !isOtherModalActive && !isModalOpen) {
setIsModalOpen(true);
}
if (!isEligibleToShow && isModalOpen) {
setIsModalOpen(false);
}

const handleClose = useCallback(() => {
setIsModalOpen(false);
dismiss();
}, [dismiss]);

const handleAddPaymentCard = useCallback(() => {
setIsModalOpen(false);
dismiss();
// Adding a payment card is web-only; on native this routes to the subscription page instead.
navigateToSubscriptionPayment();
}, [dismiss]);

if (!currentVariation) {
return null;
}

return (
<TrialPaymentReminderModal
isVisible={isModalOpen}
variant={currentVariation.variant}
daysRemaining={currentVariation.daysRemaining}
countdownTime={countdownTime}
onClose={handleClose}
onAddPaymentCard={handleAddPaymentCard}
/>
);
}

export default TrialPaymentReminderModalManager;
Loading
Loading