[web] Non functional code rearrangement for the plan list (#2597)

This commit is contained in:
Manav Rathi
2024-08-01 21:19:04 +05:30
committed by GitHub
12 changed files with 794 additions and 945 deletions

View File

@@ -1,12 +0,0 @@
import { Box, styled } from "@mui/material";
import { CSSProperties } from "@mui/material/styles/createTypography";
export const Badge = styled(Box)(({ theme }) => ({
borderRadius: theme.shape.borderRadius,
padding: "2px 4px",
backgroundColor: theme.colors.black.muted,
backdropFilter: `blur(${theme.colors.blur.muted})`,
color: theme.colors.white.base,
textTransform: "uppercase",
...(theme.typography.tiny as CSSProperties),
}));

View File

@@ -0,0 +1,794 @@
import log from "@/base/log";
import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units";
import {
FlexWrapper,
FluidContainer,
SpaceBetweenFlex,
} from "@ente/shared/components/Container";
import ArrowForward from "@mui/icons-material/ArrowForward";
import ChevronRight from "@mui/icons-material/ChevronRight";
import Close from "@mui/icons-material/Close";
import Done from "@mui/icons-material/Done";
import {
Button,
ButtonProps,
Dialog,
IconButton,
Link,
Stack,
styled,
ToggleButton,
ToggleButtonGroup,
useMediaQuery,
useTheme,
} from "@mui/material";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { PLAN_PERIOD } from "constants/gallery";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
import { useContext, useEffect, useMemo, useState } from "react";
import { Trans } from "react-i18next";
import billingService, { type PlansResponse } from "services/billingService";
import { Plan, Subscription } from "types/billing";
import { SetLoading } from "types/gallery";
import { BonusData } from "types/user";
import {
activateSubscription,
cancelSubscription,
getLocalUserSubscription,
hasAddOnBonus,
hasMobileSubscription,
hasPaidSubscription,
hasStripeSubscription,
isOnFreePlan,
isPopularPlan,
isSubscriptionActive,
isSubscriptionCancelled,
isUserSubscribedPlan,
manageFamilyMethod,
planForSubscription,
updatePaymentMethod,
updateSubscription,
} from "utils/billing";
import { getLocalUserDetails } from "utils/user";
import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family";
interface PlanSelectorProps {
modalView: boolean;
closeModal: any;
setLoading: SetLoading;
}
function PlanSelector(props: PlanSelectorProps) {
const fullScreen = useMediaQuery(useTheme().breakpoints.down("sm"));
if (!props.modalView) {
return <></>;
}
return (
<Dialog
{...{ fullScreen }}
open={props.modalView}
onClose={props.closeModal}
PaperProps={{
sx: (theme) => ({
width: { sm: "391px" },
p: 1,
[theme.breakpoints.down(360)]: { p: 0 },
}),
}}
>
<PlanSelectorCard
closeModal={props.closeModal}
setLoading={props.setLoading}
/>
</Dialog>
);
}
export default PlanSelector;
interface PlanSelectorCardProps {
closeModal: any;
setLoading: SetLoading;
}
function PlanSelectorCard(props: PlanSelectorCardProps) {
const subscription = useMemo(() => getLocalUserSubscription(), []);
const [plansResponse, setPlansResponse] = useState<
PlansResponse | undefined
>();
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(
subscription?.period || PLAN_PERIOD.MONTH,
);
const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
const bonusData = useMemo(() => {
const userDetails = getLocalUserDetails();
if (!userDetails) {
return null;
}
return userDetails.bonusData;
}, []);
const usage = useMemo(() => {
const userDetails = getLocalUserDetails();
if (!userDetails) {
return 0;
}
return isPartOfFamily(userDetails.familyData)
? getTotalFamilyUsage(userDetails.familyData)
: userDetails.usage;
}, []);
const togglePeriod = () => {
setPlanPeriod((prevPeriod) =>
prevPeriod === PLAN_PERIOD.MONTH
? PLAN_PERIOD.YEAR
: PLAN_PERIOD.MONTH,
);
};
function onReopenClick() {
appContext.closeMessageDialog();
galleryContext.showPlanSelectorModal();
}
useEffect(() => {
const main = async () => {
try {
props.setLoading(true);
const response = await billingService.getPlans();
const { plans } = response;
if (isSubscriptionActive(subscription)) {
const planNotListed =
plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription),
).length === 0;
if (
subscription &&
!isOnFreePlan(subscription) &&
planNotListed
) {
plans.push(planForSubscription(subscription));
}
}
setPlansResponse(response);
} catch (e) {
log.error("plan selector modal open failed", e);
props.closeModal();
appContext.setDialogMessage({
title: t("OPEN_PLAN_SELECTOR_MODAL_FAILED"),
content: t("UNKNOWN_ERROR"),
close: { text: t("CLOSE"), variant: "secondary" },
proceed: {
text: t("REOPEN_PLAN_SELECTOR_MODAL"),
variant: "accent",
action: onReopenClick,
},
});
} finally {
props.setLoading(false);
}
};
main();
}, []);
async function onPlanSelect(plan: Plan) {
if (
!hasPaidSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
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" },
});
}
} 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,
),
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: (
<Trans
i18nKey={"MAIL_TO_MANAGE_SUBSCRIPTION"}
components={{
a: <Link href="mailto:support@ente.io" />,
}}
values={{ emailID: "support@ente.io" }}
/>
),
close: { variant: "secondary" },
});
}
}
const { closeModal, setLoading } = props;
const commonCardData = {
subscription,
bonusData,
closeModal,
planPeriod,
togglePeriod,
setLoading,
};
const plansList = (
<Plans
plansResponse={plansResponse}
planPeriod={planPeriod}
onPlanSelect={onPlanSelect}
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
/>
);
return (
<>
<Stack spacing={3} p={1.5}>
{hasPaidSubscription(subscription) ? (
<PaidSubscriptionPlanSelectorCard
{...commonCardData}
usage={usage}
>
{plansList}
</PaidSubscriptionPlanSelectorCard>
) : (
<FreeSubscriptionPlanSelectorCard {...commonCardData}>
{plansList}
</FreeSubscriptionPlanSelectorCard>
)}
</Stack>
</>
);
}
function FreeSubscriptionPlanSelectorCard({
children,
subscription,
bonusData,
closeModal,
setLoading,
planPeriod,
togglePeriod,
}) {
return (
<>
<Typography variant="h3" fontWeight={"bold"}>
{t("CHOOSE_PLAN")}
</Typography>
<Box>
<Stack spacing={3}>
<Box>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Typography variant="small" mt={0.5} color="text.muted">
{t("TWO_MONTHS_FREE")}
</Typography>
</Box>
{children}
{hasAddOnBonus(bonusData) && (
<BFAddOnRow
bonusData={bonusData}
closeModal={closeModal}
/>
)}
{hasAddOnBonus(bonusData) && (
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
)}
</Stack>
</Box>
</>
);
}
function PaidSubscriptionPlanSelectorCard({
children,
subscription,
bonusData,
closeModal,
usage,
planPeriod,
togglePeriod,
setLoading,
}) {
return (
<>
<Box pl={1.5} py={0.5}>
<SpaceBetweenFlex>
<Box>
<Typography variant="h3" fontWeight={"bold"}>
{t("SUBSCRIPTION")}
</Typography>
<Typography variant="small" color={"text.muted"}>
{bytesInGB(subscription.storage, 2)}{" "}
{t("storage_unit.gb")}
</Typography>
</Box>
<IconButton onClick={closeModal} color="secondary">
<Close />
</IconButton>
</SpaceBetweenFlex>
</Box>
<Box px={1.5}>
<Typography color={"text.muted"} fontWeight={"bold"}>
<Trans
i18nKey="CURRENT_USAGE"
values={{
usage: `${bytesInGB(usage, 2)} ${t("storage_unit.gb")}`,
}}
/>
</Typography>
</Box>
<Box>
<Stack
spacing={3}
border={(theme) => `1px solid ${theme.palette.divider}`}
p={1.5}
borderRadius={(theme) => `${theme.shape.borderRadius}px`}
>
<Box>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Typography variant="small" mt={0.5} color="text.muted">
{t("TWO_MONTHS_FREE")}
</Typography>
</Box>
{children}
</Stack>
<Box py={1} px={1.5}>
<Typography color={"text.muted"}>
{!isSubscriptionCancelled(subscription)
? t("subscription_status_renewal_active", {
date: subscription.expiryTime,
})
: t("subscription_status_renewal_cancelled", {
date: subscription.expiryTime,
})}
</Typography>
{hasAddOnBonus(bonusData) && (
<BFAddOnRow
bonusData={bonusData}
closeModal={closeModal}
/>
)}
</Box>
</Box>
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
</>
);
}
function PeriodToggler({ planPeriod, togglePeriod }) {
const handleChange = (_, newPlanPeriod: PLAN_PERIOD) => {
if (newPlanPeriod !== null && newPlanPeriod !== planPeriod) {
togglePeriod();
}
};
return (
<ToggleButtonGroup
value={planPeriod}
exclusive
onChange={handleChange}
color="primary"
>
<CustomToggleButton value={PLAN_PERIOD.MONTH}>
{t("MONTHLY")}
</CustomToggleButton>
<CustomToggleButton value={PLAN_PERIOD.YEAR}>
{t("YEARLY")}
</CustomToggleButton>
</ToggleButtonGroup>
);
}
const CustomToggleButton = styled(ToggleButton)(({ theme }) => ({
textTransform: "none",
padding: "12px 16px",
borderRadius: "4px",
backgroundColor: theme.colors.fill.faint,
border: `1px solid transparent`,
color: theme.colors.text.faint,
"&.Mui-selected": {
backgroundColor: theme.colors.accent.A500,
color: theme.colors.text.base,
},
"&.Mui-selected:hover": {
backgroundColor: theme.colors.accent.A500,
color: theme.colors.text.base,
},
width: "97.433px",
}));
interface PlansProps {
plansResponse: PlansResponse | undefined;
planPeriod: PLAN_PERIOD;
subscription: Subscription;
bonusData?: BonusData;
onPlanSelect: (plan: Plan) => void;
closeModal: () => void;
}
const Plans = ({
plansResponse,
planPeriod,
subscription,
bonusData,
onPlanSelect,
closeModal,
}: PlansProps) => {
const { freePlan, plans } = plansResponse ?? {};
return (
<Stack spacing={2}>
{plans
?.filter((plan) => plan.period === planPeriod)
?.map((plan) => (
<PlanRow
disabled={isUserSubscribedPlan(plan, subscription)}
popular={isPopularPlan(plan)}
key={plan.stripeID}
plan={plan}
subscription={subscription}
onPlanSelect={onPlanSelect}
/>
))}
{!hasPaidSubscription(subscription) &&
!hasAddOnBonus(bonusData) &&
freePlan && (
<FreePlanRow
storage={freePlan.storage}
closeModal={closeModal}
/>
)}
</Stack>
);
};
interface PlanRowProps {
plan: Plan;
subscription: Subscription;
onPlanSelect: (plan: Plan) => void;
disabled: boolean;
popular: boolean;
}
function PlanRow({
plan,
subscription,
onPlanSelect,
disabled,
popular,
}: PlanRowProps) {
const handleClick = () => {
!isUserSubscribedPlan(plan, subscription) && onPlanSelect(plan);
};
const PlanButton = disabled ? DisabledPlanButton : ActivePlanButton;
return (
<PlanRowContainer>
<TopAlignedFluidContainer>
<Typography variant="h1" fontWeight={"bold"}>
{bytesInGB(plan.storage)}
</Typography>
<FlexWrapper flexWrap={"wrap"} gap={1}>
<Typography variant="h3" color="text.muted">
{t("storage_unit.gb")}
</Typography>
{popular && !hasPaidSubscription(subscription) && (
<Badge>{t("POPULAR")}</Badge>
)}
</FlexWrapper>
</TopAlignedFluidContainer>
<Box width="136px">
<PlanButton
sx={{
justifyContent: "flex-end",
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
size="large"
onClick={handleClick}
>
<Box textAlign={"right"}>
<Typography fontWeight={"bold"} variant="large">
{plan.price}{" "}
</Typography>{" "}
<Typography color="text.muted" variant="small">
{`/ ${
plan.period === PLAN_PERIOD.MONTH
? t("MONTH_SHORT")
: t("YEAR")
}`}
</Typography>
</Box>
</PlanButton>
</Box>
</PlanRowContainer>
);
}
const PlanRowContainer = styled(FlexWrapper)(() => ({
background:
"linear-gradient(268.22deg, rgba(256, 256, 256, 0.08) -3.72%, rgba(256, 256, 256, 0) 85.73%)",
}));
const TopAlignedFluidContainer = styled(FluidContainer)`
align-items: flex-start;
`;
const DisabledPlanButton = styled((props: ButtonProps) => (
<Button disabled endIcon={<Done />} {...props} />
))(({ theme }) => ({
"&.Mui-disabled": {
backgroundColor: "transparent",
color: theme.colors.text.base,
},
}));
const ActivePlanButton = styled((props: ButtonProps) => (
<Button color="accent" {...props} endIcon={<ArrowForward />} />
))(() => ({
".MuiButton-endIcon": {
transition: "transform .2s ease-in-out",
},
"&:hover .MuiButton-endIcon": {
transform: "translateX(4px)",
},
}));
const Badge = styled(Box)(({ theme }) => ({
borderRadius: theme.shape.borderRadius,
padding: "2px 4px",
backgroundColor: theme.colors.black.muted,
backdropFilter: `blur(${theme.colors.blur.muted})`,
color: theme.colors.white.base,
textTransform: "uppercase",
...theme.typography.mini,
}));
interface FreePlanRowProps {
storage: number;
closeModal: () => void;
}
const FreePlanRow: React.FC<FreePlanRowProps> = ({ closeModal, storage }) => {
return (
<FreePlanRow_ onClick={closeModal}>
<Box>
<Typography>{t("free_plan_option")}</Typography>
<Typography variant="small" color="text.muted">
{t("free_plan_description", {
storage: formattedStorageByteSize(storage),
})}
</Typography>
</Box>
<IconButton className={"endIcon"}>
<ArrowForward />
</IconButton>
</FreePlanRow_>
);
};
const FreePlanRow_ = styled(SpaceBetweenFlex)(({ theme }) => ({
gap: theme.spacing(1.5),
padding: theme.spacing(1.5, 1),
cursor: "pointer",
"&:hover .endIcon": {
backgroundColor: "rgba(255,255,255,0.08)",
},
}));
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;
})}
</>
);
}
const AddOnRowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({
// gap: theme.spacing(1.5),
padding: theme.spacing(1, 0),
cursor: "pointer",
"&:hover .endIcon": {
backgroundColor: "rgba(255,255,255,0.08)",
},
}));
interface ManageSubscriptionProps {
subscription: Subscription;
bonusData?: BonusData;
closeModal: () => void;
setLoading: SetLoading;
}
function ManageSubscription({
subscription,
bonusData,
closeModal,
setLoading,
}: ManageSubscriptionProps) {
const appContext = useContext(AppContext);
const openFamilyPortal = () =>
manageFamilyMethod(appContext.setDialogMessage, setLoading);
return (
<Stack spacing={1}>
{hasStripeSubscription(subscription) && (
<StripeSubscriptionOptions
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
)}
<ManageSubscriptionButton
color="secondary"
onClick={openFamilyPortal}
>
{t("MANAGE_FAMILY_PORTAL")}
</ManageSubscriptionButton>
</Stack>
);
}
function StripeSubscriptionOptions({
subscription,
bonusData,
setLoading,
closeModal,
}: ManageSubscriptionProps) {
const appContext = useContext(AppContext);
const confirmReactivation = () =>
appContext.setDialogMessage({
title: t("REACTIVATE_SUBSCRIPTION"),
content: t("REACTIVATE_SUBSCRIPTION_MESSAGE", {
date: subscription.expiryTime,
}),
proceed: {
text: t("REACTIVATE_SUBSCRIPTION"),
action: activateSubscription.bind(
null,
appContext.setDialogMessage,
closeModal,
setLoading,
),
variant: "accent",
},
close: {
text: t("cancel"),
},
});
const confirmCancel = () =>
appContext.setDialogMessage({
title: t("CANCEL_SUBSCRIPTION"),
content: hasAddOnBonus(bonusData) ? (
<Trans i18nKey={"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE"} />
) : (
<Trans i18nKey={"CANCEL_SUBSCRIPTION_MESSAGE"} />
),
proceed: {
text: t("CANCEL_SUBSCRIPTION"),
action: cancelSubscription.bind(
null,
appContext.setDialogMessage,
closeModal,
setLoading,
),
variant: "critical",
},
close: {
text: t("NEVERMIND"),
},
});
const openManagementPortal = updatePaymentMethod.bind(
null,
appContext.setDialogMessage,
setLoading,
);
return (
<>
{isSubscriptionCancelled(subscription) ? (
<ManageSubscriptionButton
color="secondary"
onClick={confirmReactivation}
>
{t("REACTIVATE_SUBSCRIPTION")}
</ManageSubscriptionButton>
) : (
<ManageSubscriptionButton
color="secondary"
onClick={confirmCancel}
>
{t("CANCEL_SUBSCRIPTION")}
</ManageSubscriptionButton>
)}
<ManageSubscriptionButton
color="secondary"
onClick={openManagementPortal}
>
{t("MANAGEMENT_PORTAL")}
</ManageSubscriptionButton>
</>
);
}
const ManageSubscriptionButton = ({ children, ...props }: ButtonProps) => (
<Button size="large" endIcon={<ChevronRight />} {...props}>
<FluidContainer>{children}</FluidContainer>
</Button>
);

View File

@@ -1,355 +0,0 @@
import log from "@/base/log";
import { bytesInGB } from "@/new/photos/utils/units";
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import Close from "@mui/icons-material/Close";
import { IconButton, Link, Stack } from "@mui/material";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { PLAN_PERIOD } from "constants/gallery";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
import { useContext, useEffect, useMemo, useState } from "react";
import { Trans } from "react-i18next";
import billingService, { type PlansResponse } from "services/billingService";
import { Plan } from "types/billing";
import { SetLoading } from "types/gallery";
import {
getLocalUserSubscription,
hasAddOnBonus,
hasMobileSubscription,
hasPaidSubscription,
hasStripeSubscription,
isOnFreePlan,
isSubscriptionActive,
isSubscriptionCancelled,
isUserSubscribedPlan,
planForSubscription,
updateSubscription,
} from "utils/billing";
import { getLocalUserDetails } from "utils/user";
import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family";
import { ManageSubscription } from "./manageSubscription";
import { PeriodToggler } from "./periodToggler";
import Plans from "./plans";
import { BFAddOnRow } from "./plans/BfAddOnRow";
interface Props {
closeModal: any;
setLoading: SetLoading;
}
function PlanSelectorCard(props: Props) {
const subscription = useMemo(() => getLocalUserSubscription(), []);
const [plansResponse, setPlansResponse] = useState<
PlansResponse | undefined
>();
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(
subscription?.period || PLAN_PERIOD.MONTH,
);
const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
const bonusData = useMemo(() => {
const userDetails = getLocalUserDetails();
if (!userDetails) {
return null;
}
return userDetails.bonusData;
}, []);
const usage = useMemo(() => {
const userDetails = getLocalUserDetails();
if (!userDetails) {
return 0;
}
return isPartOfFamily(userDetails.familyData)
? getTotalFamilyUsage(userDetails.familyData)
: userDetails.usage;
}, []);
const togglePeriod = () => {
setPlanPeriod((prevPeriod) =>
prevPeriod === PLAN_PERIOD.MONTH
? PLAN_PERIOD.YEAR
: PLAN_PERIOD.MONTH,
);
};
function onReopenClick() {
appContext.closeMessageDialog();
galleryContext.showPlanSelectorModal();
}
useEffect(() => {
const main = async () => {
try {
props.setLoading(true);
const response = await billingService.getPlans();
const { plans } = response;
if (isSubscriptionActive(subscription)) {
const planNotListed =
plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription),
).length === 0;
if (
subscription &&
!isOnFreePlan(subscription) &&
planNotListed
) {
plans.push(planForSubscription(subscription));
}
}
setPlansResponse(response);
} catch (e) {
log.error("plan selector modal open failed", e);
props.closeModal();
appContext.setDialogMessage({
title: t("OPEN_PLAN_SELECTOR_MODAL_FAILED"),
content: t("UNKNOWN_ERROR"),
close: { text: t("CLOSE"), variant: "secondary" },
proceed: {
text: t("REOPEN_PLAN_SELECTOR_MODAL"),
variant: "accent",
action: onReopenClick,
},
});
} finally {
props.setLoading(false);
}
};
main();
}, []);
async function onPlanSelect(plan: Plan) {
if (
!hasPaidSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
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" },
});
}
} 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,
),
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: (
<Trans
i18nKey={"MAIL_TO_MANAGE_SUBSCRIPTION"}
components={{
a: <Link href="mailto:support@ente.io" />,
}}
values={{ emailID: "support@ente.io" }}
/>
),
close: { variant: "secondary" },
});
}
}
const { closeModal, setLoading } = props;
const commonCardData = {
subscription,
bonusData,
closeModal,
planPeriod,
togglePeriod,
setLoading,
};
const plansList = (
<Plans
plansResponse={plansResponse}
planPeriod={planPeriod}
onPlanSelect={onPlanSelect}
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
/>
);
return (
<>
<Stack spacing={3} p={1.5}>
{hasPaidSubscription(subscription) ? (
<PaidSubscriptionPlanSelectorCard
{...commonCardData}
usage={usage}
>
{plansList}
</PaidSubscriptionPlanSelectorCard>
) : (
<FreeSubscriptionPlanSelectorCard {...commonCardData}>
{plansList}
</FreeSubscriptionPlanSelectorCard>
)}
</Stack>
</>
);
}
export default PlanSelectorCard;
function FreeSubscriptionPlanSelectorCard({
children,
subscription,
bonusData,
closeModal,
setLoading,
planPeriod,
togglePeriod,
}) {
return (
<>
<Typography variant="h3" fontWeight={"bold"}>
{t("CHOOSE_PLAN")}
</Typography>
<Box>
<Stack spacing={3}>
<Box>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Typography variant="small" mt={0.5} color="text.muted">
{t("TWO_MONTHS_FREE")}
</Typography>
</Box>
{children}
{hasAddOnBonus(bonusData) && (
<BFAddOnRow
bonusData={bonusData}
closeModal={closeModal}
/>
)}
{hasAddOnBonus(bonusData) && (
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
)}
</Stack>
</Box>
</>
);
}
function PaidSubscriptionPlanSelectorCard({
children,
subscription,
bonusData,
closeModal,
usage,
planPeriod,
togglePeriod,
setLoading,
}) {
return (
<>
<Box pl={1.5} py={0.5}>
<SpaceBetweenFlex>
<Box>
<Typography variant="h3" fontWeight={"bold"}>
{t("SUBSCRIPTION")}
</Typography>
<Typography variant="small" color={"text.muted"}>
{bytesInGB(subscription.storage, 2)}{" "}
{t("storage_unit.gb")}
</Typography>
</Box>
<IconButton onClick={closeModal} color="secondary">
<Close />
</IconButton>
</SpaceBetweenFlex>
</Box>
<Box px={1.5}>
<Typography color={"text.muted"} fontWeight={"bold"}>
<Trans
i18nKey="CURRENT_USAGE"
values={{
usage: `${bytesInGB(usage, 2)} ${t("storage_unit.gb")}`,
}}
/>
</Typography>
</Box>
<Box>
<Stack
spacing={3}
border={(theme) => `1px solid ${theme.palette.divider}`}
p={1.5}
borderRadius={(theme) => `${theme.shape.borderRadius}px`}
>
<Box>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Typography variant="small" mt={0.5} color="text.muted">
{t("TWO_MONTHS_FREE")}
</Typography>
</Box>
{children}
</Stack>
<Box py={1} px={1.5}>
<Typography color={"text.muted"}>
{!isSubscriptionCancelled(subscription)
? t("subscription_status_renewal_active", {
date: subscription.expiryTime,
})
: t("subscription_status_renewal_cancelled", {
date: subscription.expiryTime,
})}
</Typography>
{hasAddOnBonus(bonusData) && (
<BFAddOnRow
bonusData={bonusData}
closeModal={closeModal}
/>
)}
</Box>
</Box>
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
</>
);
}

View File

@@ -1,39 +0,0 @@
import { Dialog, useMediaQuery, useTheme } from "@mui/material";
import { SetLoading } from "types/gallery";
import PlanSelectorCard from "./card";
interface Props {
modalView: boolean;
closeModal: any;
setLoading: SetLoading;
}
function PlanSelector(props: Props) {
const fullScreen = useMediaQuery(useTheme().breakpoints.down("sm"));
if (!props.modalView) {
return <></>;
}
return (
<Dialog
{...{ fullScreen }}
open={props.modalView}
onClose={props.closeModal}
PaperProps={{
sx: (theme) => ({
width: { sm: "391px" },
p: 1,
[theme.breakpoints.down(360)]: { p: 0 },
}),
}}
>
<PlanSelectorCard
closeModal={props.closeModal}
setLoading={props.setLoading}
/>
</Dialog>
);
}
export default PlanSelector;

View File

@@ -1,11 +0,0 @@
import { FluidContainer } from "@ente/shared/components/Container";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { Button, ButtonProps } from "@mui/material";
const ManageSubscriptionButton = ({ children, ...props }: ButtonProps) => (
<Button size="large" endIcon={<ChevronRight />} {...props}>
<FluidContainer>{children}</FluidContainer>
</Button>
);
export default ManageSubscriptionButton;

View File

@@ -1,137 +0,0 @@
import { Stack } from "@mui/material";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import { Trans } from "react-i18next";
import { Subscription } from "types/billing";
import { SetLoading } from "types/gallery";
import { BonusData } from "types/user";
import {
activateSubscription,
cancelSubscription,
hasAddOnBonus,
hasStripeSubscription,
isSubscriptionCancelled,
manageFamilyMethod,
updatePaymentMethod,
} from "utils/billing";
import ManageSubscriptionButton from "./button";
interface Iprops {
subscription: Subscription;
bonusData?: BonusData;
closeModal: () => void;
setLoading: SetLoading;
}
export function ManageSubscription({
subscription,
bonusData,
closeModal,
setLoading,
}: Iprops) {
const appContext = useContext(AppContext);
const openFamilyPortal = () =>
manageFamilyMethod(appContext.setDialogMessage, setLoading);
return (
<Stack spacing={1}>
{hasStripeSubscription(subscription) && (
<StripeSubscriptionOptions
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
)}
<ManageSubscriptionButton
color="secondary"
onClick={openFamilyPortal}
>
{t("MANAGE_FAMILY_PORTAL")}
</ManageSubscriptionButton>
</Stack>
);
}
function StripeSubscriptionOptions({
subscription,
bonusData,
setLoading,
closeModal,
}: Iprops) {
const appContext = useContext(AppContext);
const confirmReactivation = () =>
appContext.setDialogMessage({
title: t("REACTIVATE_SUBSCRIPTION"),
content: t("REACTIVATE_SUBSCRIPTION_MESSAGE", {
date: subscription.expiryTime,
}),
proceed: {
text: t("REACTIVATE_SUBSCRIPTION"),
action: activateSubscription.bind(
null,
appContext.setDialogMessage,
closeModal,
setLoading,
),
variant: "accent",
},
close: {
text: t("cancel"),
},
});
const confirmCancel = () =>
appContext.setDialogMessage({
title: t("CANCEL_SUBSCRIPTION"),
content: hasAddOnBonus(bonusData) ? (
<Trans i18nKey={"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE"} />
) : (
<Trans i18nKey={"CANCEL_SUBSCRIPTION_MESSAGE"} />
),
proceed: {
text: t("CANCEL_SUBSCRIPTION"),
action: cancelSubscription.bind(
null,
appContext.setDialogMessage,
closeModal,
setLoading,
),
variant: "critical",
},
close: {
text: t("NEVERMIND"),
},
});
const openManagementPortal = updatePaymentMethod.bind(
null,
appContext.setDialogMessage,
setLoading,
);
return (
<>
{isSubscriptionCancelled(subscription) ? (
<ManageSubscriptionButton
color="secondary"
onClick={confirmReactivation}
>
{t("REACTIVATE_SUBSCRIPTION")}
</ManageSubscriptionButton>
) : (
<ManageSubscriptionButton
color="secondary"
onClick={confirmCancel}
>
{t("CANCEL_SUBSCRIPTION")}
</ManageSubscriptionButton>
)}
<ManageSubscriptionButton
color="secondary"
onClick={openManagementPortal}
>
{t("MANAGEMENT_PORTAL")}
</ManageSubscriptionButton>
</>
);
}

View File

@@ -1,45 +0,0 @@
import { styled, ToggleButton, ToggleButtonGroup } from "@mui/material";
import { PLAN_PERIOD } from "constants/gallery";
import { t } from "i18next";
const CustomToggleButton = styled(ToggleButton)(({ theme }) => ({
textTransform: "none",
padding: "12px 16px",
borderRadius: "4px",
backgroundColor: theme.colors.fill.faint,
border: `1px solid transparent`,
color: theme.colors.text.faint,
"&.Mui-selected": {
backgroundColor: theme.colors.accent.A500,
color: theme.colors.text.base,
},
"&.Mui-selected:hover": {
backgroundColor: theme.colors.accent.A500,
color: theme.colors.text.base,
},
width: "97.433px",
}));
export function PeriodToggler({ planPeriod, togglePeriod }) {
const handleChange = (_, newPlanPeriod: PLAN_PERIOD) => {
if (newPlanPeriod !== null && newPlanPeriod !== planPeriod) {
togglePeriod();
}
};
return (
<ToggleButtonGroup
value={planPeriod}
exclusive
onChange={handleChange}
color="primary"
>
<CustomToggleButton value={PLAN_PERIOD.MONTH}>
{t("MONTHLY")}
</CustomToggleButton>
<CustomToggleButton value={PLAN_PERIOD.YEAR}>
{t("YEARLY")}
</CustomToggleButton>
</ToggleButtonGroup>
);
}

View File

@@ -1,42 +0,0 @@
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import { Box, styled, Typography } from "@mui/material";
import { formattedStorageByteSize } from "@/new/photos/utils/units";
import { Trans } from "react-i18next";
const RowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({
// gap: theme.spacing(1.5),
padding: theme.spacing(1, 0),
cursor: "pointer",
"&:hover .endIcon": {
backgroundColor: "rgba(255,255,255,0.08)",
},
}));
export function BFAddOnRow({ bonusData, closeModal }) {
return (
<>
{bonusData.storageBonuses.map((bonus) => {
if (bonus.type.startsWith("ADD_ON")) {
return (
<RowContainer 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>
</RowContainer>
);
}
return null;
})}
</>
);
}

View File

@@ -1,64 +0,0 @@
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import ChevronRight from "@mui/icons-material/ChevronRight";
import Done from "@mui/icons-material/Done";
import { Box, Button } from "@mui/material";
import { t } from "i18next";
export function PlanIconButton({
current,
onClick,
}: {
current: boolean;
onClick: () => void;
}) {
return (
<Box mt={6}>
{current ? (
<CurrentPlanTileButton />
) : (
<NormalPlanTileButton onClick={onClick} />
)}
</Box>
);
}
function CurrentPlanTileButton() {
return (
<Button
color="accent"
disabled={true}
sx={(theme) => ({
"&&": {
color: theme.colors.accent.A500,
borderColor: theme.colors.accent.A500,
},
})}
fullWidth
onClick={() => null}
variant={"outlined"}
>
<SpaceBetweenFlex>
{t("active")}
<Done />
</SpaceBetweenFlex>
</Button>
);
}
function NormalPlanTileButton({ onClick }) {
return (
<Button
color="accent"
sx={(theme) => ({
border: `1px solid ${theme.colors.accent.A500}`,
})}
fullWidth
onClick={onClick}
variant={"contained"}
>
<SpaceBetweenFlex>
{t("SUBSCRIBE")}
<ChevronRight />
</SpaceBetweenFlex>
</Button>
);
}

View File

@@ -1,94 +0,0 @@
import { formattedStorageByteSize } from "@/new/photos/utils/units";
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import ArrowForward from "@mui/icons-material/ArrowForward";
import { Box, IconButton, Stack, Typography, styled } from "@mui/material";
import { PLAN_PERIOD } from "constants/gallery";
import { t } from "i18next";
import type { PlansResponse } from "services/billingService";
import { Plan, Subscription } from "types/billing";
import { BonusData } from "types/user";
import {
hasAddOnBonus,
hasPaidSubscription,
isPopularPlan,
isUserSubscribedPlan,
} from "utils/billing";
import { PlanRow } from "./planRow";
interface Iprops {
plansResponse: PlansResponse | undefined;
planPeriod: PLAN_PERIOD;
subscription: Subscription;
bonusData?: BonusData;
onPlanSelect: (plan: Plan) => void;
closeModal: () => void;
}
const Plans = ({
plansResponse,
planPeriod,
subscription,
bonusData,
onPlanSelect,
closeModal,
}: Iprops) => {
const { freePlan, plans } = plansResponse ?? {};
return (
<Stack spacing={2}>
{plans
?.filter((plan) => plan.period === planPeriod)
?.map((plan) => (
<PlanRow
disabled={isUserSubscribedPlan(plan, subscription)}
popular={isPopularPlan(plan)}
key={plan.stripeID}
plan={plan}
subscription={subscription}
onPlanSelect={onPlanSelect}
/>
))}
{!hasPaidSubscription(subscription) &&
!hasAddOnBonus(bonusData) &&
freePlan && (
<FreePlanRow
storage={freePlan.storage}
closeModal={closeModal}
/>
)}
</Stack>
);
};
export default Plans;
interface FreePlanRowProps {
storage: number;
closeModal: () => void;
}
const FreePlanRow: React.FC<FreePlanRowProps> = ({ closeModal, storage }) => {
return (
<FreePlanRow_ onClick={closeModal}>
<Box>
<Typography>{t("free_plan_option")}</Typography>
<Typography variant="small" color="text.muted">
{t("free_plan_description", {
storage: formattedStorageByteSize(storage),
})}
</Typography>
</Box>
<IconButton className={"endIcon"}>
<ArrowForward />
</IconButton>
</FreePlanRow_>
);
};
const FreePlanRow_ = styled(SpaceBetweenFlex)(({ theme }) => ({
gap: theme.spacing(1.5),
padding: theme.spacing(1.5, 1),
cursor: "pointer",
"&:hover .endIcon": {
backgroundColor: "rgba(255,255,255,0.08)",
},
}));

View File

@@ -1,103 +0,0 @@
import { bytesInGB } from "@/new/photos/utils/units";
import { FlexWrapper, FluidContainer } from "@ente/shared/components/Container";
import ArrowForward from "@mui/icons-material/ArrowForward";
import Done from "@mui/icons-material/Done";
import { Box, Button, ButtonProps, Typography, styled } from "@mui/material";
import { Badge } from "components/Badge";
import { PLAN_PERIOD } from "constants/gallery";
import { t } from "i18next";
import { Plan, Subscription } from "types/billing";
import { hasPaidSubscription, isUserSubscribedPlan } from "utils/billing";
interface Iprops {
plan: Plan;
subscription: Subscription;
onPlanSelect: (plan: Plan) => void;
disabled: boolean;
popular: boolean;
}
const PlanRowContainer = styled(FlexWrapper)(() => ({
background:
"linear-gradient(268.22deg, rgba(256, 256, 256, 0.08) -3.72%, rgba(256, 256, 256, 0) 85.73%)",
}));
const TopAlignedFluidContainer = styled(FluidContainer)`
align-items: flex-start;
`;
const DisabledPlanButton = styled((props: ButtonProps) => (
<Button disabled endIcon={<Done />} {...props} />
))(({ theme }) => ({
"&.Mui-disabled": {
backgroundColor: "transparent",
color: theme.colors.text.base,
},
}));
const ActivePlanButton = styled((props: ButtonProps) => (
<Button color="accent" {...props} endIcon={<ArrowForward />} />
))(() => ({
".MuiButton-endIcon": {
transition: "transform .2s ease-in-out",
},
"&:hover .MuiButton-endIcon": {
transform: "translateX(4px)",
},
}));
export function PlanRow({
plan,
subscription,
onPlanSelect,
disabled,
popular,
}: Iprops) {
const handleClick = () => {
!isUserSubscribedPlan(plan, subscription) && onPlanSelect(plan);
};
const PlanButton = disabled ? DisabledPlanButton : ActivePlanButton;
return (
<PlanRowContainer>
<TopAlignedFluidContainer>
<Typography variant="h1" fontWeight={"bold"}>
{bytesInGB(plan.storage)}
</Typography>
<FlexWrapper flexWrap={"wrap"} gap={1}>
<Typography variant="h3" color="text.muted">
{t("storage_unit.gb")}
</Typography>
{popular && !hasPaidSubscription(subscription) && (
<Badge>{t("POPULAR")}</Badge>
)}
</FlexWrapper>
</TopAlignedFluidContainer>
<Box width="136px">
<PlanButton
sx={{
justifyContent: "flex-end",
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
size="large"
onClick={handleClick}
>
<Box textAlign={"right"}>
<Typography fontWeight={"bold"} variant="large">
{plan.price}{" "}
</Typography>{" "}
<Typography color="text.muted" variant="small">
{`/ ${
plan.period === PLAN_PERIOD.MONTH
? t("MONTH_SHORT")
: t("YEAR")
}`}
</Typography>
</Box>
</PlanButton>
</Box>
</PlanRowContainer>
);
}

View File

@@ -1,43 +0,0 @@
import { styled } from "@mui/material";
const PlanTile = styled("div")<{ current: boolean }>(({ theme, current }) => ({
padding: theme.spacing(3),
border: `1px solid ${theme.palette.divider}`,
"&:hover": {
backgroundColor: " rgba(40, 214, 101, 0.11)",
cursor: "pointer",
},
...(current && {
borderColor: theme.colors.accent.A500,
cursor: "not-allowed",
"&:hover": { backgroundColor: "transparent" },
}),
width: " 260px",
"@media (min-width: 1152px)": {
"&:first-of-type": {
borderTopLeftRadius: "8px",
},
"&:last-of-type": {
borderTopRightRadius: "8px",
},
},
"@media (max-width: 1151px) and (min-width:551px)": {
"&:first-of-type": {
borderTopLeftRadius: "8px",
},
"&:nth-of-type(2)": {
borderTopRightRadius: "8px",
},
},
"@media (max-width: 551px)": {
"&:first-of-type": {
borderTopLeftRadius: "8px",
borderTopRightRadius: "8px",
},
},
}));
export default PlanTile;