update plan selector card with active plan

This commit is contained in:
Abhinav 2022-07-03 14:04:11 +05:30
parent 94a408d815
commit ee03cce577
10 changed files with 274 additions and 230 deletions

View file

@ -0,0 +1,202 @@
import { PLAN_PERIOD } from './../../../../constants/gallery/index';
import { PeriodToggler } from './periodToggler';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import constants from 'utils/strings/constants';
import { Plan } from 'types/billing';
import {
isUserSubscribedPlan,
isSubscriptionCancelled,
updateSubscription,
hasStripeSubscription,
isOnFreePlan,
planForSubscription,
hasMobileSubscription,
hasPaypalSubscription,
getLocalUserSubscription,
hasPaidSubscription,
convertBytesToGBs,
getTotalFamilyUsage,
makeHumanReadableStorage,
} from 'utils/billing';
import { reverseString } from 'utils/common';
import { GalleryContext } from 'pages/gallery';
import billingService from 'services/billingService';
import { SetLoading } from 'types/gallery';
import { logError } from 'utils/sentry';
import { AppContext } from 'pages/_app';
import Plans from './plans';
import { Box, Stack, Typography } from '@mui/material';
import { useLocalState } from 'hooks/useLocalState';
import { LS_KEYS } from 'utils/storage/localStorage';
import { getLocalUserDetails } from 'utils/user';
import { ManageSubscription } from './manageSubscription';
interface Props {
closeModal: any;
setLoading: SetLoading;
}
function PlanSelectorCard(props: Props) {
const subscription = useMemo(() => getLocalUserSubscription(), []);
const totalFamilyUsage = useMemo(
() => getTotalFamilyUsage(getLocalUserDetails().familyData),
[]
);
const [plans, setPlans] = useLocalState<Plan[]>(LS_KEYS.PLANS);
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(PLAN_PERIOD.YEAR);
const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
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);
let plans = await billingService.getPlans();
const planNotListed =
plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription)
).length === 0;
if (
subscription &&
!isOnFreePlan(subscription) &&
planNotListed
) {
plans = [planForSubscription(subscription), ...plans];
}
setPlans(plans);
} catch (e) {
logError(e, 'plan selector modal open failed');
props.closeModal();
appContext.setDialogMessage({
title: constants.OPEN_PLAN_SELECTOR_MODAL_FAILED,
content: constants.UNKNOWN_ERROR,
close: { text: 'close', variant: 'danger' },
proceed: {
text: constants.REOPEN_PLAN_SELECTOR_MODAL,
variant: 'accent',
action: onReopenClick,
},
});
} finally {
props.setLoading(false);
}
};
main();
}, []);
async function onPlanSelect(plan: Plan) {
if (
hasMobileSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
appContext.setDialogMessage({
title: constants.ERROR,
content: constants.CANCEL_SUBSCRIPTION_ON_MOBILE,
close: { variant: 'danger' },
});
} else if (
hasPaypalSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
appContext.setDialogMessage({
title: constants.MANAGE_PLAN,
content: constants.PAYPAL_MANAGE_NOT_SUPPORTED_MESSAGE(),
close: { variant: 'danger' },
});
} else if (hasStripeSubscription(subscription)) {
appContext.setDialogMessage({
title: `${constants.CONFIRM} ${reverseString(
constants.UPDATE_SUBSCRIPTION
)}`,
content: constants.UPDATE_SUBSCRIPTION_MESSAGE,
proceed: {
text: constants.UPDATE_SUBSCRIPTION,
action: updateSubscription.bind(
null,
plan,
appContext.setDialogMessage,
props.setLoading,
props.closeModal
),
variant: 'accent',
},
close: { text: constants.CANCEL },
});
} else {
try {
props.setLoading(true);
await billingService.buySubscription(plan.stripeID);
} catch (e) {
props.setLoading(false);
appContext.setDialogMessage({
title: constants.ERROR,
content: constants.SUBSCRIPTION_PURCHASE_FAILED,
close: { variant: 'danger' },
});
}
}
}
return (
<>
<Stack spacing={3} p={1.5}>
{hasPaidSubscription(subscription) ? (
<Box>
<Typography variant="h3" fontWeight={'bold'}>
{constants.SUBSCRIPTION}
</Typography>
<Typography variant="body2" color={'text.secondary'}>
{convertBytesToGBs(subscription.storage)}{' '}
{constants.GB}
</Typography>
</Box>
) : (
<Typography variant="h3" fontWeight={'bold'}>
{constants.CHOOSE_PLAN}
</Typography>
)}
{totalFamilyUsage > 0 && (
<Typography color={'text.secondary'} fontWeight={'bold'}>
{constants.CURRENT_USAGE(
makeHumanReadableStorage(totalFamilyUsage)
)}
</Typography>
)}
<Box>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Typography mt={0.5} color="text.secondary">
{constants.TWO_MONTHS_FREE}
</Typography>
</Box>
<Plans
plans={plans}
planPeriod={planPeriod}
onPlanSelect={onPlanSelect}
subscription={subscription}
/>
<ManageSubscription
subscription={subscription}
closeModal={props.closeModal}
setLoading={props.setLoading}
/>
</Stack>
</>
);
}
export default PlanSelectorCard;

View file

@ -1,150 +1,19 @@
import { PeriodToggler } from './periodToggler';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import constants from 'utils/strings/constants';
import { Plan } from 'types/billing';
import {
isUserSubscribedPlan,
isSubscriptionCancelled,
updateSubscription,
hasStripeSubscription,
isOnFreePlan,
planForSubscription,
hasMobileSubscription,
hasPaypalSubscription,
getLocalUserSubscription,
} from 'utils/billing';
import { reverseString } from 'utils/common';
import { GalleryContext } from 'pages/gallery';
import billingService from 'services/billingService';
import React, { useContext } from 'react';
import { SetLoading } from 'types/gallery';
import { logError } from 'utils/sentry';
import { AppContext } from 'pages/_app';
import Plans from './plans';
import { Box, Dialog, Stack, Typography } from '@mui/material';
import { useLocalState } from 'hooks/useLocalState';
import { LS_KEYS } from 'utils/storage/localStorage';
import { Box, Dialog } from '@mui/material';
import PlanSelectorCard from './card';
interface Props {
modalView: boolean;
closeModal: any;
setLoading: SetLoading;
}
export enum PLAN_PERIOD {
MONTH = 'month',
YEAR = 'year',
}
function PlanSelector(props: Props) {
const subscription = useMemo(() => getLocalUserSubscription(), []);
const [plans, setPlans] = useLocalState<Plan[]>(LS_KEYS.PLANS);
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(PLAN_PERIOD.YEAR);
const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
const togglePeriod = () => {
setPlanPeriod((prevPeriod) =>
prevPeriod === PLAN_PERIOD.MONTH
? PLAN_PERIOD.YEAR
: PLAN_PERIOD.MONTH
);
};
function onReopenClick() {
appContext.closeMessageDialog();
galleryContext.showPlanSelectorModal();
}
useEffect(() => {
if (!props.modalView) {
return;
}
const main = async () => {
try {
props.setLoading(true);
let plans = await billingService.getPlans();
const planNotListed =
plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription)
).length === 0;
if (
subscription &&
!isOnFreePlan(subscription) &&
planNotListed
) {
plans = [planForSubscription(subscription), ...plans];
}
setPlans(plans);
} catch (e) {
logError(e, 'plan selector modal open failed');
props.closeModal();
appContext.setDialogMessage({
title: constants.OPEN_PLAN_SELECTOR_MODAL_FAILED,
content: constants.UNKNOWN_ERROR,
close: { text: 'close', variant: 'danger' },
proceed: {
text: constants.REOPEN_PLAN_SELECTOR_MODAL,
variant: 'accent',
action: onReopenClick,
},
});
} finally {
props.setLoading(false);
}
};
main();
}, [props.modalView]);
async function onPlanSelect(plan: Plan) {
if (
hasMobileSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
appContext.setDialogMessage({
title: constants.ERROR,
content: constants.CANCEL_SUBSCRIPTION_ON_MOBILE,
close: { variant: 'danger' },
});
} else if (
hasPaypalSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
appContext.setDialogMessage({
title: constants.MANAGE_PLAN,
content: constants.PAYPAL_MANAGE_NOT_SUPPORTED_MESSAGE(),
close: { variant: 'danger' },
});
} else if (hasStripeSubscription(subscription)) {
appContext.setDialogMessage({
title: `${constants.CONFIRM} ${reverseString(
constants.UPDATE_SUBSCRIPTION
)}`,
content: constants.UPDATE_SUBSCRIPTION_MESSAGE,
proceed: {
text: constants.UPDATE_SUBSCRIPTION,
action: updateSubscription.bind(
null,
plan,
appContext.setDialogMessage,
props.setLoading,
props.closeModal
),
variant: 'accent',
},
close: { text: constants.CANCEL },
});
} else {
try {
props.setLoading(true);
await billingService.buySubscription(plan.stripeID);
} catch (e) {
props.setLoading(false);
appContext.setDialogMessage({
title: constants.ERROR,
content: constants.SUBSCRIPTION_PURCHASE_FAILED,
close: { variant: 'danger' },
});
}
}
if (!props.modalView) {
return <></>;
}
return (
@ -154,35 +23,10 @@ function PlanSelector(props: Props) {
onClose={props.closeModal}
PaperProps={{ sx: { width: '400px' } }}>
<Box p={1}>
<Stack spacing={3} p={1.5}>
<Typography variant="h3" fontWeight={'bold'}>
{
/* hasPaidSubscription(subscription)
? constants.MANAGE_PLAN
: */ constants.CHOOSE_PLAN
}
</Typography>
<Box>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Typography mt={0.5} color="text.secondary">
{constants.TWO_MONTHS_FREE}
</Typography>
</Box>
<Plans
plans={plans}
planPeriod={planPeriod}
onPlanSelect={onPlanSelect}
subscription={subscription}
/>
{/* <ManageSubscription
subscription={subscription}
<PlanSelectorCard
closeModal={props.closeModal}
setLoading={props.setLoading}
/> */}
</Stack>
/>
</Box>
</Dialog>
);

View file

@ -1,7 +1,7 @@
import { styled, ToggleButton, ToggleButtonGroup } from '@mui/material';
import { PLAN_PERIOD } from 'constants/gallery';
import React from 'react';
import constants from 'utils/strings/constants';
import { PLAN_PERIOD } from '.';
export function PeriodToggler({ planPeriod, togglePeriod }) {
const CustomToggleButton = styled(ToggleButton)(({ theme }) => ({
textTransform: 'none',

View file

@ -2,6 +2,7 @@ import ArrowForward from '@mui/icons-material/ArrowForward';
import { Box, Typography } from '@mui/material';
import { FlexWrapper, SpaceBetweenFlex } from 'components/Container';
import React from 'react';
import { isUserSubscribedPlan } from 'utils/billing';
import constants from 'utils/strings/constants';
import { PlanRow } from './planRow';
@ -11,6 +12,7 @@ const Plans = ({ plans, planPeriod, subscription, onPlanSelect }) => (
?.filter((plan) => plan.period === planPeriod)
?.map((plan) => (
<PlanRow
disabled={isUserSubscribedPlan(plan, subscription)}
key={plan.stripeID}
plan={plan}
subscription={subscription}

View file

@ -1,40 +0,0 @@
import { PlanIconButton } from './button';
import { Typography } from '@mui/material';
import React from 'react';
import { isUserSubscribedPlan, convertBytesToGBs } from 'utils/billing';
import constants from 'utils/strings/constants';
import { PLAN_PERIOD } from '..';
import PlanTile from './planTile';
export function PlanCard({ plan, subscription, onPlanSelect }) {
const handleClick = () => {
!isUserSubscribedPlan(plan, subscription) && onPlanSelect(plan);
};
return (
<PlanTile
key={plan.stripeID}
current={isUserSubscribedPlan(plan, subscription)}
onClick={handleClick}>
<Typography variant="title" fontWeight={'bold'}>
{convertBytesToGBs(plan.storage, 0)}
</Typography>
<Typography
color="text.secondary"
variant="title"
fontWeight={'normal'}>
{`${plan.price} / ${
plan.period === PLAN_PERIOD.MONTH
? constants.MONTH_SHORT
: constants.YEAR_SHORT
}`}
</Typography>
<PlanIconButton
current={isUserSubscribedPlan(plan, subscription)}
onClick={handleClick}
/>
</PlanTile>
);
}

View file

@ -1,51 +1,75 @@
import { Box, Button, Typography } from '@mui/material';
import { Box, Button, ButtonProps, styled, Typography } from '@mui/material';
import React from 'react';
import { isUserSubscribedPlan, convertBytesToGBs } from 'utils/billing';
import constants from 'utils/strings/constants';
import { PLAN_PERIOD } from '..';
import { FlexWrapper, FluidContainer } from 'components/Container';
import ArrowForward from '@mui/icons-material/ArrowForward';
import { PLAN_PERIOD } from 'constants/gallery';
import Done from '@mui/icons-material/Done';
import { Plan, Subscription } from 'types/billing';
export function PlanRow({ plan, subscription, onPlanSelect }) {
interface Iprops {
plan: Plan;
subscription: Subscription;
onPlanSelect: (plan: Plan) => void;
disabled: boolean;
}
const DisabledPlanButton = styled((props: ButtonProps) => (
<Button disabled endIcon={<Done />} {...props} />
))(({ theme }) => ({
'&&': {
cursor: 'default',
backgroundColor: 'transparent',
color: theme.palette.text.primary,
},
}));
const ActivePlanButton = (props: ButtonProps) => (
<Button color="accent" {...props} endIcon={<ArrowForward />} />
);
export function PlanRow({
plan,
subscription,
onPlanSelect,
disabled,
}: Iprops) {
const handleClick = () => {
!isUserSubscribedPlan(plan, subscription) && onPlanSelect(plan);
};
const PlanButton = disabled ? DisabledPlanButton : ActivePlanButton;
return (
<FlexWrapper
sx={{
background:
'linear-gradient(268.22deg, rgba(256, 256, 256, 0.08) -3.72%, rgba(256, 256, 256, 0) 85.73%)',
}}
// current={isUserSubscribedPlan(plan, subscription)}
onClick={handleClick}>
<FluidContainer sx={{ '&&': { alignItems: 'flex-start' } }}>
<Typography variant="h1" fontWeight={'bold'}>
{convertBytesToGBs(plan.storage, 0)}
{convertBytesToGBs(plan.storage)}
</Typography>
<Typography variant="h3" color="text.secondary">
{constants.GB}
</Typography>
</FluidContainer>
{/* <PlanIconButton
current={isUserSubscribedPlan(plan, subscription)} */}
<Button
onClick={handleClick}
sx={{ width: '136px' }}
color="accent"
endIcon={<ArrowForward />}>
<Box>
<Typography fontWeight={'bold'} variant="h4">
{plan.price}{' '}
</Typography>{' '}
<Typography color="text.secondary" variant="body2">
/ $
{plan.period === PLAN_PERIOD.MONTH
? constants.MONTH_SHORT
: constants.YEAR_SHORT}
</Typography>
</Box>
</Button>
<Box width="136px">
<PlanButton size="large" onClick={handleClick}>
<Box>
<Typography fontWeight={'bold'} variant="h4">
{plan.price}{' '}
</Typography>{' '}
<Typography color="text.secondary" variant="body2">
/ $
{plan.period === PLAN_PERIOD.MONTH
? constants.MONTH_SHORT
: constants.YEAR_SHORT}
</Typography>
</Box>
</PlanButton>
</Box>
</FlexWrapper>
);
}

View file

@ -6,3 +6,8 @@ export const IMAGE_CONTAINER_MAX_WIDTH =
IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;
export enum PLAN_PERIOD {
MONTH = 'month',
YEAR = 'year',
}

View file

@ -52,7 +52,6 @@ export function convertBytesToHumanReadable(
export function makeHumanReadableStorage(
bytes: number,
round: 'round-up' | 'round-down' = 'round-down'
): string {
const i = Math.floor(Math.log(bytes) / Math.log(1024));
@ -139,6 +138,13 @@ export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
}
}
export function getTotalFamilyUsage(familyData: FamilyData): number {
return familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0
);
}
export function getLocalUserSubscription(): Subscription {
return getData(LS_KEYS.SUBSCRIPTION);
}

View file

@ -754,6 +754,7 @@ const englishConstants = {
GB: 'GB',
FREE_PLAN_OPTION_LABEL: 'Continue with free trial',
FREE_PLAN_DESCRIPTION: '1 GB for 1 year',
CURRENT_USAGE: (usage) => `Current usage is ${usage}`,
};
export default englishConstants;

View file

@ -24,5 +24,5 @@ export function getUserAnonymizedID() {
}
export function getLocalUserDetails(): UserDetails {
return getData(LS_KEYS.USER_DETAILS);
return getData(LS_KEYS.USER_DETAILS)?.value;
}