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)) {