[web] dynamic free storage (#1644)
Behave like the mobile app and use the server's response to show the storage value for the free plan instead of hardcoding it to 1 GB. Tested with a local museum: <img width="203" alt="Screenshot 2024-05-07 at 13 12 39" src="https://github.com/ente-io/ente/assets/24503581/79a81a3f-614e-4e0a-9839-de39d7e9396a">
This commit is contained in:
commit
425cc9050b
|
@ -0,0 +1,356 @@
|
||||||
|
import log from "@/next/log";
|
||||||
|
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||||
|
import { SUPPORT_EMAIL } from "@ente/shared/constants/urls";
|
||||||
|
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 { bytesInGB } from "utils/units";
|
||||||
|
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_EMAIL}`} />,
|
||||||
|
}}
|
||||||
|
values={{ emailID: SUPPORT_EMAIL }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
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("RENEWAL_ACTIVE_SUBSCRIPTION_STATUS", {
|
||||||
|
date: subscription.expiryTime,
|
||||||
|
})
|
||||||
|
: t("RENEWAL_CANCELLED_SUBSCRIPTION_STATUS", {
|
||||||
|
date: subscription.expiryTime,
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
{hasAddOnBonus(bonusData) && (
|
||||||
|
<BFAddOnRow
|
||||||
|
bonusData={bonusData}
|
||||||
|
closeModal={closeModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ManageSubscription
|
||||||
|
subscription={subscription}
|
||||||
|
bonusData={bonusData}
|
||||||
|
closeModal={closeModal}
|
||||||
|
setLoading={setLoading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
import { Stack } from "@mui/material";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { hasAddOnBonus } from "utils/billing";
|
|
||||||
import { ManageSubscription } from "../manageSubscription";
|
|
||||||
import { PeriodToggler } from "../periodToggler";
|
|
||||||
import Plans from "../plans";
|
|
||||||
import { BFAddOnRow } from "../plans/BfAddOnRow";
|
|
||||||
|
|
||||||
export default function FreeSubscriptionPlanSelectorCard({
|
|
||||||
plans,
|
|
||||||
subscription,
|
|
||||||
bonusData,
|
|
||||||
closeModal,
|
|
||||||
setLoading,
|
|
||||||
planPeriod,
|
|
||||||
togglePeriod,
|
|
||||||
onPlanSelect,
|
|
||||||
}) {
|
|
||||||
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>
|
|
||||||
<Plans
|
|
||||||
plans={plans}
|
|
||||||
planPeriod={planPeriod}
|
|
||||||
onPlanSelect={onPlanSelect}
|
|
||||||
subscription={subscription}
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={closeModal}
|
|
||||||
/>
|
|
||||||
{hasAddOnBonus(bonusData) && (
|
|
||||||
<BFAddOnRow
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={closeModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasAddOnBonus(bonusData) && (
|
|
||||||
<ManageSubscription
|
|
||||||
subscription={subscription}
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={closeModal}
|
|
||||||
setLoading={setLoading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,202 +0,0 @@
|
||||||
import log from "@/next/log";
|
|
||||||
import { SUPPORT_EMAIL } from "@ente/shared/constants/urls";
|
|
||||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
|
||||||
import { LS_KEYS } from "@ente/shared/storage/localStorage";
|
|
||||||
import { Link, Stack } from "@mui/material";
|
|
||||||
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 from "services/billingService";
|
|
||||||
import { Plan } from "types/billing";
|
|
||||||
import { SetLoading } from "types/gallery";
|
|
||||||
import {
|
|
||||||
getLocalUserSubscription,
|
|
||||||
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 FreeSubscriptionPlanSelectorCard from "./free";
|
|
||||||
import PaidSubscriptionPlanSelectorCard from "./paid";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
closeModal: any;
|
|
||||||
setLoading: SetLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PlanSelectorCard(props: Props) {
|
|
||||||
const subscription = useMemo(() => getLocalUserSubscription(), []);
|
|
||||||
const [plans, setPlans] = useLocalState<Plan[]>(LS_KEYS.PLANS);
|
|
||||||
|
|
||||||
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 plans = await billingService.getPlans();
|
|
||||||
if (isSubscriptionActive(subscription)) {
|
|
||||||
const planNotListed =
|
|
||||||
plans.filter((plan) =>
|
|
||||||
isUserSubscribedPlan(plan, subscription),
|
|
||||||
).length === 0;
|
|
||||||
if (
|
|
||||||
subscription &&
|
|
||||||
!isOnFreePlan(subscription) &&
|
|
||||||
planNotListed
|
|
||||||
) {
|
|
||||||
plans.push(planForSubscription(subscription));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setPlans(plans);
|
|
||||||
} 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_EMAIL}`} />,
|
|
||||||
}}
|
|
||||||
values={{ emailID: SUPPORT_EMAIL }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
close: { variant: "secondary" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack spacing={3} p={1.5}>
|
|
||||||
{hasPaidSubscription(subscription) ? (
|
|
||||||
<PaidSubscriptionPlanSelectorCard
|
|
||||||
plans={plans}
|
|
||||||
subscription={subscription}
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={props.closeModal}
|
|
||||||
planPeriod={planPeriod}
|
|
||||||
togglePeriod={togglePeriod}
|
|
||||||
onPlanSelect={onPlanSelect}
|
|
||||||
setLoading={props.setLoading}
|
|
||||||
usage={usage}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FreeSubscriptionPlanSelectorCard
|
|
||||||
plans={plans}
|
|
||||||
subscription={subscription}
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={props.closeModal}
|
|
||||||
setLoading={props.setLoading}
|
|
||||||
planPeriod={planPeriod}
|
|
||||||
togglePeriod={togglePeriod}
|
|
||||||
onPlanSelect={onPlanSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlanSelectorCard;
|
|
|
@ -1,109 +0,0 @@
|
||||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
|
||||||
import Close from "@mui/icons-material/Close";
|
|
||||||
import { IconButton, Stack } from "@mui/material";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
import { hasAddOnBonus, isSubscriptionCancelled } from "utils/billing";
|
|
||||||
import { bytesInGB } from "utils/units";
|
|
||||||
import { ManageSubscription } from "../manageSubscription";
|
|
||||||
import { PeriodToggler } from "../periodToggler";
|
|
||||||
import Plans from "../plans";
|
|
||||||
import { BFAddOnRow } from "../plans/BfAddOnRow";
|
|
||||||
|
|
||||||
export default function PaidSubscriptionPlanSelectorCard({
|
|
||||||
plans,
|
|
||||||
subscription,
|
|
||||||
bonusData,
|
|
||||||
closeModal,
|
|
||||||
usage,
|
|
||||||
planPeriod,
|
|
||||||
togglePeriod,
|
|
||||||
onPlanSelect,
|
|
||||||
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>
|
|
||||||
<Plans
|
|
||||||
plans={plans}
|
|
||||||
planPeriod={planPeriod}
|
|
||||||
onPlanSelect={onPlanSelect}
|
|
||||||
subscription={subscription}
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={closeModal}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Box py={1} px={1.5}>
|
|
||||||
<Typography color={"text.muted"}>
|
|
||||||
{!isSubscriptionCancelled(subscription)
|
|
||||||
? t("RENEWAL_ACTIVE_SUBSCRIPTION_STATUS", {
|
|
||||||
date: subscription.expiryTime,
|
|
||||||
})
|
|
||||||
: t("RENEWAL_CANCELLED_SUBSCRIPTION_STATUS", {
|
|
||||||
date: subscription.expiryTime,
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
{hasAddOnBonus(bonusData) && (
|
|
||||||
<BFAddOnRow
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={closeModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<ManageSubscription
|
|
||||||
subscription={subscription}
|
|
||||||
bonusData={bonusData}
|
|
||||||
closeModal={closeModal}
|
|
||||||
setLoading={setLoading}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
|
||||||
import ArrowForward from "@mui/icons-material/ArrowForward";
|
|
||||||
import { Box, IconButton, styled, Typography } from "@mui/material";
|
|
||||||
import { t } from "i18next";
|
|
||||||
|
|
||||||
const RowContainer = 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)",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
export function FreePlanRow({ closeModal }) {
|
|
||||||
return (
|
|
||||||
<RowContainer onClick={closeModal}>
|
|
||||||
<Box>
|
|
||||||
<Typography> {t("FREE_PLAN_OPTION_LABEL")}</Typography>
|
|
||||||
<Typography variant="small" color="text.muted">
|
|
||||||
{t("FREE_PLAN_DESCRIPTION")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<IconButton className={"endIcon"}>
|
|
||||||
<ArrowForward />
|
|
||||||
</IconButton>
|
|
||||||
</RowContainer>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { Stack } from "@mui/material";
|
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 { PLAN_PERIOD } from "constants/gallery";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import type { PlansResponse } from "services/billingService";
|
||||||
import { Plan, Subscription } from "types/billing";
|
import { Plan, Subscription } from "types/billing";
|
||||||
import { BonusData } from "types/user";
|
import { BonusData } from "types/user";
|
||||||
import {
|
import {
|
||||||
|
@ -8,11 +12,11 @@ import {
|
||||||
isPopularPlan,
|
isPopularPlan,
|
||||||
isUserSubscribedPlan,
|
isUserSubscribedPlan,
|
||||||
} from "utils/billing";
|
} from "utils/billing";
|
||||||
import { FreePlanRow } from "./FreePlanRow";
|
import { formattedStorageByteSize } from "utils/units";
|
||||||
import { PlanRow } from "./planRow";
|
import { PlanRow } from "./planRow";
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
plans: Plan[];
|
plansResponse: PlansResponse | undefined;
|
||||||
planPeriod: PLAN_PERIOD;
|
planPeriod: PLAN_PERIOD;
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
bonusData?: BonusData;
|
bonusData?: BonusData;
|
||||||
|
@ -21,30 +25,70 @@ interface Iprops {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plans = ({
|
const Plans = ({
|
||||||
plans,
|
plansResponse,
|
||||||
planPeriod,
|
planPeriod,
|
||||||
subscription,
|
subscription,
|
||||||
bonusData,
|
bonusData,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
closeModal,
|
closeModal,
|
||||||
}: Iprops) => (
|
}: Iprops) => {
|
||||||
<Stack spacing={2}>
|
const { freePlan, plans } = plansResponse ?? {};
|
||||||
{plans
|
return (
|
||||||
?.filter((plan) => plan.period === planPeriod)
|
<Stack spacing={2}>
|
||||||
?.map((plan) => (
|
{plans
|
||||||
<PlanRow
|
?.filter((plan) => plan.period === planPeriod)
|
||||||
disabled={isUserSubscribedPlan(plan, subscription)}
|
?.map((plan) => (
|
||||||
popular={isPopularPlan(plan)}
|
<PlanRow
|
||||||
key={plan.stripeID}
|
disabled={isUserSubscribedPlan(plan, subscription)}
|
||||||
plan={plan}
|
popular={isPopularPlan(plan)}
|
||||||
subscription={subscription}
|
key={plan.stripeID}
|
||||||
onPlanSelect={onPlanSelect}
|
plan={plan}
|
||||||
/>
|
subscription={subscription}
|
||||||
))}
|
onPlanSelect={onPlanSelect}
|
||||||
{!hasPaidSubscription(subscription) && !hasAddOnBonus(bonusData) && (
|
/>
|
||||||
<FreePlanRow closeModal={closeModal} />
|
))}
|
||||||
)}
|
{!hasPaidSubscription(subscription) &&
|
||||||
</Stack>
|
!hasAddOnBonus(bonusData) &&
|
||||||
);
|
freePlan && (
|
||||||
|
<FreePlanRow
|
||||||
|
storage={freePlan.storage}
|
||||||
|
closeModal={closeModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Plans;
|
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_LABEL")}</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)",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
|
@ -19,8 +19,18 @@ enum PaymentActionType {
|
||||||
Update = "update",
|
Update = "update",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FreePlan {
|
||||||
|
/* Number of bytes available in the free plan */
|
||||||
|
storage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlansResponse {
|
||||||
|
freePlan: FreePlan;
|
||||||
|
plans: Plan[];
|
||||||
|
}
|
||||||
|
|
||||||
class billingService {
|
class billingService {
|
||||||
public async getPlans(): Promise<Plan[]> {
|
public async getPlans(): Promise<PlansResponse> {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
@ -37,8 +47,7 @@ class billingService {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { plans } = response.data;
|
return response.data;
|
||||||
return plans;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("failed to get plans", e);
|
log.error("failed to get plans", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ export interface Subscription {
|
||||||
price: string;
|
price: string;
|
||||||
period: PLAN_PERIOD;
|
period: PLAN_PERIOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Plan {
|
export interface Plan {
|
||||||
id: string;
|
id: string;
|
||||||
androidID: string;
|
androidID: string;
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localized unit keys.
|
||||||
|
*
|
||||||
|
* For each of these, there is expected to be a localized key under
|
||||||
|
* "storage_unit". e.g. "storage_unit.tb".
|
||||||
|
*/
|
||||||
const units = ["b", "kb", "mb", "gb", "tb"];
|
const units = ["b", "kb", "mb", "gb", "tb"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,13 +27,16 @@ export const bytesInGB = (bytes: number, precision = 0): string =>
|
||||||
* Defaults to 2.
|
* Defaults to 2.
|
||||||
*/
|
*/
|
||||||
export function formattedByteSize(bytes: number, precision = 2): string {
|
export function formattedByteSize(bytes: number, precision = 2): string {
|
||||||
if (bytes === 0 || isNaN(bytes)) {
|
if (bytes <= 0) return `0 ${t("storage_unit.mb")}`;
|
||||||
return "0 MB";
|
|
||||||
}
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.min(
|
||||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i];
|
units.length - 1,
|
||||||
|
);
|
||||||
|
const quantity = bytes / Math.pow(1024, i);
|
||||||
|
const unit = units[i];
|
||||||
|
|
||||||
|
return `${quantity.toFixed(precision)} ${t(`storage_unit.${unit}`)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormattedStorageByteSizeOptions {
|
interface FormattedStorageByteSizeOptions {
|
||||||
|
@ -50,7 +59,7 @@ interface FormattedStorageByteSizeOptions {
|
||||||
* displaying the "storage size" (in different contexts) as opposed to, say, a
|
* displaying the "storage size" (in different contexts) as opposed to, say, a
|
||||||
* generic "file size".
|
* generic "file size".
|
||||||
*
|
*
|
||||||
* @param options
|
* @param options {@link FormattedStorageByteSizeOptions}.
|
||||||
*
|
*
|
||||||
* @return A user visible string, including the localized unit suffix.
|
* @return A user visible string, including the localized unit suffix.
|
||||||
*/
|
*/
|
||||||
|
@ -58,21 +67,27 @@ export const formattedStorageByteSize = (
|
||||||
bytes: number,
|
bytes: number,
|
||||||
options?: FormattedStorageByteSizeOptions,
|
options?: FormattedStorageByteSizeOptions,
|
||||||
): string => {
|
): string => {
|
||||||
if (bytes <= 0) {
|
if (bytes <= 0) return `0 ${t("storage_unit.mb")}`;
|
||||||
return `0 ${t("storage_unit.mb")}`;
|
|
||||||
}
|
const i = Math.min(
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||||
|
units.length - 1,
|
||||||
|
);
|
||||||
|
|
||||||
let quantity = bytes / Math.pow(1024, i);
|
let quantity = bytes / Math.pow(1024, i);
|
||||||
let unit = units[i];
|
let unit = units[i];
|
||||||
|
|
||||||
if (quantity > 100 && unit !== "GB") {
|
// Round up bytes, KBs and MBs to the bigger unit whenever they'll come of
|
||||||
|
// as more than 0.1.
|
||||||
|
if (quantity > 100 && i < units.length - 2) {
|
||||||
quantity /= 1024;
|
quantity /= 1024;
|
||||||
unit = units[i + 1];
|
unit = units[i + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
quantity = Number(quantity.toFixed(1));
|
quantity = Number(quantity.toFixed(1));
|
||||||
|
|
||||||
|
// Truncate or round storage sizes to trim off unnecessary and potentially
|
||||||
|
// obscuring precision when they are larger that 10 GB.
|
||||||
if (bytes >= 10 * 1024 * 1024 * 1024 /* 10 GB */) {
|
if (bytes >= 10 * 1024 * 1024 * 1024 /* 10 GB */) {
|
||||||
if (options?.round) {
|
if (options?.round) {
|
||||||
quantity = Math.ceil(quantity);
|
quantity = Math.ceil(quantity);
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements",
|
"TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements",
|
||||||
"POPULAR": "Beliebt",
|
"POPULAR": "Beliebt",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren",
|
"FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB für 1 Jahr",
|
"free_plan_description": "{{storage}} für 1 Jahr",
|
||||||
"CURRENT_USAGE": "Aktuelle Nutzung ist <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "Aktuelle Nutzung ist <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "Dein Browser ist nicht leistungsstark genug, um deine Bilder zu verschlüsseln. Versuche, dich an einem Computer bei Ente anzumelden, oder lade dir die Ente-App für dein Gerät (Handy oder Desktop) herunter.",
|
"WEAK_DEVICE": "Dein Browser ist nicht leistungsstark genug, um deine Bilder zu verschlüsseln. Versuche, dich an einem Computer bei Ente anzumelden, oder lade dir die Ente-App für dein Gerät (Handy oder Desktop) herunter.",
|
||||||
"DRAG_AND_DROP_HINT": "Oder ziehe Dateien per Drag-and-Drop in das Ente-Fenster",
|
"DRAG_AND_DROP_HINT": "Oder ziehe Dateien per Drag-and-Drop in das Ente-Fenster",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Get 2 months free on yearly plans",
|
"TWO_MONTHS_FREE": "Get 2 months free on yearly plans",
|
||||||
"POPULAR": "Popular",
|
"POPULAR": "Popular",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Continue with free trial",
|
"FREE_PLAN_OPTION_LABEL": "Continue with free trial",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB for 1 year",
|
"free_plan_description": "{{storage}} for 1 year",
|
||||||
"CURRENT_USAGE": "Current usage is <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "Current usage is <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to Ente on your computer, or download the Ente mobile/desktop app.",
|
"WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to Ente on your computer, or download the Ente mobile/desktop app.",
|
||||||
"DRAG_AND_DROP_HINT": "Or drag and drop into the Ente window",
|
"DRAG_AND_DROP_HINT": "Or drag and drop into the Ente window",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Obtén 2 meses gratis en planes anuales",
|
"TWO_MONTHS_FREE": "Obtén 2 meses gratis en planes anuales",
|
||||||
"POPULAR": "Popular",
|
"POPULAR": "Popular",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Continuar con el plan gratuito",
|
"FREE_PLAN_OPTION_LABEL": "Continuar con el plan gratuito",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB por 1 año",
|
"free_plan_description": "{{storage}} por 1 año",
|
||||||
"CURRENT_USAGE": "El uso actual es <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "El uso actual es <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "El navegador web que está utilizando no es lo suficientemente poderoso para cifrar sus fotos. Por favor, intente iniciar sesión en ente en su computadora, o descargue la aplicación ente para móvil/escritorio.",
|
"WEAK_DEVICE": "El navegador web que está utilizando no es lo suficientemente poderoso para cifrar sus fotos. Por favor, intente iniciar sesión en ente en su computadora, o descargue la aplicación ente para móvil/escritorio.",
|
||||||
"DRAG_AND_DROP_HINT": "O arrastre y suelte en la ventana ente",
|
"DRAG_AND_DROP_HINT": "O arrastre y suelte en la ventana ente",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Obtenir 2 mois gratuits sur les plans annuels",
|
"TWO_MONTHS_FREE": "Obtenir 2 mois gratuits sur les plans annuels",
|
||||||
"POPULAR": "Populaire",
|
"POPULAR": "Populaire",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Poursuivre avec la version d'essai gratuite",
|
"FREE_PLAN_OPTION_LABEL": "Poursuivre avec la version d'essai gratuite",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 Go pour 1 an",
|
"free_plan_description": "{{storage}} pour 1 an",
|
||||||
"CURRENT_USAGE": "L'utilisation actuelle est de <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "L'utilisation actuelle est de <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "Le navigateur que vous utilisez n'est pas assez puissant pour chiffrer vos photos. Veuillez essayer de vous connecter à Ente sur votre ordinateur, ou télécharger l'appli Ente mobile/ordinateur.",
|
"WEAK_DEVICE": "Le navigateur que vous utilisez n'est pas assez puissant pour chiffrer vos photos. Veuillez essayer de vous connecter à Ente sur votre ordinateur, ou télécharger l'appli Ente mobile/ordinateur.",
|
||||||
"DRAG_AND_DROP_HINT": "Sinon glissez déposez dans la fenêtre Ente",
|
"DRAG_AND_DROP_HINT": "Sinon glissez déposez dans la fenêtre Ente",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Ottieni 2 mesi gratis sui piani annuali",
|
"TWO_MONTHS_FREE": "Ottieni 2 mesi gratis sui piani annuali",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB per 1 anno",
|
"free_plan_description": "{{storage}} per 1 anno",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Krijg 2 maanden gratis op jaarlijkse abonnementen",
|
"TWO_MONTHS_FREE": "Krijg 2 maanden gratis op jaarlijkse abonnementen",
|
||||||
"POPULAR": "Populair",
|
"POPULAR": "Populair",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Doorgaan met gratis account",
|
"FREE_PLAN_OPTION_LABEL": "Doorgaan met gratis account",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB voor 1 jaar",
|
"free_plan_description": "{{storage}} voor 1 jaar",
|
||||||
"CURRENT_USAGE": "Huidig gebruik is <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "Huidig gebruik is <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de Ente mobiel/desktop app.",
|
"WEAK_DEVICE": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de Ente mobiel/desktop app.",
|
||||||
"DRAG_AND_DROP_HINT": "Of sleep en plaats in het Ente venster",
|
"DRAG_AND_DROP_HINT": "Of sleep en plaats in het Ente venster",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Obtenha 2 meses gratuitos em planos anuais",
|
"TWO_MONTHS_FREE": "Obtenha 2 meses gratuitos em planos anuais",
|
||||||
"POPULAR": "Popular",
|
"POPULAR": "Popular",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Continuar com teste gratuito",
|
"FREE_PLAN_OPTION_LABEL": "Continuar com teste gratuito",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB por 1 ano",
|
"free_plan_description": "{{storage}} por 1 ano",
|
||||||
"CURRENT_USAGE": "O uso atual é <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "O uso atual é <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "O navegador da web que você está usando não é poderoso o suficiente para criptografar suas fotos. Por favor, tente entrar para o ente no computador ou baixe o aplicativo móvel.",
|
"WEAK_DEVICE": "O navegador da web que você está usando não é poderoso o suficiente para criptografar suas fotos. Por favor, tente entrar para o ente no computador ou baixe o aplicativo móvel.",
|
||||||
"DRAG_AND_DROP_HINT": "Ou arraste e solte na janela ente",
|
"DRAG_AND_DROP_HINT": "Ou arraste e solte na janela ente",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "Получите 2 месяца бесплатно по годовым планам",
|
"TWO_MONTHS_FREE": "Получите 2 месяца бесплатно по годовым планам",
|
||||||
"POPULAR": "Популярный",
|
"POPULAR": "Популярный",
|
||||||
"FREE_PLAN_OPTION_LABEL": "Продолжайте пользоваться бесплатной пробной версией",
|
"FREE_PLAN_OPTION_LABEL": "Продолжайте пользоваться бесплатной пробной версией",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 ГБ на 1 год",
|
"free_plan_description": "{{storage}} на 1 год",
|
||||||
"CURRENT_USAGE": "Текущее использование составляет <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "Текущее использование составляет <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "Используемый вами веб-браузер недостаточно мощный, чтобы зашифровать ваши фотографии. Пожалуйста, попробуйте войти в Ente на своем компьютере или загрузить мобильное/настольное приложение Ente.",
|
"WEAK_DEVICE": "Используемый вами веб-браузер недостаточно мощный, чтобы зашифровать ваши фотографии. Пожалуйста, попробуйте войти в Ente на своем компьютере или загрузить мобильное/настольное приложение Ente.",
|
||||||
"DRAG_AND_DROP_HINT": "Или перетащите в основное окно",
|
"DRAG_AND_DROP_HINT": "Или перетащите в основное окно",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "",
|
"TWO_MONTHS_FREE": "",
|
||||||
"POPULAR": "",
|
"POPULAR": "",
|
||||||
"FREE_PLAN_OPTION_LABEL": "",
|
"FREE_PLAN_OPTION_LABEL": "",
|
||||||
"FREE_PLAN_DESCRIPTION": "",
|
"free_plan_description": "",
|
||||||
"CURRENT_USAGE": "",
|
"CURRENT_USAGE": "",
|
||||||
"WEAK_DEVICE": "",
|
"WEAK_DEVICE": "",
|
||||||
"DRAG_AND_DROP_HINT": "",
|
"DRAG_AND_DROP_HINT": "",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"TWO_MONTHS_FREE": "在年度计划上免费获得 2 个月",
|
"TWO_MONTHS_FREE": "在年度计划上免费获得 2 个月",
|
||||||
"POPULAR": "流行的",
|
"POPULAR": "流行的",
|
||||||
"FREE_PLAN_OPTION_LABEL": "继续免费试用",
|
"FREE_PLAN_OPTION_LABEL": "继续免费试用",
|
||||||
"FREE_PLAN_DESCRIPTION": "1 GB 1年",
|
"free_plan_description": "{{storage}} 1年",
|
||||||
"CURRENT_USAGE": "当前使用量是 <strong>{{usage}}</strong>",
|
"CURRENT_USAGE": "当前使用量是 <strong>{{usage}}</strong>",
|
||||||
"WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。",
|
"WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。",
|
||||||
"DRAG_AND_DROP_HINT": "或者拖动并拖动到 Ente 窗口",
|
"DRAG_AND_DROP_HINT": "或者拖动并拖动到 Ente 窗口",
|
||||||
|
|
|
@ -7,7 +7,6 @@ export enum LS_KEYS {
|
||||||
ORIGINAL_KEY_ATTRIBUTES = "originalKeyAttributes",
|
ORIGINAL_KEY_ATTRIBUTES = "originalKeyAttributes",
|
||||||
SUBSCRIPTION = "subscription",
|
SUBSCRIPTION = "subscription",
|
||||||
FAMILY_DATA = "familyData",
|
FAMILY_DATA = "familyData",
|
||||||
PLANS = "plans",
|
|
||||||
IS_FIRST_LOGIN = "isFirstLogin",
|
IS_FIRST_LOGIN = "isFirstLogin",
|
||||||
JUST_SIGNED_UP = "justSignedUp",
|
JUST_SIGNED_UP = "justSignedUp",
|
||||||
SHOW_BACK_BUTTON = "showBackButton",
|
SHOW_BACK_BUTTON = "showBackButton",
|
||||||
|
|
Loading…
Reference in a new issue