[mobile][web] Redirect to payment portal if subscription is past due (#1222)
## Description When a customer whose Stripe subscription is past due (within the 30 day window after expiry time and has not been cancelled) clicks on the subscription modal, take them to the payment portal to complete the subscription. ## Tests - [x] Tested web - [x] Tested mobile
This commit is contained in:
commit
eef33e9c0c
6 changed files with 58 additions and 7 deletions
|
@ -30,6 +30,19 @@ class Subscription {
|
|||
return expiryTime > DateTime.now().microsecondsSinceEpoch;
|
||||
}
|
||||
|
||||
bool isCancelled() {
|
||||
return attributes?.isCancelled ?? false;
|
||||
}
|
||||
|
||||
bool isPastDue() {
|
||||
return !isCancelled() &&
|
||||
expiryTime < DateTime.now().microsecondsSinceEpoch &&
|
||||
expiryTime >=
|
||||
DateTime.now()
|
||||
.subtract(const Duration(days: 30))
|
||||
.microsecondsSinceEpoch;
|
||||
}
|
||||
|
||||
bool isYearlyPlan() {
|
||||
return 'year' == period;
|
||||
}
|
||||
|
|
|
@ -81,6 +81,11 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
userDetails.hasPaidAddon();
|
||||
_hasActiveSubscription = _currentSubscription!.isValid();
|
||||
_isStripeSubscriber = _currentSubscription!.paymentProvider == stripe;
|
||||
|
||||
if (_isStripeSubscriber && _currentSubscription!.isPastDue()) {
|
||||
_redirectToPaymentPortal();
|
||||
}
|
||||
|
||||
return _filterStripeForUI().then((value) {
|
||||
_hasLoadedData = true;
|
||||
setState(() {});
|
||||
|
@ -254,7 +259,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
singleBorderRadius: 4,
|
||||
alignCaptionedTextToLeft: true,
|
||||
onTap: () async {
|
||||
_onStripSupportedPaymentDetailsTap();
|
||||
_redirectToPaymentPortal();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -295,9 +300,9 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
);
|
||||
}
|
||||
|
||||
// _onStripSupportedPaymentDetailsTap action allows the user to update
|
||||
// _redirectToPaymentPortal action allows the user to update
|
||||
// their stripe payment details
|
||||
void _onStripSupportedPaymentDetailsTap() async {
|
||||
void _redirectToPaymentPortal() async {
|
||||
final String paymentProvider = _currentSubscription!.paymentProvider;
|
||||
switch (_currentSubscription!.paymentProvider) {
|
||||
case stripe:
|
||||
|
|
|
@ -406,6 +406,12 @@ func (c *StripeController) handlePaymentIntentFailed(event stripe.Event, country
|
|||
if err != nil {
|
||||
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
err = c.BillingRepo.UpdateSubscriptionCancellationStatus(userID, true)
|
||||
if err != nil {
|
||||
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
err = c.sendAccountOnHoldEmail(userID)
|
||||
if err != nil {
|
||||
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
isOnFreePlan,
|
||||
isSubscriptionActive,
|
||||
isSubscriptionCancelled,
|
||||
isSubscriptionPastDue,
|
||||
} from "utils/billing";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
|
@ -54,7 +55,10 @@ export default function SubscriptionStatus({
|
|||
showPlanSelectorModal();
|
||||
}
|
||||
} else {
|
||||
if (hasStripeSubscription(userDetails.subscription)) {
|
||||
if (
|
||||
hasStripeSubscription(userDetails.subscription) &&
|
||||
isSubscriptionPastDue(userDetails.subscription)
|
||||
) {
|
||||
billingService.redirectToCustomerPortal();
|
||||
} else {
|
||||
showPlanSelectorModal();
|
||||
|
|
|
@ -4,8 +4,10 @@ import { Box, Skeleton } from "@mui/material";
|
|||
import Typography from "@mui/material/Typography";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import billingService from "services/billingService";
|
||||
import { getUserDetailsV2 } from "services/userService";
|
||||
import { UserDetails } from "types/user";
|
||||
import { hasStripeSubscription, isSubscriptionPastDue } from "utils/billing";
|
||||
import { isFamilyAdmin, isPartOfFamily } from "utils/user/family";
|
||||
import { MemberSubscriptionManage } from "../MemberSubscriptionManage";
|
||||
import SubscriptionCard from "./SubscriptionCard";
|
||||
|
@ -50,9 +52,20 @@ export default function UserDetailsSection({ sidebarView }) {
|
|||
[userDetails],
|
||||
);
|
||||
|
||||
const handleSubscriptionCardClick = isMemberSubscription
|
||||
? openMemberSubscriptionManage
|
||||
: galleryContext.showPlanSelectorModal;
|
||||
const handleSubscriptionCardClick = () => {
|
||||
if (isMemberSubscription) {
|
||||
openMemberSubscriptionManage();
|
||||
} else {
|
||||
if (
|
||||
hasStripeSubscription(userDetails.subscription) &&
|
||||
isSubscriptionPastDue(userDetails.subscription)
|
||||
) {
|
||||
billingService.redirectToCustomerPortal();
|
||||
} else {
|
||||
galleryContext.showPlanSelectorModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -17,6 +17,7 @@ const PAYMENT_PROVIDER_STRIPE = "stripe";
|
|||
const PAYMENT_PROVIDER_APPSTORE = "appstore";
|
||||
const PAYMENT_PROVIDER_PLAYSTORE = "playstore";
|
||||
const FREE_PLAN = "free";
|
||||
const THIRTY_DAYS_IN_MICROSECONDS = 30 * 24 * 60 * 60 * 1000 * 1000;
|
||||
|
||||
enum FAILURE_REASON {
|
||||
AUTHENTICATION_FAILED = "authentication_failed",
|
||||
|
@ -151,6 +152,15 @@ export function hasExceededStorageQuota(userDetails: UserDetails) {
|
|||
}
|
||||
}
|
||||
|
||||
export function isSubscriptionPastDue(subscription: Subscription) {
|
||||
const currentTime = Date.now() * 1000;
|
||||
return (
|
||||
!isSubscriptionCancelled(subscription) &&
|
||||
subscription.expiryTime < currentTime &&
|
||||
subscription.expiryTime >= currentTime - THIRTY_DAYS_IN_MICROSECONDS
|
||||
);
|
||||
}
|
||||
|
||||
export function isPopularPlan(plan: Plan) {
|
||||
return plan.storage === 100 * ONE_GB;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue