From ad94f68b61f73510d164b7b92f0b0f43699dc500 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Sun, 22 Dec 2024 02:13:15 +0100 Subject: [PATCH 01/16] Create discount component --- Mobile-Expensify | 2 +- src/CONST.ts | 1 + src/libs/SubscriptionUtils.ts | 49 +++ src/pages/home/HeaderView.tsx | 294 +++++++++--------- src/pages/home/ReportScreen.tsx | 4 + .../BillingBanner/EarlyDiscountBanner.tsx | 60 ++++ 6 files changed, 266 insertions(+), 144 deletions(-) create mode 100644 src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx diff --git a/Mobile-Expensify b/Mobile-Expensify index 177c9b6cbeb1..d81135521e25 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 177c9b6cbeb11b29e8e48abd3a748891a40f9746 +Subproject commit d81135521e25add10bfebebf83fa5ec4a67cca13 diff --git a/src/CONST.ts b/src/CONST.ts index 4bfaad7b6d1b..d33f791c2e2b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -514,6 +514,7 @@ const CONST = { MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', ORDINAL_DAY_OF_MONTH: 'do', + SECONDS_PER_DAY: 84600, }, SMS: { DOMAIN: '@expensify.sms', diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 87f750957abc..07ccac1f9be9 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -1,6 +1,7 @@ import {differenceInSeconds, fromUnixTime, isAfter, isBefore} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BillingGraceEndPeriod, BillingStatus, Fund, FundList, Policy, StripeCustomerID} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -234,6 +235,52 @@ function hasCardExpiringSoon(): boolean { return isExpiringThisMonth || isExpiringNextMonth; } +function shouldShowDiscountBanner(firstDayFreeTrial: string, lastDayFreeTrial: string): boolean { + if (!isUserOnFreeTrial()) { + return false; + } + + if (doesUserHavePaymentCardAdded()) { + return false; + } + + const dateNow = Date.now() / 1000; + const firstDayTimestamp = new Date(`${firstDayFreeTrial} UTC`).getTime() / 1000; + const lastDayTimestamp = new Date(`${lastDayFreeTrial} UTC`).getTime() / 1000; + if (dateNow > lastDayTimestamp) { + return false; + } + + return dateNow <= firstDayTimestamp + 8 * CONST.DATE.SECONDS_PER_DAY * 1000; +} + +function getDiscountTimeRemaining(firstDayFreeTrial: string | undefined) { + if (!firstDayFreeTrial) { + return null; + } + const dateNow = Date.now() / 1000; + const firstDayTimestamp = new Date(`${firstDayFreeTrial} UTC`).getTime() / 1000; + + let timeLeftInSeconds; + const timeLeft24 = CONST.DATE.SECONDS_PER_DAY - (dateNow - firstDayTimestamp); + if (timeLeft24 > 0) { + timeLeftInSeconds = timeLeft24; + } else { + timeLeftInSeconds = firstDayTimestamp + 8 * CONST.DATE.SECONDS_PER_DAY - dateNow; + } + + if (timeLeftInSeconds <= 0) { + return null; + } + + return { + days: Math.floor(timeLeftInSeconds / 86400), + hours: Math.floor((timeLeftInSeconds % 86400) / 3600), + minutes: Math.floor((timeLeftInSeconds % 3600) / 60), + seconds: Math.floor(timeLeftInSeconds % 60), + }; +} + /** * @returns Whether there is a retry billing error. */ @@ -494,4 +541,6 @@ export { PAYMENT_STATUS, shouldRestrictUserBillableActions, shouldShowPreTrialBillingBanner, + shouldShowDiscountBanner, + getDiscountTimeRemaining, }; diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 03e7dcd82156..117573b262a8 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -10,6 +10,7 @@ import DisplayNames from '@components/DisplayNames'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {FallbackAvatar} from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; @@ -29,12 +30,15 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import * as ReportUtils from '@libs/ReportUtils'; +import {getDiscountTimeRemaining, shouldShowDiscountBanner} from '@libs/SubscriptionUtils'; +import EarlyDiscountBanner from '@pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner'; import FreeTrial from '@pages/settings/Subscription/FreeTrial'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import BillingBanner from '@src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -88,6 +92,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM); + const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL); const isChatThread = ReportUtils.isChatThread(report); const isChatRoom = ReportUtils.isChatRoom(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); @@ -155,164 +160,167 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto const isChatUsedForOnboarding = ReportUtils.isChatUsedForOnboarding(report); return ( - - - {isLoading ? ( - - ) : ( - - {shouldUseNarrowLayout && ( - - + + + {isLoading ? ( + + ) : ( + + {shouldUseNarrowLayout && ( + - - - - - - )} - - ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute())} - style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} - disabled={shouldDisableDetailPage} - accessibilityLabel={title} - role={CONST.ROLE.BUTTON} - > - {shouldShowSubscript ? ( - - ) : ( - - - - )} - + + + + + + )} + + ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute())} + style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} + disabled={shouldDisableDetailPage} + accessibilityLabel={title} + role={CONST.ROLE.BUTTON} > - - - - {!isEmptyObject(parentNavigationSubtitleData) && ( - - )} - {shouldShowSubtitle() && ( - - {subtitle} - + ) : ( + + + )} - {isChatRoom && !!reportDescription && isEmptyObject(parentNavigationSubtitleData) && ( - { - const activeRoute = Navigation.getReportRHPActiveRoute(); - if (ReportUtils.canEditReportDescription(report, policy)) { - Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(reportID, activeRoute)); - return; - } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, activeRoute)); - }} - style={[styles.alignSelfStart, styles.mw100]} - accessibilityLabel={translate('reportDescriptionPage.roomDescription')} - > - + + - {reportDescription} - - - )} - {isPolicyExpenseChat && !!policyDescription && isEmptyObject(parentNavigationSubtitleData) && ( - { - if (ReportUtils.canEditPolicyDescription(policy)) { - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(report.policyID ?? '-1')); - return; - } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, Navigation.getReportRHPActiveRoute())); - }} - style={[styles.alignSelfStart, styles.mw100]} - accessibilityLabel={translate('workspace.editor.descriptionInputLabel')} - > + textStyles={[styles.headerText, styles.pre]} + shouldUseFullTitle={isChatRoom || isPolicyExpenseChat || isChatThread || isTaskReport || shouldUseGroupTitle} + renderAdditionalText={renderAdditionalText} + /> + + {!isEmptyObject(parentNavigationSubtitleData) && ( + + )} + {shouldShowSubtitle() && ( - {policyDescription} + {subtitle} - + )} + {isChatRoom && !!reportDescription && isEmptyObject(parentNavigationSubtitleData) && ( + { + const activeRoute = Navigation.getReportRHPActiveRoute(); + if (ReportUtils.canEditReportDescription(report, policy)) { + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(reportID, activeRoute)); + return; + } + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, activeRoute)); + }} + style={[styles.alignSelfStart, styles.mw100]} + accessibilityLabel={translate('reportDescriptionPage.roomDescription')} + > + + {reportDescription} + + + )} + {isPolicyExpenseChat && !!policyDescription && isEmptyObject(parentNavigationSubtitleData) && ( + { + if (ReportUtils.canEditPolicyDescription(policy)) { + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(report.policyID ?? '-1')); + return; + } + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, Navigation.getReportRHPActiveRoute())); + }} + style={[styles.alignSelfStart, styles.mw100]} + accessibilityLabel={translate('workspace.editor.descriptionInputLabel')} + > + + {policyDescription} + + + )} + + {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( + + + )} + + + {!shouldUseNarrowLayout && isChatUsedForOnboarding && freeTrialButton} + {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } + {!isParentReportLoading && canJoin && !shouldUseNarrowLayout && joinButton} - {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( - - - - )} - - - {!shouldUseNarrowLayout && isChatUsedForOnboarding && freeTrialButton} - {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } - {!isParentReportLoading && canJoin && !shouldUseNarrowLayout && joinButton} + {shouldDisplaySearchRouter && } - {shouldDisplaySearchRouter && } + { + setIsDeleteTaskConfirmModalVisible(false); + Task.deleteTask(report); + }} + onCancel={() => setIsDeleteTaskConfirmModalVisible(false)} + title={translate('task.deleteTask')} + prompt={translate('task.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + shouldEnableNewFocusManagement + /> - { - setIsDeleteTaskConfirmModalVisible(false); - Task.deleteTask(report); - }} - onCancel={() => setIsDeleteTaskConfirmModalVisible(false)} - title={translate('task.deleteTask')} - prompt={translate('task.deleteConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - shouldEnableNewFocusManagement - /> - - )} + )} + + {!isParentReportLoading && !isLoading && canJoin && shouldUseNarrowLayout && {joinButton}} + {!isLoading && isChatUsedForOnboarding && shouldUseNarrowLayout && {freeTrialButton}} - {!isParentReportLoading && !isLoading && canJoin && shouldUseNarrowLayout && {joinButton}} - {!isLoading && isChatUsedForOnboarding && shouldUseNarrowLayout && {freeTrialButton}} - + {shouldShowDiscountBanner() && } + ); } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 97582f75b7b1..5fc98373ef6c 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -339,6 +339,10 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ); } + if (ReportUtils.isAdminRoom(report)) { + console.log('uyes'); + } + /** * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. */ diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx new file mode 100644 index 000000000000..060479cef06c --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx @@ -0,0 +1,60 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import * as Illustrations from '@components/Icon/Illustrations'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Report from '@libs/actions/Report'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; +import {getDiscountTimeRemaining} from '@libs/SubscriptionUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import BillingBanner from './BillingBanner'; + +function EarlyDiscountBanner({timeRemainingProp}) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL); + const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL); + const [timeRemaining, setTimeRemaining] = useState(timeRemainingProp); + const discountType = useMemo(() => (timeRemaining < CONST.DATE.SECONDS_PER_DAY ? 50 : 25), [timeRemaining]); + + useEffect(() => { + const intervalID = setInterval(() => { + setTimeRemaining(getDiscountTimeRemaining(firstDayFreeTrial)); + }, 1000); + + return () => clearInterval(intervalID); + }, [firstDayFreeTrial]); + + const title = ( + + Limited time offer: +  50% off your first year! + + ); + + const formatTimeRemaining = useCallback(() => { + if (timeRemaining.days === 0) { + return `Claim within ${timeRemaining.hours}h : ${timeRemaining.minutes}m : ${timeRemaining.seconds}s`; + } + return `Claim within ${timeRemaining.days}d : ${timeRemaining.hours}h : ${timeRemaining.minutes}m : ${timeRemaining.seconds}s`; + }, [timeRemaining]); + + return ( + + ); +} + +EarlyDiscountBanner.displayName = 'PreTrialBillingBanner'; + +export default EarlyDiscountBanner; From f99771998cc60a7ad268f57c97ab41a5a15360e2 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Sun, 22 Dec 2024 19:22:48 +0100 Subject: [PATCH 02/16] Add banner to subscription page --- src/libs/SubscriptionUtils.ts | 4 ++-- src/pages/home/HeaderView.tsx | 3 ++- .../BillingBanner/EarlyDiscountBanner.tsx | 13 +++++++++++-- .../Subscription/CardSection/CardSection.tsx | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 07ccac1f9be9..4cdd10782ff2 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -235,7 +235,7 @@ function hasCardExpiringSoon(): boolean { return isExpiringThisMonth || isExpiringNextMonth; } -function shouldShowDiscountBanner(firstDayFreeTrial: string, lastDayFreeTrial: string): boolean { +function shouldShowDiscountBanner(): boolean { if (!isUserOnFreeTrial()) { return false; } @@ -254,7 +254,7 @@ function shouldShowDiscountBanner(firstDayFreeTrial: string, lastDayFreeTrial: s return dateNow <= firstDayTimestamp + 8 * CONST.DATE.SECONDS_PER_DAY * 1000; } -function getDiscountTimeRemaining(firstDayFreeTrial: string | undefined) { +function getDiscountTimeRemaining() { if (!firstDayFreeTrial) { return null; } diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 117573b262a8..da44a7b57510 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -93,6 +93,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM); const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL); + const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL); const isChatThread = ReportUtils.isChatThread(report); const isChatRoom = ReportUtils.isChatRoom(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); @@ -319,7 +320,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto {!isParentReportLoading && !isLoading && canJoin && shouldUseNarrowLayout && {joinButton}} {!isLoading && isChatUsedForOnboarding && shouldUseNarrowLayout && {freeTrialButton}} - {shouldShowDiscountBanner() && } + {shouldShowDiscountBanner() && ReportUtils.isChatUsedForOnboarding(report) && } ); } diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx index 060479cef06c..eae090791a37 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx @@ -14,18 +14,20 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import BillingBanner from './BillingBanner'; -function EarlyDiscountBanner({timeRemainingProp}) { +function EarlyDiscountBanner({isSubscriptionPage}) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL); const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL); - const [timeRemaining, setTimeRemaining] = useState(timeRemainingProp); + + const [timeRemaining, setTimeRemaining] = useState({}); const discountType = useMemo(() => (timeRemaining < CONST.DATE.SECONDS_PER_DAY ? 50 : 25), [timeRemaining]); useEffect(() => { const intervalID = setInterval(() => { setTimeRemaining(getDiscountTimeRemaining(firstDayFreeTrial)); + console.log('hi'); }, 1000); return () => clearInterval(intervalID); @@ -39,12 +41,19 @@ function EarlyDiscountBanner({timeRemainingProp}) { ); const formatTimeRemaining = useCallback(() => { + if (!timeRemaining) { + return; + } if (timeRemaining.days === 0) { return `Claim within ${timeRemaining.hours}h : ${timeRemaining.minutes}m : ${timeRemaining.seconds}s`; } return `Claim within ${timeRemaining.days}d : ${timeRemaining.hours}h : ${timeRemaining.minutes}m : ${timeRemaining.seconds}s`; }, [timeRemaining]); + if (!firstDayFreeTrial || !lastDayFreeTrial) { + return null; + } + return ( ; + } else if (SubscriptionUtils.shouldShowPreTrialBillingBanner()) { BillingBanner = ; } else if (SubscriptionUtils.isUserOnFreeTrial()) { BillingBanner = ; From 70ffbfa0a7e4202e476cd1c3eb662db4e18b4abe Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 24 Dec 2024 15:07:21 +0100 Subject: [PATCH 03/16] Make 25% discount banner dismissable --- .../CardSection/BillingBanner/EarlyDiscountBanner.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx index 783e063004ca..d8730f3426bb 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/EarlyDiscountBanner.tsx @@ -52,7 +52,6 @@ function EarlyDiscountBanner({isSubscriptionPage}) { }, [discountInfo]); const {shouldUseNarrowLayout} = useResponsiveLayout(); - console.log(shouldUseNarrowLayout); const rightComponent = useMemo(() => { const smallScreenStyle = shouldUseNarrowLayout ? [styles.flex0, styles.flexBasis100, styles.flexRow, styles.justifyContentCenter] : []; return ( @@ -62,7 +61,7 @@ function EarlyDiscountBanner({isSubscriptionPage}) { text="Claim offer" onPress={() => Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION)} /> - {discountInfo?.discountType === 50 && ( + {discountInfo?.discountType === 25 && (