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;