diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx index 3879d3667a..c016bbe0dd 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx @@ -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: ( + , + }} + 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: ( - , - }} - values={{ emailID: "support@ente.io" }} - /> - ), - close: { variant: "secondary" }, - }); + close: { variant: "secondary" }, + }); + break; } } diff --git a/web/apps/photos/src/utils/billing/index.ts b/web/apps/photos/src/utils/billing/index.ts index 1eecbac553..50366e0407 100644 --- a/web/apps/photos/src/utils/billing/index.ts +++ b/web/apps/photos/src/utils/billing/index.ts @@ -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)) {