wip checkpoint

This commit is contained in:
Manav Rathi 2024-11-07 11:39:22 +05:30
parent ec1b294425
commit 1f8ddb0c08
No known key found for this signature in database
6 changed files with 145 additions and 117 deletions

View File

@ -19,7 +19,6 @@ import {
} from "@/new/photos/services/collection";
import type { CollectionSummaries } from "@/new/photos/services/collection/ui";
import {
hasAddOnBonus,
hasExceededStorageQuota,
isFamilyAdmin,
isPartOfFamily,
@ -30,6 +29,7 @@ import {
isSubscriptionPastDue,
isSubscriptionStripe,
redirectToCustomerPortal,
userDetailsAddOnBonuses,
} from "@/new/photos/services/plan";
import { isInternalUser } from "@/new/photos/services/settings";
import { syncUserDetails, type UserDetails } from "@/new/photos/services/user";
@ -195,7 +195,9 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
userDetails={userDetails}
onClick={handleSubscriptionCardClick}
/>
<SubscriptionStatus userDetails={userDetails} />
{userDetails && (
<SubscriptionStatus userDetails={userDetails} />
)}
</Box>
{isMemberSubscription && (
<MemberSubscriptionManage
@ -260,8 +262,10 @@ const SubscriptionStatus: React.FC<SubscriptionStatusProps> = ({
return <></>;
}
const hasAddOnBonus = userDetailsAddOnBonuses(userDetails).length > 0;
let message: React.ReactNode;
if (!hasAddOnBonus(userDetails.bonusData)) {
if (!hasAddOnBonus) {
if (isSubscriptionActive(userDetails.subscription)) {
if (isSubscriptionActiveFree(userDetails.subscription)) {
message = t("subscription_info_free");

View File

@ -2,6 +2,7 @@ import { genericRetriableErrorDialogAttributes } from "@/base/components/utils/d
import log from "@/base/log";
import { useUserDetailsSnapshot } from "@/new/photos/components/utils/use-snapshot";
import type {
Bonus,
Plan,
PlanPeriod,
PlansData,
@ -11,7 +12,6 @@ import {
activateSubscription,
cancelSubscription,
getPlansData,
hasAddOnBonus,
isSubscriptionActive,
isSubscriptionActiveFree,
isSubscriptionActivePaid,
@ -20,8 +20,8 @@ import {
planUsage,
redirectToCustomerPortal,
redirectToPaymentsApp,
userDetailsAddOnBonuses,
} from "@/new/photos/services/plan";
import { BonusData } from "@/new/photos/services/user";
import { AppContext } from "@/new/photos/types/context";
import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units";
import { openURL } from "@/new/photos/utils/web";
@ -111,7 +111,9 @@ const PlanSelectorCard: React.FC<PlanSelectorCardProps> = ({
const usage = userDetails ? planUsage(userDetails) : 0;
const subscription = userDetails?.subscription;
const bonusData = userDetails?.bonusData;
const addOnBonuses = userDetails
? userDetailsAddOnBonuses(userDetails)
: [];
const togglePeriod = useCallback(
() => setPlanPeriod((prev) => (prev == "month" ? "year" : "month")),
@ -231,7 +233,7 @@ const PlanSelectorCard: React.FC<PlanSelectorCardProps> = ({
const commonCardData = {
subscription,
bonusData,
addOnBonuses,
closeModal,
planPeriod,
togglePeriod,
@ -244,7 +246,7 @@ const PlanSelectorCard: React.FC<PlanSelectorCardProps> = ({
planPeriod={planPeriod}
onPlanSelect={onPlanSelect}
subscription={subscription}
bonusData={bonusData}
hasAddOnBonus={addOnBonuses.length > 0}
closeModal={closeModal}
/>
);
@ -307,7 +309,7 @@ const planSelectionOutcome = (subscription: Subscription | undefined) => {
function FreeSubscriptionPlanSelectorCard({
children,
subscription,
bonusData,
addOnBonuses,
closeModal,
setLoading,
planPeriod,
@ -331,19 +333,19 @@ function FreeSubscriptionPlanSelectorCard({
</Typography>
</Box>
{children}
{hasAddOnBonus(bonusData) && (
<BFAddOnRow
bonusData={bonusData}
closeModal={closeModal}
/>
)}
{hasAddOnBonus(bonusData) && (
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
{addOnBonuses.length > 0 && (
<>
<AddOnBonusRows
addOnBonuses={addOnBonuses}
closeModal={closeModal}
/>
<ManageSubscription
subscription={subscription}
hasAddOnBonus={true}
closeModal={closeModal}
setLoading={setLoading}
/>
</>
)}
</Stack>
</Box>
@ -354,7 +356,7 @@ function FreeSubscriptionPlanSelectorCard({
function PaidSubscriptionPlanSelectorCard({
children,
subscription,
bonusData,
addOnBonuses,
closeModal,
usage,
planPeriod,
@ -420,9 +422,9 @@ function PaidSubscriptionPlanSelectorCard({
date: subscription.expiryTime,
})}
</Typography>
{hasAddOnBonus(bonusData) && (
<BFAddOnRow
bonusData={bonusData}
{addOnBonuses.length > 0 && (
<AddOnBonusRows
addOnBonuses={addOnBonuses}
closeModal={closeModal}
/>
)}
@ -431,7 +433,7 @@ function PaidSubscriptionPlanSelectorCard({
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
hasAddOnBonus={addOnBonuses.length > 0}
closeModal={closeModal}
setLoading={setLoading}
/>
@ -485,7 +487,7 @@ interface PlansProps {
plansData: PlansData | undefined;
planPeriod: PlanPeriod;
subscription: Subscription;
bonusData?: BonusData;
hasAddOnBonus: boolean;
onPlanSelect: (plan: Plan) => void;
closeModal: () => void;
}
@ -494,7 +496,7 @@ const Plans = ({
plansData,
planPeriod,
subscription,
bonusData,
hasAddOnBonus,
onPlanSelect,
closeModal,
}: PlansProps) => {
@ -514,7 +516,7 @@ const Plans = ({
/>
))}
{!isSubscriptionActivePaid(subscription) &&
!hasAddOnBonus(bonusData) &&
!hasAddOnBonus &&
freePlan && (
<FreePlanRow
storage={freePlan.storage}
@ -672,35 +674,32 @@ const FreePlanRow_ = styled(SpaceBetweenFlex)(({ theme }) => ({
},
}));
function BFAddOnRow({ bonusData, closeModal }) {
return (
<>
{bonusData.storageBonuses.map((bonus) => {
if (bonus.type.startsWith("ADD_ON")) {
return (
<AddOnRowContainer key={bonus.id} onClick={closeModal}>
<Box>
<Typography color="text.muted">
<Trans
i18nKey={"add_on_valid_till"}
values={{
storage: formattedStorageByteSize(
bonus.storage,
),
date: bonus.validTill,
}}
/>
</Typography>
</Box>
</AddOnRowContainer>
);
}
return null;
})}
</>
);
interface AddOnBonusRowsProps {
addOnBonuses: Bonus[];
closeModal: () => void;
}
const AddOnBonusRows: React.FC<AddOnBonusRowsProps> = ({
addOnBonuses,
closeModal,
}) => (
<>
{addOnBonuses.map((bonus, i) => (
<AddOnRowContainer key={i} onClick={closeModal}>
<Typography color="text.muted">
<Trans
i18nKey={"add_on_valid_till"}
values={{
storage: formattedStorageByteSize(bonus.storage),
date: bonus.validTill,
}}
/>
</Typography>
</AddOnRowContainer>
))}
</>
);
const AddOnRowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({
// gap: theme.spacing(1.5),
padding: theme.spacing(1, 0),
@ -712,14 +711,14 @@ const AddOnRowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({
interface ManageSubscriptionProps {
subscription: Subscription;
bonusData?: BonusData;
hasAddOnBonus: boolean;
closeModal: () => void;
setLoading: SetLoading;
}
function ManageSubscription({
subscription,
bonusData,
hasAddOnBonus,
closeModal,
setLoading,
}: ManageSubscriptionProps) {
@ -745,7 +744,7 @@ function ManageSubscription({
{isSubscriptionStripe(subscription) && (
<StripeSubscriptionOptions
subscription={subscription}
bonusData={bonusData}
hasAddOnBonus={hasAddOnBonus}
closeModal={closeModal}
setLoading={setLoading}
/>
@ -762,7 +761,7 @@ function ManageSubscription({
function StripeSubscriptionOptions({
subscription,
bonusData,
hasAddOnBonus,
setLoading,
closeModal,
}: ManageSubscriptionProps) {
@ -810,7 +809,7 @@ function StripeSubscriptionOptions({
const confirmCancel = () =>
appContext.setDialogMessage({
title: t("CANCEL_SUBSCRIPTION"),
content: hasAddOnBonus(bonusData) ? (
content: hasAddOnBonus ? (
<Trans i18nKey={"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE"} />
) : (
<Trans i18nKey={"CANCEL_SUBSCRIPTION_MESSAGE"} />

View File

@ -104,6 +104,8 @@ const m3 = () =>
// TODO: Not enabled yet since it is not critical. Enable with next batch of changes.
// // Added: Nov 2025 (v1.7.7-beta). Prunable.
// const m4 = () => {
// // Delete the legacy key that used to store the map preference.
// // Delete old local storage keys that have been subsumed elsewhere.
// localStorage.removeItem("mapEnabled");
// localStorage.removeItem("userDetails");
// localStorage.removeItem("familyData");
// };

View File

@ -7,7 +7,7 @@ import { nullToUndefined } from "@/utils/transform";
import { LS_KEYS, setData } from "@ente/shared/storage/localStorage";
import isElectron from "is-electron";
import { z } from "zod";
import type { BonusData, UserDetails } from "./user";
import type { UserDetails } from "./user";
import { syncUserDetails, userDetailsSnapshot } from "./user";
const PlanPeriod = z.enum(["month", "year"]);
@ -79,6 +79,38 @@ export const FamilyData = z.object({
*/
export type FamilyData = z.infer<typeof FamilyData>;
const Bonus = z.object({
/**
* The type of the bonus.
*/
type: z.string(),
/**
* Amount of storage bonus (in bytes) added to the account.
*/
storage: z.number(),
/**
* Validity of the storage bonus. If it is 0, it is valid forever.
*/
validTill: z.number(),
});
/**
* Details about an individual bonus applied for the user.
*/
export type Bonus = z.infer<typeof Bonus>;
export const BonusData = z.object({
/**
* List of bonuses applied for the user.
*/
storageBonuses: Bonus.array(),
});
/**
* Information about bonuses applied for the user.
*/
export type BonusData = z.infer<typeof BonusData>;
/**
* Zod schema for an individual plan received in the list of plans.
*/
@ -262,40 +294,6 @@ export const isSubscriptionStripe = (subscription: Subscription) =>
export const isSubscriptionCancelled = (subscription: Subscription) =>
subscription && subscription.attributes?.isCancelled;
export function isSubscriptionPastDue(subscription: Subscription) {
const thirtyDaysMicroseconds = 30 * 24 * 60 * 60 * 1000 * 1000;
const currentTime = Date.now() * 1000;
return (
!isSubscriptionCancelled(subscription) &&
subscription.expiryTime < currentTime &&
subscription.expiryTime >= currentTime - thirtyDaysMicroseconds
);
}
// Checks if the bonus data contain any bonus whose type starts with 'ADD_ON'
export function hasAddOnBonus(bonusData?: BonusData) {
return (
bonusData &&
bonusData.storageBonuses &&
bonusData.storageBonuses.length > 0 &&
bonusData.storageBonuses.some((bonus) =>
bonus.type.startsWith("ADD_ON"),
)
);
}
export function hasExceededStorageQuota(userDetails: UserDetails) {
const bonusStorage = userDetails.storageBonus ?? 0;
if (userDetails.familyData && isPartOfFamily(userDetails.familyData)) {
const usage = getTotalFamilyUsage(userDetails.familyData);
return usage > userDetails.familyData.storage + bonusStorage;
} else {
return (
userDetails.usage > userDetails.subscription.storage + bonusStorage
);
}
}
/**
* Return true if the user (represented by the given {@link userDetails}) is
* part of a family plan.
@ -374,3 +372,46 @@ export const leaveFamily = async () => {
);
return syncUserDetails();
};
/**
* Return true if the given {@link Subscription} has expired, and is also beyond
* the grace period.
*/
export const isSubscriptionPastDue = (subscription: Subscription) => {
const thirtyDaysMicroseconds = 30 * 24 * 60 * 60 * 1000 * 1000;
const currentTime = Date.now() * 1000;
return (
!isSubscriptionCancelled(subscription) &&
subscription.expiryTime < currentTime &&
subscription.expiryTime >= currentTime - thirtyDaysMicroseconds
);
};
/**
* Return the bonuses whose type starts with "ADD_ON" applicable for the user
* (represented by the given {@link userDetails}).
*/
export const userDetailsAddOnBonuses = (userDetails: UserDetails) =>
userDetails.bonusData?.storageBonuses?.filter((bonus) =>
bonus.type.startsWith("ADD_ON"),
) ?? [];
/**
* Return true if the user (represented by the given {@link userDetails}) has a
* exceeded their storage quota (individual or of the family plan they are a
* part of).
*/
export const hasExceededStorageQuota = (userDetails: UserDetails) => {
let usage: number;
let storage: number;
if (isPartOfFamily(userDetails)) {
usage = familyUsage(userDetails);
storage = userDetails.familyData?.storage ?? 0;
} else {
usage = userDetails.usage;
storage = userDetails.subscription?.storage ?? 0;
}
const bonusStorage = userDetails.storageBonus ?? 0;
return usage > storage + bonusStorage;
};

View File

@ -3,23 +3,7 @@ import { getKV, setKV } from "@/base/kv";
import { apiURL } from "@/base/origins";
import { getData, LS_KEYS, setLSUser } from "@ente/shared/storage/localStorage";
import { z } from "zod";
import { FamilyData, Subscription } from "./plan";
const BonusData = z.object({
/**
* List of bonuses applied for the user.
*/
storageBonuses: z
.object({
type: z.string() /** The type of the bonus. */,
})
.array(),
});
/**
* Information about bonuses applied to the user.
*/
export type BonusData = z.infer<typeof BonusData>;
import { FamilyData, Subscription, BonusData } from "./plan";
/**
* Zod schema for {@link UserDetails}

View File

@ -6,13 +6,11 @@ export enum LS_KEYS {
KEY_ATTRIBUTES = "keyAttributes",
ORIGINAL_KEY_ATTRIBUTES = "originalKeyAttributes",
SUBSCRIPTION = "subscription",
FAMILY_DATA = "familyData",
IS_FIRST_LOGIN = "isFirstLogin",
JUST_SIGNED_UP = "justSignedUp",
SHOW_BACK_BUTTON = "showBackButton",
EXPORT = "export",
// LOGS = "logs",
USER_DETAILS = "userDetails",
// Migrated to (and only used by) useCollectionsSortByLocalState.
COLLECTION_SORT_BY = "collectionSortBy",
THEME = "theme",