[web] Improve handling of cancelled sub updates (#2635)
This fixes an issue where a user with a cancelled _and_ expired subscription would try to purchase a plan, and would instead get redirected to the updated subscription flow in stripe (instead of the buy flow). Smoke tested a few scenarios locally.
This commit is contained in:
@@ -39,7 +39,6 @@ import {
|
||||
cancelSubscription,
|
||||
getLocalUserSubscription,
|
||||
hasAddOnBonus,
|
||||
hasMobileSubscription,
|
||||
hasPaidSubscription,
|
||||
hasStripeSubscription,
|
||||
isOnFreePlan,
|
||||
@@ -49,6 +48,7 @@ import {
|
||||
isUserSubscribedPlan,
|
||||
manageFamilyMethod,
|
||||
planForSubscription,
|
||||
planSelectionOutcome,
|
||||
updatePaymentMethod,
|
||||
updateSubscription,
|
||||
} from "utils/billing";
|
||||
@@ -177,58 +177,63 @@ function PlanSelectorCard(props: PlanSelectorCardProps) {
|
||||
}, []);
|
||||
|
||||
async function onPlanSelect(plan: Plan) {
|
||||
if (
|
||||
!hasPaidSubscription(subscription) &&
|
||||
!isSubscriptionCancelled(subscription)
|
||||
) {
|
||||
try {
|
||||
props.setLoading(true);
|
||||
await billingService.buySubscription(plan.stripeID);
|
||||
} catch (e) {
|
||||
props.setLoading(false);
|
||||
switch (planSelectionOutcome(subscription)) {
|
||||
case "buyPlan":
|
||||
try {
|
||||
props.setLoading(true);
|
||||
await billingService.buySubscription(plan.stripeID);
|
||||
} catch (e) {
|
||||
props.setLoading(false);
|
||||
appContext.setDialogMessage({
|
||||
title: t("ERROR"),
|
||||
content: t("SUBSCRIPTION_PURCHASE_FAILED"),
|
||||
close: { variant: "critical" },
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "updateSubscriptionToPlan":
|
||||
appContext.setDialogMessage({
|
||||
title: t("ERROR"),
|
||||
content: t("SUBSCRIPTION_PURCHASE_FAILED"),
|
||||
close: { variant: "critical" },
|
||||
title: t("update_subscription_title"),
|
||||
content: t("UPDATE_SUBSCRIPTION_MESSAGE"),
|
||||
proceed: {
|
||||
text: t("UPDATE_SUBSCRIPTION"),
|
||||
action: updateSubscription.bind(
|
||||
null,
|
||||
plan,
|
||||
appContext.setDialogMessage,
|
||||
props.setLoading,
|
||||
props.closeModal,
|
||||
),
|
||||
variant: "accent",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
}
|
||||
} else if (hasStripeSubscription(subscription)) {
|
||||
appContext.setDialogMessage({
|
||||
title: t("update_subscription_title"),
|
||||
content: t("UPDATE_SUBSCRIPTION_MESSAGE"),
|
||||
proceed: {
|
||||
text: t("UPDATE_SUBSCRIPTION"),
|
||||
action: updateSubscription.bind(
|
||||
null,
|
||||
plan,
|
||||
appContext.setDialogMessage,
|
||||
props.setLoading,
|
||||
props.closeModal,
|
||||
break;
|
||||
|
||||
case "cancelOnMobile":
|
||||
appContext.setDialogMessage({
|
||||
title: t("CANCEL_SUBSCRIPTION_ON_MOBILE"),
|
||||
content: t("CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE"),
|
||||
close: { variant: "secondary" },
|
||||
});
|
||||
break;
|
||||
|
||||
case "contactSupport":
|
||||
appContext.setDialogMessage({
|
||||
title: t("MANAGE_PLAN"),
|
||||
content: (
|
||||
<Trans
|
||||
i18nKey={"MAIL_TO_MANAGE_SUBSCRIPTION"}
|
||||
components={{
|
||||
a: <Link href="mailto:support@ente.io" />,
|
||||
}}
|
||||
values={{ emailID: "support@ente.io" }}
|
||||
/>
|
||||
),
|
||||
variant: "accent",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
} else if (hasMobileSubscription(subscription)) {
|
||||
appContext.setDialogMessage({
|
||||
title: t("CANCEL_SUBSCRIPTION_ON_MOBILE"),
|
||||
content: t("CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE"),
|
||||
close: { variant: "secondary" },
|
||||
});
|
||||
} else {
|
||||
appContext.setDialogMessage({
|
||||
title: t("MANAGE_PLAN"),
|
||||
content: (
|
||||
<Trans
|
||||
i18nKey={"MAIL_TO_MANAGE_SUBSCRIPTION"}
|
||||
components={{
|
||||
a: <Link href="mailto:support@ente.io" />,
|
||||
}}
|
||||
values={{ emailID: "support@ente.io" }}
|
||||
/>
|
||||
),
|
||||
close: { variant: "secondary" },
|
||||
});
|
||||
close: { variant: "secondary" },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ import { getSubscriptionPurchaseSuccessMessage } from "utils/ui";
|
||||
import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family";
|
||||
|
||||
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;
|
||||
|
||||
@@ -31,6 +29,51 @@ enum RESPONSE_STATUS {
|
||||
fail = "fail",
|
||||
}
|
||||
|
||||
export type PlanSelectionOutcome =
|
||||
| "buyPlan"
|
||||
| "updateSubscriptionToPlan"
|
||||
| "cancelOnMobile"
|
||||
| "contactSupport";
|
||||
|
||||
/**
|
||||
* Return the outcome that should happen when the user selects a paid plan on
|
||||
* the plan selection screen.
|
||||
*
|
||||
* @param subscription Their current subscription details.
|
||||
*/
|
||||
export const planSelectionOutcome = (
|
||||
subscription: Subscription | undefined,
|
||||
) => {
|
||||
// This shouldn't happen, but we need this case to handle missing types.
|
||||
if (!subscription) return "buyPlan";
|
||||
|
||||
// The user is a on a free plan and can buy the plan they selected.
|
||||
if (subscription.productID == "free") return "buyPlan";
|
||||
|
||||
// Their existing subscription has expired. They can buy a new plan.
|
||||
if (subscription.expiryTime < Date.now() * 1000) return "buyPlan";
|
||||
|
||||
// -- The user already has an active subscription to a paid plan.
|
||||
|
||||
// Using stripe
|
||||
if (subscription.paymentProvider == "stripe") {
|
||||
// Update their existing subscription to the new plan.
|
||||
return "updateSubscriptionToPlan";
|
||||
}
|
||||
|
||||
// Using one of the mobile app stores
|
||||
if (
|
||||
subscription.paymentProvider == "appstore" ||
|
||||
subscription.paymentProvider == "playstore"
|
||||
) {
|
||||
// They need to cancel first on the mobile app stores.
|
||||
return "cancelOnMobile";
|
||||
}
|
||||
|
||||
// Some other bespoke case. They should contact support.
|
||||
return "contactSupport";
|
||||
};
|
||||
|
||||
export function hasPaidSubscription(subscription: Subscription) {
|
||||
return (
|
||||
subscription &&
|
||||
@@ -92,15 +135,6 @@ export function hasStripeSubscription(subscription: Subscription) {
|
||||
);
|
||||
}
|
||||
|
||||
export function hasMobileSubscription(subscription: Subscription) {
|
||||
return (
|
||||
hasPaidSubscription(subscription) &&
|
||||
subscription.paymentProvider.length > 0 &&
|
||||
(subscription.paymentProvider === PAYMENT_PROVIDER_APPSTORE ||
|
||||
subscription.paymentProvider === PAYMENT_PROVIDER_PLAYSTORE)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasExceededStorageQuota(userDetails: UserDetails) {
|
||||
const bonusStorage = userDetails.storageBonus ?? 0;
|
||||
if (isPartOfFamily(userDetails.familyData)) {
|
||||
|
||||
Reference in New Issue
Block a user