diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card.tsx new file mode 100644 index 000000000..6fe86769e --- /dev/null +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/card.tsx @@ -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( + 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: ( + , + }} + values={{ emailID: SUPPORT_EMAIL }} + /> + ), + close: { variant: "secondary" }, + }); + } + } + + const { closeModal, setLoading } = props; + + const commonCardData = { + subscription, + bonusData, + closeModal, + planPeriod, + togglePeriod, + setLoading, + }; + + const plansList = ( + + ); + + return ( + <> + + {hasPaidSubscription(subscription) ? ( + + {plansList} + + ) : ( + + {plansList} + + )} + + + ); +} + +export default PlanSelectorCard; + +function FreeSubscriptionPlanSelectorCard({ + children, + subscription, + bonusData, + closeModal, + setLoading, + planPeriod, + togglePeriod, +}) { + return ( + <> + + {t("CHOOSE_PLAN")} + + + + + + + + {t("TWO_MONTHS_FREE")} + + + {children} + {hasAddOnBonus(bonusData) && ( + + )} + {hasAddOnBonus(bonusData) && ( + + )} + + + + ); +} + +function PaidSubscriptionPlanSelectorCard({ + children, + subscription, + bonusData, + closeModal, + usage, + planPeriod, + togglePeriod, + setLoading, +}) { + return ( + <> + + + + + {t("SUBSCRIPTION")} + + + {bytesInGB(subscription.storage, 2)}{" "} + {t("storage_unit.gb")} + + + + + + + + + + + + + + + + `1px solid ${theme.palette.divider}`} + p={1.5} + borderRadius={(theme) => `${theme.shape.borderRadius}px`} + > + + + + {t("TWO_MONTHS_FREE")} + + + {children} + + + + + {!isSubscriptionCancelled(subscription) + ? t("RENEWAL_ACTIVE_SUBSCRIPTION_STATUS", { + date: subscription.expiryTime, + }) + : t("RENEWAL_CANCELLED_SUBSCRIPTION_STATUS", { + date: subscription.expiryTime, + })} + + {hasAddOnBonus(bonusData) && ( + + )} + + + + + + ); +} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/free.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/free.tsx deleted file mode 100644 index a2ac1090b..000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/free.tsx +++ /dev/null @@ -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 ( - <> - - {t("CHOOSE_PLAN")} - - - - - - - - {t("TWO_MONTHS_FREE")} - - - - {hasAddOnBonus(bonusData) && ( - - )} - {hasAddOnBonus(bonusData) && ( - - )} - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/index.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/index.tsx deleted file mode 100644 index 2ef3c361f..000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/index.tsx +++ /dev/null @@ -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(LS_KEYS.PLANS); - - const [planPeriod, setPlanPeriod] = useState( - 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: ( - , - }} - values={{ emailID: SUPPORT_EMAIL }} - /> - ), - close: { variant: "secondary" }, - }); - } - } - - return ( - <> - - {hasPaidSubscription(subscription) ? ( - - ) : ( - - )} - - - ); -} - -export default PlanSelectorCard; diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx deleted file mode 100644 index ba318330e..000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx +++ /dev/null @@ -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 ( - <> - - - - - {t("SUBSCRIPTION")} - - - {bytesInGB(subscription.storage, 2)}{" "} - {t("storage_unit.gb")} - - - - - - - - - - - - - - - - `1px solid ${theme.palette.divider}`} - p={1.5} - borderRadius={(theme) => `${theme.shape.borderRadius}px`} - > - - - - {t("TWO_MONTHS_FREE")} - - - - - - - - {!isSubscriptionCancelled(subscription) - ? t("RENEWAL_ACTIVE_SUBSCRIPTION_STATUS", { - date: subscription.expiryTime, - }) - : t("RENEWAL_CANCELLED_SUBSCRIPTION_STATUS", { - date: subscription.expiryTime, - })} - - {hasAddOnBonus(bonusData) && ( - - )} - - - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/FreePlanRow.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/FreePlanRow.tsx deleted file mode 100644 index f3651e12d..000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/FreePlanRow.tsx +++ /dev/null @@ -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 ( - - - {t("FREE_PLAN_OPTION_LABEL")} - - {t("FREE_PLAN_DESCRIPTION")} - - - - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx index ed1a666ed..31e97c68e 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx @@ -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 { t } from "i18next"; +import type { PlansResponse } from "services/billingService"; import { Plan, Subscription } from "types/billing"; import { BonusData } from "types/user"; import { @@ -8,11 +12,11 @@ import { isPopularPlan, isUserSubscribedPlan, } from "utils/billing"; -import { FreePlanRow } from "./FreePlanRow"; +import { formattedStorageByteSize } from "utils/units"; import { PlanRow } from "./planRow"; interface Iprops { - plans: Plan[]; + plansResponse: PlansResponse | undefined; planPeriod: PLAN_PERIOD; subscription: Subscription; bonusData?: BonusData; @@ -21,30 +25,70 @@ interface Iprops { } const Plans = ({ - plans, + plansResponse, planPeriod, subscription, bonusData, onPlanSelect, closeModal, -}: Iprops) => ( - - {plans - ?.filter((plan) => plan.period === planPeriod) - ?.map((plan) => ( - - ))} - {!hasPaidSubscription(subscription) && !hasAddOnBonus(bonusData) && ( - - )} - -); +}: Iprops) => { + const { freePlan, plans } = plansResponse ?? {}; + return ( + + {plans + ?.filter((plan) => plan.period === planPeriod) + ?.map((plan) => ( + + ))} + {!hasPaidSubscription(subscription) && + !hasAddOnBonus(bonusData) && + freePlan && ( + + )} + + ); +}; export default Plans; + +interface FreePlanRowProps { + storage: number; + closeModal: () => void; +} + +const FreePlanRow: React.FC = ({ closeModal, storage }) => { + return ( + + + {t("FREE_PLAN_OPTION_LABEL")} + + {t("free_plan_description", { + storage: formattedStorageByteSize(storage), + })} + + + + + + + ); +}; + +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)", + }, +})); diff --git a/web/apps/photos/src/services/billingService.ts b/web/apps/photos/src/services/billingService.ts index 090eca17c..d68938eb5 100644 --- a/web/apps/photos/src/services/billingService.ts +++ b/web/apps/photos/src/services/billingService.ts @@ -19,8 +19,18 @@ enum PaymentActionType { Update = "update", } +export interface FreePlan { + /* Number of bytes available in the free plan */ + storage: number; +} + +export interface PlansResponse { + freePlan: FreePlan; + plans: Plan[]; +} + class billingService { - public async getPlans(): Promise { + public async getPlans(): Promise { const token = getToken(); try { let response; @@ -37,8 +47,7 @@ class billingService { }, ); } - const { plans } = response.data; - return plans; + return response.data; } catch (e) { log.error("failed to get plans", e); } diff --git a/web/apps/photos/src/types/billing/index.ts b/web/apps/photos/src/types/billing/index.ts index b2058948b..ef203d49f 100644 --- a/web/apps/photos/src/types/billing/index.ts +++ b/web/apps/photos/src/types/billing/index.ts @@ -14,6 +14,7 @@ export interface Subscription { price: string; period: PLAN_PERIOD; } + export interface Plan { id: string; androidID: string; diff --git a/web/apps/photos/src/utils/units.ts b/web/apps/photos/src/utils/units.ts index 1eb1ffb81..229ec2ab9 100644 --- a/web/apps/photos/src/utils/units.ts +++ b/web/apps/photos/src/utils/units.ts @@ -1,5 +1,11 @@ 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"]; /** @@ -21,13 +27,16 @@ export const bytesInGB = (bytes: number, precision = 0): string => * Defaults to 2. */ export function formattedByteSize(bytes: number, precision = 2): string { - if (bytes === 0 || isNaN(bytes)) { - return "0 MB"; - } + if (bytes <= 0) return `0 ${t("storage_unit.mb")}`; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; + const i = Math.min( + Math.floor(Math.log(bytes) / Math.log(1024)), + units.length - 1, + ); + const quantity = bytes / Math.pow(1024, i); + const unit = units[i]; + + return `${quantity.toFixed(precision)} ${t(`storage_unit.${unit}`)}`; } interface FormattedStorageByteSizeOptions { @@ -50,7 +59,7 @@ interface FormattedStorageByteSizeOptions { * displaying the "storage size" (in different contexts) as opposed to, say, a * generic "file size". * - * @param options + * @param options {@link FormattedStorageByteSizeOptions}. * * @return A user visible string, including the localized unit suffix. */ @@ -58,21 +67,27 @@ export const formattedStorageByteSize = ( bytes: number, options?: FormattedStorageByteSizeOptions, ): string => { - if (bytes <= 0) { - return `0 ${t("storage_unit.mb")}`; - } - const i = Math.floor(Math.log(bytes) / Math.log(1024)); + if (bytes <= 0) return `0 ${t("storage_unit.mb")}`; + + const i = Math.min( + Math.floor(Math.log(bytes) / Math.log(1024)), + units.length - 1, + ); let quantity = bytes / Math.pow(1024, 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; unit = units[i + 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 (options?.round) { quantity = Math.ceil(quantity); diff --git a/web/packages/next/locales/bg-BG/translation.json b/web/packages/next/locales/bg-BG/translation.json index 52ce2c47b..aa88d9c50 100644 --- a/web/packages/next/locales/bg-BG/translation.json +++ b/web/packages/next/locales/bg-BG/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/de-DE/translation.json b/web/packages/next/locales/de-DE/translation.json index c9e479ecb..8bfddc1cd 100644 --- a/web/packages/next/locales/de-DE/translation.json +++ b/web/packages/next/locales/de-DE/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements", "POPULAR": "Beliebt", "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 {{usage}}", "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", diff --git a/web/packages/next/locales/en-US/translation.json b/web/packages/next/locales/en-US/translation.json index 9a363f3df..f4d6f6100 100644 --- a/web/packages/next/locales/en-US/translation.json +++ b/web/packages/next/locales/en-US/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", "POPULAR": "Popular", "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 {{usage}}", "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", diff --git a/web/packages/next/locales/es-ES/translation.json b/web/packages/next/locales/es-ES/translation.json index 69b783207..ffc06ffa3 100644 --- a/web/packages/next/locales/es-ES/translation.json +++ b/web/packages/next/locales/es-ES/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Obtén 2 meses gratis en planes anuales", "POPULAR": "Popular", "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 {{usage}}", "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", diff --git a/web/packages/next/locales/fa-IR/translation.json b/web/packages/next/locales/fa-IR/translation.json index 34977aa3d..2f9605019 100644 --- a/web/packages/next/locales/fa-IR/translation.json +++ b/web/packages/next/locales/fa-IR/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/fi-FI/translation.json b/web/packages/next/locales/fi-FI/translation.json index b94891efd..33306389c 100644 --- a/web/packages/next/locales/fi-FI/translation.json +++ b/web/packages/next/locales/fi-FI/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/fr-FR/translation.json b/web/packages/next/locales/fr-FR/translation.json index 796b40a44..dd17e54ab 100644 --- a/web/packages/next/locales/fr-FR/translation.json +++ b/web/packages/next/locales/fr-FR/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Obtenir 2 mois gratuits sur les plans annuels", "POPULAR": "Populaire", "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 {{usage}}", "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", diff --git a/web/packages/next/locales/it-IT/translation.json b/web/packages/next/locales/it-IT/translation.json index 0d4298c29..8c767c054 100644 --- a/web/packages/next/locales/it-IT/translation.json +++ b/web/packages/next/locales/it-IT/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Ottieni 2 mesi gratis sui piani annuali", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "1 GB per 1 anno", + "free_plan_description": "{{storage}} per 1 anno", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/ko-KR/translation.json b/web/packages/next/locales/ko-KR/translation.json index b9709ee92..17fc40588 100644 --- a/web/packages/next/locales/ko-KR/translation.json +++ b/web/packages/next/locales/ko-KR/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/nl-NL/translation.json b/web/packages/next/locales/nl-NL/translation.json index a364061a3..23850582d 100644 --- a/web/packages/next/locales/nl-NL/translation.json +++ b/web/packages/next/locales/nl-NL/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Krijg 2 maanden gratis op jaarlijkse abonnementen", "POPULAR": "Populair", "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 {{usage}}", "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", diff --git a/web/packages/next/locales/pt-BR/translation.json b/web/packages/next/locales/pt-BR/translation.json index 3edd83f6b..6d36812ce 100644 --- a/web/packages/next/locales/pt-BR/translation.json +++ b/web/packages/next/locales/pt-BR/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Obtenha 2 meses gratuitos em planos anuais", "POPULAR": "Popular", "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 é {{usage}}", "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", diff --git a/web/packages/next/locales/pt-PT/translation.json b/web/packages/next/locales/pt-PT/translation.json index d6751f32c..c89049ec2 100644 --- a/web/packages/next/locales/pt-PT/translation.json +++ b/web/packages/next/locales/pt-PT/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/ru-RU/translation.json b/web/packages/next/locales/ru-RU/translation.json index 68816b47b..d8c90af17 100644 --- a/web/packages/next/locales/ru-RU/translation.json +++ b/web/packages/next/locales/ru-RU/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "Получите 2 месяца бесплатно по годовым планам", "POPULAR": "Популярный", "FREE_PLAN_OPTION_LABEL": "Продолжайте пользоваться бесплатной пробной версией", - "FREE_PLAN_DESCRIPTION": "1 ГБ на 1 год", + "free_plan_description": "{{storage}} на 1 год", "CURRENT_USAGE": "Текущее использование составляет {{usage}}", "WEAK_DEVICE": "Используемый вами веб-браузер недостаточно мощный, чтобы зашифровать ваши фотографии. Пожалуйста, попробуйте войти в Ente на своем компьютере или загрузить мобильное/настольное приложение Ente.", "DRAG_AND_DROP_HINT": "Или перетащите в основное окно", diff --git a/web/packages/next/locales/sv-SE/translation.json b/web/packages/next/locales/sv-SE/translation.json index 775bb5a60..1ceb6370c 100644 --- a/web/packages/next/locales/sv-SE/translation.json +++ b/web/packages/next/locales/sv-SE/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/th-TH/translation.json b/web/packages/next/locales/th-TH/translation.json index b94891efd..33306389c 100644 --- a/web/packages/next/locales/th-TH/translation.json +++ b/web/packages/next/locales/th-TH/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/tr-TR/translation.json b/web/packages/next/locales/tr-TR/translation.json index b94891efd..33306389c 100644 --- a/web/packages/next/locales/tr-TR/translation.json +++ b/web/packages/next/locales/tr-TR/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", diff --git a/web/packages/next/locales/zh-CN/translation.json b/web/packages/next/locales/zh-CN/translation.json index 9e10cdce8..2489bdd43 100644 --- a/web/packages/next/locales/zh-CN/translation.json +++ b/web/packages/next/locales/zh-CN/translation.json @@ -449,7 +449,7 @@ "TWO_MONTHS_FREE": "在年度计划上免费获得 2 个月", "POPULAR": "流行的", "FREE_PLAN_OPTION_LABEL": "继续免费试用", - "FREE_PLAN_DESCRIPTION": "1 GB 1年", + "free_plan_description": "{{storage}} 1年", "CURRENT_USAGE": "当前使用量是 {{usage}}", "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。", "DRAG_AND_DROP_HINT": "或者拖动并拖动到 Ente 窗口", diff --git a/web/packages/shared/storage/localStorage/index.ts b/web/packages/shared/storage/localStorage/index.ts index 70b9687cd..c6ec3f57f 100644 --- a/web/packages/shared/storage/localStorage/index.ts +++ b/web/packages/shared/storage/localStorage/index.ts @@ -7,7 +7,6 @@ export enum LS_KEYS { ORIGINAL_KEY_ATTRIBUTES = "originalKeyAttributes", SUBSCRIPTION = "subscription", FAMILY_DATA = "familyData", - PLANS = "plans", IS_FIRST_LOGIN = "isFirstLogin", JUST_SIGNED_UP = "justSignedUp", SHOW_BACK_BUTTON = "showBackButton",