Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the free trial badge to display Start a free trial badge when we create a workspace for a user #48512

Merged
merged 25 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ac12ad3
Changing free trial text
abzokhattab Sep 1, 2024
b1822c0
use getOwnedPaidPolicies instead of hasPaidPolicies
abzokhattab Sep 3, 2024
f6b9286
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 3, 2024
b54212e
cleaning
abzokhattab Sep 3, 2024
6174fec
minor edit
abzokhattab Sep 3, 2024
5be3131
minor edit
abzokhattab Sep 3, 2024
7a681c1
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 4, 2024
e2db201
Fixing tests
abzokhattab Sep 4, 2024
ce9e069
fixing eslint
abzokhattab Sep 4, 2024
7af63e8
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 7, 2024
71b3d2e
Fix free trial text to update with Onyx data changes
abzokhattab Sep 8, 2024
15f6dba
cleaning
abzokhattab Sep 8, 2024
fc76adf
passing badge styles
abzokhattab Sep 8, 2024
d877cf4
fixing lint
abzokhattab Sep 8, 2024
7535793
Cleanup
abzokhattab Sep 9, 2024
c4b3d73
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 10, 2024
413c149
Merge remote-tracking branch 'origin' into change-free-trail-text
abzokhattab Sep 10, 2024
348f7bb
using debounced state
abzokhattab Sep 10, 2024
5a4fe7c
removing stateDebounce
abzokhattab Sep 11, 2024
bee4bec
removing the`Your trial has ended` case
abzokhattab Sep 12, 2024
8810a65
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 12, 2024
4d5ce8e
Enable calling getFreeTrialText if user is offline
abzokhattab Sep 12, 2024
e45328e
Adding React to the FreeTrialBadge imports
abzokhattab Sep 16, 2024
40ef823
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 18, 2024
b8dcfe8
Merge remote-tracking branch 'origin/main' into change-free-trail-text
abzokhattab Sep 18, 2024
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
11 changes: 2 additions & 9 deletions src/components/LHNOptionsList/OptionRowLHN.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React, {useCallback, useRef, useState} from 'react';
import type {GestureResponderEvent, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Badge from '@components/Badge';
import DisplayNames from '@components/DisplayNames';
import Hoverable from '@components/Hoverable';
import Icon from '@components/Icon';
Expand All @@ -26,8 +25,8 @@ import Parser from '@libs/Parser';
import Performance from '@libs/Performance';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportUtils from '@libs/ReportUtils';
import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import FreeTrialBadge from '@pages/settings/Subscription/FreeTrailBadge';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
Expand Down Expand Up @@ -229,13 +228,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
ReportUtils.isSystemChat(report)
}
/>
{ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && (
<Badge
success
text={translate('subscription.badge.freeTrial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()})}
badgeStyles={[styles.mnh0, styles.pl2, styles.pr2, styles.ml1]}
/>
)}
{ReportUtils.isChatUsedForOnboarding(report) && <FreeTrialBadge badgeStyles={[styles.mnh0, styles.pl2, styles.pr2, styles.ml1]} />}
{isStatusVisible && (
<Tooltip
text={statusContent}
Expand Down
10 changes: 2 additions & 8 deletions src/hooks/useSubscriptionPlan.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useMemo} from 'react';
import {useOnyx} from 'react-native-onyx';
import {isPolicyOwner} from '@libs/PolicyUtils';
import {getOwnedPaidPolicies} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
Expand All @@ -10,13 +10,7 @@ function useSubscriptionPlan() {
const [session] = useOnyx(ONYXKEYS.SESSION);

// Filter workspaces in which user is the owner and the type is either corporate (control) or team (collect)
const ownerPolicies = useMemo(
() =>
Object.values(policies ?? {}).filter(
(policy) => isPolicyOwner(policy, session?.accountID ?? -1) && (CONST.POLICY.TYPE.CORPORATE === policy?.type || CONST.POLICY.TYPE.TEAM === policy?.type),
),
[policies, session?.accountID],
);
const ownerPolicies = useMemo(() => getOwnedPaidPolicies(policies, session?.accountID ?? -1), [policies, session?.accountID]);

if (isEmptyObject(ownerPolicies)) {
return null;
Expand Down
5 changes: 5 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ function isPaidGroupPolicy(policy: OnyxEntry<Policy>): boolean {
return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE;
}

function getOwnedPaidPolicies(policies: OnyxCollection<Policy> | null, currentUserAccountID: number): Policy[] {
return Object.values(policies ?? {}).filter((policy): policy is Policy => isPolicyOwner(policy, currentUserAccountID ?? -1) && isPaidGroupPolicy(policy));
}

function isControlPolicy(policy: OnyxEntry<Policy>): boolean {
return policy?.type === CONST.POLICY.TYPE.CORPORATE;
}
Expand Down Expand Up @@ -1042,6 +1046,7 @@ export {
isTaxTrackingEnabled,
shouldShowPolicy,
getActiveAdminWorkspaces,
getOwnedPaidPolicies,
canSendInvoiceFromWorkspace,
canSendInvoice,
hasDependentTags,
Expand Down
1 change: 1 addition & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ type OptionData = {
hasDraftComment?: boolean | null;
keyForList?: string;
searchText?: string;
freeTrialText?: string;
abzokhattab marked this conversation as resolved.
Show resolved Hide resolved
isIOUReportOwner?: boolean | null;
isArchivedRoom?: boolean | null;
shouldShowSubscript?: boolean | null;
Expand Down
85 changes: 64 additions & 21 deletions src/libs/SubscriptionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {differenceInSeconds, fromUnixTime, isAfter, isBefore} from 'date-fns';
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import type {BillingGraceEndPeriod, BillingStatus, Fund, FundList, Policy, StripeCustomerID} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {translateLocal} from './Localize';
import * as PolicyUtils from './PolicyUtils';

const PAYMENT_STATUS = {
Expand Down Expand Up @@ -198,6 +199,15 @@ function hasInsufficientFundsError() {
return billingStatus?.declineReason === 'insufficient_funds' && amountOwed !== 0;
}

/**
* Determines whether the pre-trial billing banner should be shown.
* @param [firstDay] - The start date of the free trial.
* @param [lastDay] - The end date of the free trial.
* @returns True if the pre-trial billing banner should be shown, false otherwise.
*/
function shouldShowPreTrialBillingBanner(firstDay: OnyxEntry<string> = firstDayFreeTrial, lastDay: OnyxEntry<string> = lastDayFreeTrial): boolean {
return !isUserOnFreeTrial(firstDay, lastDay) && !hasUserFreeTrialEnded(lastDay);
}
/**
* @returns The card to be used for subscription billing.
*/
Expand Down Expand Up @@ -355,49 +365,80 @@ function hasSubscriptionGreenDotInfo(): boolean {

/**
* Calculates the remaining number of days of the workspace owner's free trial before it ends.
* @param [lastDay] - The end date of the free trial.
* @returns The remaining number of free trial days.
*/
function calculateRemainingFreeTrialDays(): number {
if (!lastDayFreeTrial) {
function calculateRemainingFreeTrialDays(lastDay: OnyxEntry<string> = lastDayFreeTrial): number {
if (!lastDay) {
return 0;
}

const currentDate = new Date();
const lastDayFreeTrialDate = new Date(`${lastDayFreeTrial}Z`);
const diffInSeconds = differenceInSeconds(lastDayFreeTrialDate, currentDate);
const lastDayDate = new Date(`${lastDay}Z`);
const diffInSeconds = differenceInSeconds(lastDayDate, currentDate);
const diffInDays = Math.ceil(diffInSeconds / 86400);

return diffInDays < 0 ? 0 : diffInDays;
}

/**
* @param policies - The policies collection.
* @param [firstDay] - The start date of the free trial.
* @param [lastDay] - The end date of the free trial.
* @returns The free trial badge text.
*/
function getFreeTrialText(policies: OnyxCollection<Policy> | null, firstDay: OnyxEntry<string> = firstDayFreeTrial, lastDay: OnyxEntry<string> = lastDayFreeTrial): string | undefined {
const ownedPaidPolicies = PolicyUtils.getOwnedPaidPolicies(policies, currentUserAccountID);
if (isEmptyObject(ownedPaidPolicies)) {
return undefined;
}
if (shouldShowPreTrialBillingBanner(firstDay, lastDay)) {
return translateLocal('subscription.billingBanner.preTrial.title');
}
if (isUserOnFreeTrial(firstDay, lastDay)) {
return translateLocal('subscription.billingBanner.trialStarted.title', {numOfDays: calculateRemainingFreeTrialDays(lastDay)});
}
if (hasUserFreeTrialEnded(lastDay)) {
return translateLocal('subscription.billingBanner.trialEnded.title');
}

return undefined;
}

/**
* Whether the workspace's owner is on its free trial period.
* @param [firstDay] - The start date of the free trial.
* @param [lastDay] - The end date of the free trial.
* @returns True if the user is on a free trial, false otherwise.
*/
function isUserOnFreeTrial(): boolean {
if (!firstDayFreeTrial || !lastDayFreeTrial) {
function isUserOnFreeTrial(firstDay: OnyxEntry<string> = firstDayFreeTrial, lastDay: OnyxEntry<string> = lastDayFreeTrial): boolean {
if (!firstDay || !lastDay) {
return false;
}

const currentDate = new Date();

// Free Trials are stored in UTC so the below code will convert the provided UTC datetime to local time
const firstDayFreeTrialDate = new Date(`${firstDayFreeTrial}Z`);
const lastDayFreeTrialDate = new Date(`${lastDayFreeTrial}Z`);
const firstDayDate = new Date(`${firstDay}Z`);
const lastDayDate = new Date(`${lastDay}Z`);

return isAfter(currentDate, firstDayFreeTrialDate) && isBefore(currentDate, lastDayFreeTrialDate);
return isAfter(currentDate, firstDayDate) && isBefore(currentDate, lastDayDate);
}

/**
* Whether the workspace owner's free trial period has ended.
* @param [lastDay] - The end date of the free trial.
* @returns True if the free trial has ended, false otherwise.
*/
function hasUserFreeTrialEnded(): boolean {
if (!lastDayFreeTrial) {
function hasUserFreeTrialEnded(lastDay: OnyxEntry<string> = lastDayFreeTrial): boolean {
if (!lastDay) {
return false;
}

const currentDate = new Date();
const lastDayFreeTrialDate = new Date(`${lastDayFreeTrial}Z`);
const lastDayDate = new Date(`${lastDay}Z`);

return isAfter(currentDate, lastDayFreeTrialDate);
return isAfter(currentDate, lastDayDate);
}

/**
Expand Down Expand Up @@ -449,16 +490,18 @@ function shouldRestrictUserBillableActions(policyID: string): boolean {
export {
calculateRemainingFreeTrialDays,
doesUserHavePaymentCardAdded,
hasUserFreeTrialEnded,
isUserOnFreeTrial,
shouldRestrictUserBillableActions,
getSubscriptionStatus,
hasSubscriptionRedDotError,
getAmountOwed,
getOverdueGracePeriodDate,
getCardForSubscriptionBilling,
getFreeTrialText,
getOverdueGracePeriodDate,
getSubscriptionStatus,
hasCardAuthenticatedError,
hasSubscriptionGreenDotInfo,
hasRetryBillingError,
hasSubscriptionGreenDotInfo,
hasSubscriptionRedDotError,
hasUserFreeTrialEnded,
isUserOnFreeTrial,
PAYMENT_STATUS,
shouldRestrictUserBillableActions,
shouldShowPreTrialBillingBanner,
};
10 changes: 2 additions & 8 deletions src/pages/home/HeaderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, {memo} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import Badge from '@components/Badge';
import Button from '@components/Button';
import CaretWrapper from '@components/CaretWrapper';
import ConfirmModal from '@components/ConfirmModal';
Expand All @@ -25,7 +24,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import FreeTrialBadge from '@pages/settings/Subscription/FreeTrailBadge';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import * as Task from '@userActions/Task';
Expand Down Expand Up @@ -267,12 +266,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
)}
</PressableWithoutFeedback>
<View style={[styles.reportOptions, styles.flexRow, styles.alignItemsCenter]}>
{ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && (
<Badge
success
text={translate('subscription.badge.freeTrial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()})}
/>
)}
{ReportUtils.isChatUsedForOnboarding(report) && <FreeTrialBadge />}
{isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && <TaskHeaderActionButton report={report} />}
{canJoin && !shouldUseNarrowLayout && joinButton}
</View>
Expand Down
8 changes: 5 additions & 3 deletions src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms

const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false);

const freeTrialText = SubscriptionUtils.getFreeTrialText(policies);

useEffect(() => {
Wallet.openInitialSettingsPage();
}, []);
Expand Down Expand Up @@ -219,8 +221,8 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
icon: Expensicons.CreditCard,
routeName: ROUTES.SETTINGS_SUBSCRIPTION,
brickRoadIndicator: !!privateSubscription?.errors || SubscriptionUtils.hasSubscriptionRedDotError() ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
badgeText: SubscriptionUtils.isUserOnFreeTrial() ? translate('subscription.badge.freeTrial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()}) : undefined,
badgeStyle: SubscriptionUtils.isUserOnFreeTrial() ? styles.badgeSuccess : undefined,
badgeText: freeTrialText,
badgeStyle: freeTrialText ? styles.badgeSuccess : undefined,
});
}

Expand All @@ -229,7 +231,7 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
sectionTranslationKey: 'common.workspaces',
items,
};
}, [policies, privateSubscription?.errors, styles.badgeSuccess, styles.workspaceSettingsSectionContainer, subscriptionPlan, translate, allConnectionSyncProgresses]);
}, [allConnectionSyncProgresses, freeTrialText, policies, privateSubscription?.errors, styles.badgeSuccess, styles.workspaceSettingsSectionContainer, subscriptionPlan]);

/**
* Retuns a list of menu items data for general section
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function CardSection() {
};

let BillingBanner: React.ReactNode | undefined;
if (CardSectionUtils.shouldShowPreTrialBillingBanner()) {
if (SubscriptionUtils.shouldShowPreTrialBillingBanner()) {
BillingBanner = <PreTrialBillingBanner />;
} else if (SubscriptionUtils.isUserOnFreeTrial()) {
BillingBanner = <TrialStartedBillingBanner />;
Expand Down
10 changes: 1 addition & 9 deletions src/pages/settings/Subscription/CardSection/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,5 @@ function getNextBillingDate(): string {
return format(nextBillingDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT);
}

function shouldShowPreTrialBillingBanner(): boolean {
return !SubscriptionUtils.isUserOnFreeTrial() && !SubscriptionUtils.hasUserFreeTrialEnded();
}

export default {
getBillingStatus,
shouldShowPreTrialBillingBanner,
getNextBillingDate,
};
export default {getBillingStatus, getNextBillingDate};
export type {BillingStatusResult};
34 changes: 34 additions & 0 deletions src/pages/settings/Subscription/FreeTrailBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Badge from '@components/Badge';
import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import ONYXKEYS from '@src/ONYXKEYS';

type FreeTrialBadgeProps = {
badgeStyles?: StyleProp<ViewStyle>;
};

function FreeTrialBadge({badgeStyles}: FreeTrialBadgeProps) {
abzokhattab marked this conversation as resolved.
Show resolved Hide resolved
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL);
const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL);

const freeTrialText = SubscriptionUtils.getFreeTrialText(policies, firstDayFreeTrial, lastDayFreeTrial);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we use this pattern of passing the Onyx data directly into the function as parameters and, at the same time, initializing the function with the same Onyx data. Instead, would it not be neater to use effect and state to achieve the same as follows?

    const [freeTrialText, setFreeTrialText] = useState<string>();
    useEffect(() => {
        setFreeTrialText(SubscriptionUtils.getFreeTrialText(policies));
    }, [policies, firstDayFreeTrial, lastDayFreeTrial]);  

Copy link
Contributor Author

@abzokhattab abzokhattab Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was concerned that there might be a delay between the data being updated in useOynx and the data fetched from the global oynx.connect used in the function.
So, I decided to pass it as a function argument, thinking that could be the reason for the delayed transition from 'Start a free trial' to 'Your free trial has ended.'

But I will try this approach.


if (!freeTrialText) {
return null;
}

return (
<Badge
success
text={freeTrialText}
badgeStyles={badgeStyles}
/>
);
}

FreeTrialBadge.displayName = 'FreeTrialBadge';

export default FreeTrialBadge;
Loading