From 106994b45831a9e42269cc78d8ddfce6befb8279 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 3 Mar 2026 23:10:26 +0700 Subject: [PATCH 01/16] Introduce free trial countdown pop-up --- .../arm_with_card_pos.svg | 1 + src/CONST/index.ts | 5 + src/Expensify.tsx | 2 + src/ONYXKEYS.ts | 4 + .../Icon/chunks/illustrations.chunk.ts | 2 + src/components/TrialPaymentReminderModal.tsx | 97 +++++++++ .../TrialPaymentReminderModalManager.tsx | 35 ++++ src/hooks/useTrialPaymentReminder.ts | 194 ++++++++++++++++++ src/languages/en.ts | 8 + src/languages/es.ts | 8 + src/libs/SubscriptionUtils.ts | 32 +++ 11 files changed, 388 insertions(+) create mode 100644 assets/images/product-illustrations/arm_with_card_pos.svg create mode 100644 src/components/TrialPaymentReminderModal.tsx create mode 100644 src/components/TrialPaymentReminderModalManager.tsx create mode 100644 src/hooks/useTrialPaymentReminder.ts 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 99b67b2d50d3..82839e192123 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6234,6 +6234,11 @@ const CONST = { CONTROL: 'control', }, 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/Expensify.tsx b/src/Expensify.tsx index 4a397b68801f..0c39eeec5273 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -12,6 +12,7 @@ import ProactiveAppReviewModalManager from './components/ProactiveAppReviewModal import ScreenShareRequestModal from './components/ScreenShareRequestModal'; import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; +import TrialPaymentReminderModalManager from './components/TrialPaymentReminderModalManager'; import UpdateAppModal from './components/UpdateAppModal'; import CONFIG from './CONFIG'; import CONST from './CONST'; @@ -299,6 +300,7 @@ function Expensify() { {updateAvailable && !updateRequired ? : null} {/* Proactive app review modal shown when user has completed a trigger action */} + )} diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6971f8ec98f6..d7ba071f5f43 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -268,6 +268,9 @@ const ONYXKEYS = { /** ID associated with the payment card added by the user. */ NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID', + /** The last dismissed trial payment reminder variation */ + NVP_TRIAL_PAYMENT_REMINDER_DISMISSED: 'nvp_trialPaymentReminderDismissed', + /** The amount owed by the workspace’s owner. */ NVP_PRIVATE_AMOUNT_OWED: 'nvp_private_amountOwed', @@ -1387,6 +1390,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_TRIAL_PAYMENT_REMINDER_DISMISSED]: string; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; [ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL]: string | undefined; diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index 010a41fded30..181402ae77f4 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -54,6 +54,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'; @@ -237,6 +238,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..6052bbfcaace --- /dev/null +++ b/src/components/TrialPaymentReminderModal.tsx @@ -0,0 +1,97 @@ +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')} + +