mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
feat(premium): give failed renewals a billing-cycle grace period (#3066)
This commit is contained in:
1 parent
f5e32aed31
commit
dfdfffe5de
53 files changed
+1974
-1206
No files matched your search
+17
-1
@@ -463,6 +463,22 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
</Trans>
|
||||
);
|
||||
})()
|
||||
) : gracePeriodInfo.isPaymentRecovery ? (
|
||||
(() => {
|
||||
const graceDate = graceEndDate ? getFormattedLongDate(graceEndDate, locale) : undefined;
|
||||
return (
|
||||
<Trans comment="Plutonium subscription card text shown while a failed renewal payment is being retried. {graceDate} is a date already formatted and localized by code; never write a date into the translation.">
|
||||
Your renewal payment failed but{' '}
|
||||
<PerksButton
|
||||
onClick={scrollToPerks}
|
||||
data-flx="app.plutonium.subscription-card.perks-button.scroll-to-perks--9"
|
||||
/>{' '}
|
||||
stay active until{' '}
|
||||
<strong data-flx="app.plutonium.subscription-card.strong--16">{graceDate}</strong>. Update your
|
||||
payment method before then to keep your subscription.
|
||||
</Trans>
|
||||
);
|
||||
})()
|
||||
) : isInGracePeriod ? (
|
||||
(() => {
|
||||
const graceDate = graceEndDate ? getFormattedLongDate(graceEndDate, locale) : undefined;
|
||||
@@ -844,7 +860,7 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = observer(
|
||||
<Trans comment="Billing button that starts subscription cancellation.">Cancel subscription</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{isInGracePeriod && (
|
||||
{isInGracePeriod && !gracePeriodInfo.isPaymentRecovery && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleEndPremiumGracePeriod}
|
||||
|
||||
+20
-5
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
|
||||
import {getPremiumGraceEndDate} from '@app/features/premium/utils/PremiumGrace';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
|
||||
import {MS_PER_DAY} from '@fluxer/date_utils/src/DateConstants';
|
||||
@@ -12,6 +13,7 @@ export interface GracePeriodInfo {
|
||||
isExpired: boolean;
|
||||
graceEndDate: Date | null;
|
||||
showExpiredState: boolean;
|
||||
isPaymentRecovery: boolean;
|
||||
}
|
||||
|
||||
export interface SubscriptionStatusInfo {
|
||||
@@ -82,13 +84,25 @@ export const useSubscriptionStatus = (
|
||||
const isGiftSubscription = Boolean(!billingCycle && hasPaidPremium && !isVisionary && premiumUntil);
|
||||
const gracePeriodInfo = useMemo((): GracePeriodInfo => {
|
||||
if (isVisionary) {
|
||||
return {isInGracePeriod: false, isExpired: false, graceEndDate: null, showExpiredState: false};
|
||||
return {
|
||||
isInGracePeriod: false,
|
||||
isExpired: false,
|
||||
graceEndDate: null,
|
||||
showExpiredState: false,
|
||||
isPaymentRecovery: false,
|
||||
};
|
||||
}
|
||||
const explicitGraceEnd = premiumGraceEndsAt ?? null;
|
||||
const expiryDate = premiumUntil ? new Date(premiumUntil) : null;
|
||||
const graceEndDate = explicitGraceEnd ?? (expiryDate ? new Date(expiryDate.getTime() + 3 * MS_PER_DAY) : null);
|
||||
const graceEndDate = expiryDate ? getPremiumGraceEndDate(expiryDate, explicitGraceEnd) : explicitGraceEnd;
|
||||
if (!graceEndDate) {
|
||||
return {isInGracePeriod: false, isExpired: false, graceEndDate: null, showExpiredState: false};
|
||||
return {
|
||||
isInGracePeriod: false,
|
||||
isExpired: false,
|
||||
graceEndDate: null,
|
||||
showExpiredState: false,
|
||||
isPaymentRecovery: false,
|
||||
};
|
||||
}
|
||||
const now = new Date();
|
||||
const anchorDate = expiryDate ?? graceEndDate;
|
||||
@@ -96,8 +110,9 @@ export const useSubscriptionStatus = (
|
||||
const isInGracePeriod = (!expiryDate || now > expiryDate) && now <= graceEndDate;
|
||||
const isExpired = now > graceEndDate;
|
||||
const showExpiredState = isExpired && now <= expiredStateEndDate;
|
||||
return {isInGracePeriod, isExpired, graceEndDate, showExpiredState};
|
||||
}, [premiumUntil, premiumGraceEndsAt, isVisionary]);
|
||||
const isPaymentRecovery = isInGracePeriod && explicitGraceEnd != null && billingCycle != null;
|
||||
return {isInGracePeriod, isExpired, graceEndDate, showExpiredState, isPaymentRecovery};
|
||||
}, [premiumUntil, premiumGraceEndsAt, isVisionary, billingCycle]);
|
||||
const {isInGracePeriod, isExpired: isFullyExpired, showExpiredState} = gracePeriodInfo;
|
||||
const isPremium = useDeveloperOverride
|
||||
? hasPaidPremium && !isFullyExpired && !perksDisabled
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as NotificationUtils from '@app/features/notification/utils/Notificatio
|
||||
import NativePermission from '@app/features/permissions/system/state/NativePermission';
|
||||
import {resolvePriceAnnouncementCampaign} from '@app/features/premium/config/PriceAnnouncementCampaign';
|
||||
import PremiumState from '@app/features/premium/state/PremiumState';
|
||||
import {getPremiumGraceEndDate} from '@app/features/premium/utils/PremiumGrace';
|
||||
import {canServiceStripeSubscriptions, shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
|
||||
import Nagbar from '@app/features/ui/state/Nagbar';
|
||||
@@ -117,14 +118,11 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
if (!showPremium || !canServiceSubscription) return false;
|
||||
if (nagbarState.forceHidePremiumGracePeriod) return false;
|
||||
if (nagbarState.forcePremiumGracePeriod) return true;
|
||||
if (!user?.premiumUntil || user.premiumType === 2 || premiumWillCancel) return false;
|
||||
if (!user?.premiumUntil || !user.premiumGraceEndsAt || !user.premiumBillingCycle || user.premiumType === 2) {
|
||||
return false;
|
||||
}
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(user.premiumUntil);
|
||||
const gracePeriodMs = 3 * MS_PER_DAY;
|
||||
const graceEndDate = user.premiumGraceEndsAt
|
||||
? new Date(user.premiumGraceEndsAt)
|
||||
: new Date(expiryDate.getTime() + gracePeriodMs);
|
||||
const isInGracePeriod = now > expiryDate && now <= graceEndDate;
|
||||
const isInGracePeriod = now > new Date(user.premiumUntil) && now <= new Date(user.premiumGraceEndsAt);
|
||||
return isInGracePeriod && !nagbarState.premiumGracePeriodDismissed;
|
||||
})();
|
||||
const canShowPremiumExpired = (() => {
|
||||
@@ -134,11 +132,8 @@ export const useNagbarConditions = (): NagbarConditions => {
|
||||
if (!user?.premiumUntil || user.premiumType === 2 || premiumWillCancel) return false;
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(user.premiumUntil);
|
||||
const gracePeriodMs = 3 * MS_PER_DAY;
|
||||
const expiredStateDurationMs = 30 * MS_PER_DAY;
|
||||
const graceEndDate = user.premiumGraceEndsAt
|
||||
? new Date(user.premiumGraceEndsAt)
|
||||
: new Date(expiryDate.getTime() + gracePeriodMs);
|
||||
const graceEndDate = getPremiumGraceEndDate(expiryDate, user.premiumGraceEndsAt);
|
||||
const expiredStateEndDate = new Date(graceEndDate.getTime() + expiredStateDurationMs);
|
||||
const isExpired = now > graceEndDate;
|
||||
const showExpiredState = isExpired && now <= expiredStateEndDate;
|
||||
|
||||
+3
-5
@@ -8,12 +8,12 @@ import {NAGBAR_TONES, NagbarToneKind} from '@app/features/app/components/layout/
|
||||
import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import * as PremiumCommands from '@app/features/premium/commands/PremiumCommands';
|
||||
import {getPremiumGraceEndDate} from '@app/features/premium/utils/PremiumGrace';
|
||||
import {MANAGE_SUBSCRIPTION_DESCRIPTOR} from '@app/features/premium/utils/PremiumMessageDescriptors';
|
||||
import * as NagbarCommands from '@app/features/ui/commands/NagbarCommands';
|
||||
import {openExternalUrl} from '@app/features/ui/utils/NativeUtils';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import * as LocaleUtils from '@app/features/user/utils/LocaleUtils';
|
||||
import {MS_PER_DAY} from '@fluxer/date_utils/src/DateConstants';
|
||||
import {getFormattedLongDate} from '@fluxer/date_utils/src/DateFormatting';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
@@ -62,10 +62,8 @@ export const PremiumGracePeriodNagbar = observer(({isMobile}: {isMobile: boolean
|
||||
const handleDismiss = () => {
|
||||
NagbarCommands.dismissNagbar('premiumGracePeriodDismissed');
|
||||
};
|
||||
if (!user?.premiumUntil || user?.premiumWillCancel) return null;
|
||||
const expiryDate = new Date(user.premiumUntil);
|
||||
const gracePeriodMs = 3 * MS_PER_DAY;
|
||||
const graceEndDate = new Date(expiryDate.getTime() + gracePeriodMs);
|
||||
if (!user?.premiumUntil) return null;
|
||||
const graceEndDate = getPremiumGraceEndDate(new Date(user.premiumUntil), user.premiumGraceEndsAt);
|
||||
const locale = LocaleUtils.getCurrentLocale();
|
||||
const formattedGraceDate = getFormattedLongDate(graceEndDate, locale);
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user