diff --git a/assets/images/product-illustrations/arm_with_card_pos.svg b/assets/images/product-illustrations/arm_with_card_pos.svg new file mode 100644 index 000000000000..069df6a5b38a --- /dev/null +++ b/assets/images/product-illustrations/arm_with_card_pos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 99292ed33231..6674a089de9a 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -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', diff --git a/src/GlobalModals.tsx b/src/GlobalModals.tsx index 2f48f53a9987..ed77f5dfbf27 100644 --- a/src/GlobalModals.tsx +++ b/src/GlobalModals.tsx @@ -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. @@ -67,6 +68,9 @@ function GlobalModals() { {/* Proactive app review modal shown when user has completed a trigger action */} + + + diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ab1332db0b2d..71c76ba11955 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -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', @@ -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; diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index f3d5ed5ccb6e..f264fe895ab5 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -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'; @@ -255,6 +256,7 @@ const Illustrations = { // Product Illustrations Abracadabra, + ArmWithCardPos, BigVault, BrokenCompanyCardBankConnection, BrokenMagnifyingGlass, diff --git a/src/components/TrialPaymentReminderModal.tsx b/src/components/TrialPaymentReminderModal.tsx new file mode 100644 index 000000000000..9677e2aad6df --- /dev/null +++ b/src/components/TrialPaymentReminderModal.tsx @@ -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 ( + {}} + isVisible={isVisible} + type={shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} + innerContainerStyle={styles.pv0} + > + + + + + {variant === CONST.TRIAL_REMINDER_VARIANT.NEAR_END && daysRemaining !== undefined && ( + {translate('trialPaymentReminder.trialEndsInDays', {days: daysRemaining})} + )} + {variant === CONST.TRIAL_REMINDER_VARIANT.COUNTDOWN && !!countdownTime && ( + + {translate('trialPaymentReminder.trialEndsCountdown', { + hours: padZero(countdownTime.hours), + minutes: padZero(countdownTime.minutes), + seconds: padZero(countdownTime.seconds), + })} + + )} + + {translate('trialPaymentReminder.title')} + {translate('trialPaymentReminder.subtitle')} + +