diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx index d812e839dd..6db0b94e09 100644 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx +++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx @@ -111,7 +111,7 @@ const IndividualSubscriptionCardContent: React.FC< IndividualSubscriptionCardContentProps > = ({ userDetails }) => { const totalStorage = - userDetails.subscription.storage + (userDetails.storageBonus ?? 0); + userDetails.subscription.storage + userDetails.storageBonus; return ( <> @@ -230,7 +230,7 @@ const FamilySubscriptionCardContent: React.FC< } }, [userDetails]); const totalStorage = - userDetails.familyData.storage + (userDetails.storageBonus ?? 0); + userDetails.familyData.storage + userDetails.storageBonus; return ( <> diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx index 2bbae10bfb..bf6c011ca7 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx @@ -105,8 +105,8 @@ const PlanSelectorCard: React.FC = ({ const userDetails = useUserDetailsSnapshot(); const [plansData, setPlansData] = useState(); - const [planPeriod, setPlanPeriod] = useState( - userDetails?.subscription?.period || "month", + const [planPeriod, setPlanPeriod] = useState( + userDetails?.subscription?.period, ); const usage = userDetails ? planUsage(userDetails) : 0; @@ -443,9 +443,7 @@ function PaidSubscriptionPlanSelectorCard({ function PeriodToggler({ planPeriod, togglePeriod }) { const handleChange = (_, newPlanPeriod: PlanPeriod) => { - if (newPlanPeriod !== null && newPlanPeriod !== planPeriod) { - togglePeriod(); - } + if (newPlanPeriod !== planPeriod) togglePeriod(); }; return ( diff --git a/web/packages/new/photos/services/plan.ts b/web/packages/new/photos/services/plan.ts index a091f13bb2..e7bb52418a 100644 --- a/web/packages/new/photos/services/plan.ts +++ b/web/packages/new/photos/services/plan.ts @@ -10,28 +10,40 @@ import { z } from "zod"; import type { UserDetails } from "./user"; import { syncUserDetails, userDetailsSnapshot } from "./user"; -const PlanPeriod = z.enum(["month", "year"]); - /** * Validity of the plan. */ -export type PlanPeriod = z.infer; +export type PlanPeriod = "month" | "year"; export const Subscription = z.object({ + /** + * Store-specific ID of the product ("plan") that the user has subscribed + * to. e.g. if the user has subscribed to a plan using Stripe, then this + * will be the stripeID of the corresponding {@link Plan}. + * + * For free plans, the productID will be the constant "free". + */ productID: z.string(), + /** + * Storage (in bytes) that the user can use. + */ storage: z.number(), + /** + * Epoch microseconds indicating the time until which the user's + * subscription is valid. + */ expiryTime: z.number(), paymentProvider: z.string(), + price: z.string(), + period: z + .string() + .transform((p) => (p == "month" || p == "year" ? p : undefined)), attributes: z .object({ isCancelled: z.boolean().nullish().transform(nullToUndefined), }) .nullish() .transform(nullToUndefined), - price: z.string(), - // TODO: We get back subscriptions without a period on cancel / reactivate. - // Handle them better, or remove this TODO. - period: z.enum(["month", "year", ""]).transform((s) => (s ? s : "month")), }); /** @@ -121,7 +133,9 @@ const Plan = z.object({ stripeID: z.string().nullish().transform(nullToUndefined), storage: z.number(), price: z.string(), - period: PlanPeriod, + period: z + .string() + .transform((p) => (p == "month" || p == "year" ? p : undefined)), }); /** @@ -131,7 +145,7 @@ export type Plan = z.infer; const PlansData = z.object({ freePlan: z.object({ - /* Number of bytes available in the free plan */ + /* Number of bytes available in the free plan. */ storage: z.number(), }), plans: z.array(Plan), @@ -399,9 +413,8 @@ export const hasExceededStorageQuota = (userDetails: UserDetails) => { storage = userDetails.familyData?.storage ?? 0; } else { usage = userDetails.usage; - storage = userDetails.subscription?.storage ?? 0; + storage = userDetails.subscription.storage; } - const bonusStorage = userDetails.storageBonus ?? 0; - return usage > storage + bonusStorage; + return usage > storage + userDetails.storageBonus; }; diff --git a/web/packages/new/photos/services/user.ts b/web/packages/new/photos/services/user.ts index e842f443eb..551f09f599 100644 --- a/web/packages/new/photos/services/user.ts +++ b/web/packages/new/photos/services/user.ts @@ -1,9 +1,10 @@ import { authenticatedRequestHeaders, ensureOk } from "@/base/http"; import { getKV, setKV } from "@/base/kv"; import { apiURL } from "@/base/origins"; +import { nullishToZero, nullToUndefined } from "@/utils/transform"; import { getData, LS_KEYS, setLSUser } from "@ente/shared/storage/localStorage"; import { z } from "zod"; -import { FamilyData, Subscription, BonusData } from "./plan"; +import { BonusData, FamilyData, Subscription } from "./plan"; /** * Zod schema for {@link UserDetails} @@ -11,11 +12,11 @@ import { FamilyData, Subscription, BonusData } from "./plan"; const UserDetails = z.object({ email: z.string(), usage: z.number(), - fileCount: z.number().optional(), + fileCount: z.number().nullish().transform(nullishToZero), subscription: Subscription, - familyData: FamilyData.optional(), - storageBonus: z.number().optional(), - bonusData: BonusData.optional(), + familyData: FamilyData.nullish().transform(nullToUndefined), + storageBonus: z.number().nullish().transform(nullishToZero), + bonusData: BonusData.nullish().transform(nullToUndefined), }); export type UserDetails = z.infer; diff --git a/web/packages/utils/transform.ts b/web/packages/utils/transform.ts index 0d400619a4..16e1c1c4e0 100644 --- a/web/packages/utils/transform.ts +++ b/web/packages/utils/transform.ts @@ -1,3 +1,10 @@ -/** Convert `null` to `undefined`, passthrough everything else unchanged. */ +/** + * Convert `null` to `undefined`, passthrough everything else unchanged. + */ export const nullToUndefined = (v: T | null | undefined): T | undefined => v === null ? undefined : v; + +/** + * Convert `null` and `undefined` to `0`, passthrough everything else unchanged. + */ +export const nullishToZero = (v: number | null | undefined) => v ?? 0;