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 @@ -6825,6 +6825,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 @@ -11,6 +11,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 @@ -67,6 +68,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 @@ -278,6 +278,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 @@ -1535,6 +1538,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 @@ -57,6 +57,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 @@ -255,6 +256,7 @@ const Illustrations = {

// Product Illustrations
Abracadabra,
ArmWithCardPos,
BigVault,
BrokenCompanyCardBankConnection,
BrokenMagnifyingGlass,
Expand Down
95 changes: 95 additions & 0 deletions src/components/TrialPaymentReminderModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import {View} from 'react-native';
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 colors from '@styles/theme/colors';
import CONST from '@src/CONST';
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}
>
<View style={[styles.alignItemsCenter, styles.wAuto, {backgroundColor: colors.blue800, height: CONST.CONFIRM_CONTENT_SVG_SIZE.HEIGHT}, 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', {days: 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
style={[styles.mt5]}
onPress={onClose}
text={translate('trialPaymentReminder.closeButton')}
large
/>
<Button
success
style={[styles.mt3]}
onPress={onAddPaymentCard}
pressOnEnter
text={translate('trialPaymentReminder.addPaymentCardButton')}
large
/>
</View>
</Modal>
);
}

export default TrialPaymentReminderModal;
50 changes: 50 additions & 0 deletions src/components/TrialPaymentReminderModalManager.tsx
Comment thread
dukenv0307 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, {useCallback, useState} from 'react';
import useOnyx from '@hooks/useOnyx';
import useTrialPaymentReminder from '@hooks/useTrialPaymentReminder';
import Navigation from '@libs/Navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import TrialPaymentReminderModal from './TrialPaymentReminderModal';

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

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

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

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

const handleAddPaymentCard = useCallback(() => {
setIsModalOpen(false);
dismiss();
Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD);
}, [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