update plan selector to use mui dialog

This commit is contained in:
Abhinav 2022-06-09 16:20:33 +05:30
parent 03228664f6
commit 1abd62964f
9 changed files with 498 additions and 422 deletions

View file

@ -38,10 +38,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
<Link <Link
component="button" component="button"
sx={{ sx={{
color: color: props.color && `${props.color}.main`,
props.color && typeof props.color === 'object'
? `${props.color}.main`
: props.color,
...sx, ...sx,
}} }}
{...props}> {...props}>

View file

@ -1,417 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { Form, Modal, Button } from 'react-bootstrap';
import constants from 'utils/strings/constants';
import styled, { css } from 'styled-components';
import { Plan, Subscription } from 'types/billing';
import {
convertBytesToGBs,
getUserSubscription,
isUserSubscribedPlan,
isSubscriptionCancelled,
updatePaymentMethod,
updateSubscription,
activateSubscription,
cancelSubscription,
hasStripeSubscription,
hasPaidSubscription,
isOnFreePlan,
planForSubscription,
hasMobileSubscription,
hasPaypalSubscription,
manageFamilyMethod,
} from 'utils/billing';
import { reverseString } from 'utils/common';
import ArrowEast from 'components/icons/ArrowEast';
import LinkButton from './LinkButton';
import { DeadCenter, GalleryContext } from 'pages/gallery';
import billingService from 'services/billingService';
import { SetLoading } from 'types/gallery';
import { logError } from 'utils/sentry';
import { AppContext } from 'pages/_app';
export const PlanIcon = styled.div<{ currentlySubscribed: boolean }>`
border-radius: 20px;
width: 220px;
border: 2px solid #333;
padding: 30px;
margin: 10px;
text-align: center;
font-size: 20px;
background-color: #ffffff00;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: ${(props) =>
props.currentlySubscribed ? 'not-allowed' : 'pointer'};
border-color: ${(props) => props.currentlySubscribed && '#56e066'};
transition: all 0.3s ease-out;
overflow: hidden;
position: relative;
& > div:first-child::before {
content: ' ';
height: 600px;
width: 50px;
background-color: #444;
left: 0;
top: -50%;
position: absolute;
transform: rotate(45deg) translateX(-200px);
transition: all 0.5s ease-out;
}
&:hover
${(props) =>
!props.currentlySubscribed &&
css`
{
transform: scale(1.1);
background-color: #ffffff11;
}
`}
&:hover
> div:first-child::before {
transform: rotate(45deg) translateX(300px);
}
`;
interface Props {
modalView: boolean;
closeModal: any;
setLoading: SetLoading;
}
enum PLAN_PERIOD {
MONTH = 'month',
YEAR = 'year',
}
function PlanSelector(props: Props) {
const subscription: Subscription = getUserSubscription();
const [plans, setPlans] = useState<Plan[]>(null);
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: 'success',
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: 'success',
},
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' },
});
}
}
}
const PlanIcons: JSX.Element[] = plans
?.filter((plan) => plan.period === planPeriod)
?.map((plan) => (
<PlanIcon
key={plan.stripeID}
className="subscription-plan-selector"
currentlySubscribed={isUserSubscribedPlan(plan, subscription)}
onClick={
isUserSubscribedPlan(plan, subscription)
? () => {}
: async () => await onPlanSelect(plan)
}>
<div>
<span
style={{
color: '#ECECEC',
fontWeight: 900,
fontSize: '40px',
lineHeight: '40px',
}}>
{convertBytesToGBs(plan.storage, 0)}
</span>
<span
style={{
color: '#858585',
fontSize: '24px',
fontWeight: 900,
}}>
{' '}
GB
</span>
</div>
<div
className="bold-text"
style={{
color: '#aaa',
lineHeight: '36px',
fontSize: '20px',
}}>
{`${plan.price} / ${plan.period}`}
</div>
<Button
variant="outline-success"
block
style={{
marginTop: '20px',
fontSize: '14px',
display: 'flex',
justifyContent: 'center',
}}
disabled={isUserSubscribedPlan(plan, subscription)}>
{constants.CHOOSE_PLAN_BTN}
<ArrowEast style={{ marginLeft: '5px' }} />
</Button>
</PlanIcon>
));
return (
<Modal
show={props.modalView}
onHide={props.closeModal}
size="xl"
centered
backdrop={hasPaidSubscription(subscription) ? true : 'static'}
contentClassName="plan-selector-modal-content">
<Modal.Header closeButton>
<Modal.Title
style={{
marginLeft: '12px',
width: '100%',
textAlign: 'center',
}}>
<span>
{hasPaidSubscription(subscription)
? constants.MANAGE_PLAN
: constants.CHOOSE_PLAN}
</span>
</Modal.Title>
</Modal.Header>
<Modal.Body style={{ marginTop: '20px' }}>
<DeadCenter>
<div style={{ display: 'flex' }}>
<span
className="bold-text"
style={{ fontSize: '16px' }}>
{constants.MONTHLY}
</span>
<Form.Switch
checked={planPeriod === PLAN_PERIOD.YEAR}
id="plan-period-toggler"
style={{
margin: '-4px 0 20px 15px',
fontSize: '10px',
}}
className="custom-switch-md"
onChange={togglePeriod}
/>
<span
className="bold-text"
style={{ fontSize: '16px' }}>
{constants.YEARLY}
</span>
</div>
</DeadCenter>
<div
style={{
display: 'flex',
justifyContent: 'space-around',
flexWrap: 'wrap',
minHeight: '212px',
margin: '5px 0',
}}>
{plans && PlanIcons}
</div>
<DeadCenter style={{ marginBottom: '30px' }}>
{hasPaidSubscription(subscription) ? (
<>
{hasStripeSubscription(subscription) && (
<>
{isSubscriptionCancelled(subscription) ? (
<LinkButton
color="success"
onClick={() =>
appContext.setDialogMessage({
title: constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
content:
constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
subscription.expiryTime
),
proceed: {
text: constants.ACTIVATE_SUBSCRIPTION,
action: activateSubscription.bind(
null,
appContext.setDialogMessage,
props.closeModal,
props.setLoading
),
variant: 'success',
},
close: {
text: constants.CANCEL,
},
})
}>
{constants.ACTIVATE_SUBSCRIPTION}
</LinkButton>
) : (
<LinkButton
color="danger"
onClick={() =>
appContext.setDialogMessage({
title: constants.CONFIRM_CANCEL_SUBSCRIPTION,
content:
constants.CANCEL_SUBSCRIPTION_MESSAGE(),
proceed: {
text: constants.CANCEL_SUBSCRIPTION,
action: cancelSubscription.bind(
null,
appContext.setDialogMessage,
props.closeModal,
props.setLoading
),
variant: 'danger',
},
close: {
text: constants.CANCEL,
},
})
}>
{constants.CANCEL_SUBSCRIPTION}
</LinkButton>
)}
<LinkButton
color="primary"
onClick={updatePaymentMethod.bind(
null,
appContext.setDialogMessage,
props.setLoading
)}
style={{ marginTop: '20px' }}>
{constants.MANAGEMENT_PORTAL}
</LinkButton>
</>
)}
<LinkButton
color="primary"
onClick={manageFamilyMethod.bind(
null,
appContext.setDialogMessage,
props.setLoading
)}
style={{ marginTop: '20px' }}>
{constants.MANAGE_FAMILY_PORTAL}
</LinkButton>
</>
) : (
<LinkButton
color="primary"
onClick={props.closeModal}
style={{
color: 'rgb(121, 121, 121)',
marginTop: '20px',
}}>
{isOnFreePlan(subscription)
? constants.SKIP
: constants.CLOSE}
</LinkButton>
)}
</DeadCenter>
</Modal.Body>
</Modal>
);
}
export default PlanSelector;

View file

@ -0,0 +1,184 @@
import { PeriodToggler } from './periodToggler';
import { ManageSubscription } from './manageSubscription';
import React, { useContext, useEffect, useState } from 'react';
import constants from 'utils/strings/constants';
import { Plan, Subscription } from 'types/billing';
import {
getUserSubscription,
isUserSubscribedPlan,
isSubscriptionCancelled,
updateSubscription,
hasStripeSubscription,
hasPaidSubscription,
isOnFreePlan,
planForSubscription,
hasMobileSubscription,
hasPaypalSubscription,
} 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 DialogBox from 'components/DialogBox';
import Plans from './plans';
import { DialogBoxAttributes } from 'types/dialogBox';
interface Props {
modalView: boolean;
closeModal: any;
setLoading: SetLoading;
}
export enum PLAN_PERIOD {
MONTH = 'month',
YEAR = 'year',
}
function PlanSelector(props: Props) {
const subscription: Subscription = getUserSubscription();
const [plans, setPlans] = useState<Plan[]>(null);
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: 'success',
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: 'success',
},
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' },
});
}
}
}
const planSelectorAttributes: DialogBoxAttributes = {
closeOnBackdropClick: hasPaidSubscription(subscription) ? true : false,
title: hasPaidSubscription(subscription)
? constants.MANAGE_PLAN
: constants.CHOOSE_PLAN,
};
return (
<DialogBox
open={props.modalView}
titleCloseButton
onClose={props.closeModal}
size={'xl'}
attributes={planSelectorAttributes}
fullWidth={false}>
<PeriodToggler
planPeriod={planPeriod}
togglePeriod={togglePeriod}
/>
<Plans
plans={plans}
planPeriod={planPeriod}
onPlanSelect={onPlanSelect}
subscription={subscription}
/>
<ManageSubscription
subscription={subscription}
closeModal={props.closeModal}
setLoading={props.setLoading}
/>
</DialogBox>
);
}
export default PlanSelector;

View file

@ -0,0 +1,122 @@
import { DeadCenter } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import {
activateSubscription,
cancelSubscription,
updatePaymentMethod,
manageFamilyMethod,
hasPaidSubscription,
hasStripeSubscription,
isOnFreePlan,
isSubscriptionCancelled,
} from 'utils/billing';
import constants from 'utils/strings/constants';
import LinkButton from '../LinkButton';
export function ManageSubscription({ subscription, ...props }) {
const appContext = useContext(AppContext);
return (
<DeadCenter
style={{
marginBottom: '30px',
}}>
{hasPaidSubscription(subscription) ? (
<>
{hasStripeSubscription(subscription) && (
<>
{isSubscriptionCancelled(subscription) ? (
<LinkButton
color="accent"
onClick={() =>
appContext.setDialogMessage({
title: constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
content:
constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
subscription.expiryTime
),
proceed: {
text: constants.ACTIVATE_SUBSCRIPTION,
action: activateSubscription.bind(
null,
appContext.setDialogMessage,
props.closeModal,
props.setLoading
),
variant: 'success',
},
close: {
text: constants.CANCEL,
},
})
}>
{constants.ACTIVATE_SUBSCRIPTION}
</LinkButton>
) : (
<LinkButton
color="danger"
onClick={() =>
appContext.setDialogMessage({
title: constants.CONFIRM_CANCEL_SUBSCRIPTION,
content:
constants.CANCEL_SUBSCRIPTION_MESSAGE(),
proceed: {
text: constants.CANCEL_SUBSCRIPTION,
action: cancelSubscription.bind(
null,
appContext.setDialogMessage,
props.closeModal,
props.setLoading
),
variant: 'danger',
},
close: {
text: constants.CANCEL,
},
})
}>
{constants.CANCEL_SUBSCRIPTION}
</LinkButton>
)}
<LinkButton
color="primary"
onClick={updatePaymentMethod.bind(
null,
appContext.setDialogMessage,
props.setLoading
)}
style={{
marginTop: '20px',
}}>
{constants.MANAGEMENT_PORTAL}
</LinkButton>
</>
)}
<LinkButton
color="primary"
onClick={manageFamilyMethod.bind(
null,
appContext.setDialogMessage,
props.setLoading
)}
style={{
marginTop: '20px',
}}>
{constants.MANAGE_FAMILY_PORTAL}
</LinkButton>
</>
) : (
<LinkButton
color="primary"
onClick={props.closeModal}
style={{
color: 'rgb(121, 121, 121)',
marginTop: '20px',
}}>
{isOnFreePlan(subscription)
? constants.SKIP
: constants.CLOSE}
</LinkButton>
)}
</DeadCenter>
);
}

View file

@ -0,0 +1,41 @@
import { DeadCenter } from 'pages/gallery';
import React from 'react';
import { Form } from 'react-bootstrap';
import constants from 'utils/strings/constants';
import { PLAN_PERIOD } from '.';
export function PeriodToggler({ planPeriod, togglePeriod }) {
return (
<DeadCenter>
<div
style={{
display: 'flex',
}}>
<span
className="bold-text"
style={{
fontSize: '16px',
}}>
{constants.MONTHLY}
</span>
<Form.Switch
checked={planPeriod === PLAN_PERIOD.YEAR}
id="plan-period-toggler"
style={{
margin: '-4px 0 20px 15px',
fontSize: '10px',
}}
className="custom-switch-md"
onChange={togglePeriod}
/>
<span
className="bold-text"
style={{
fontSize: '16px',
}}>
{constants.YEARLY}
</span>
</div>
</DeadCenter>
);
}

View file

@ -0,0 +1,29 @@
import React from 'react';
import { isUserSubscribedPlan, convertBytesToGBs } from 'utils/billing';
import { PlanCard } from './planCard';
const Plans = ({ plans, planPeriod, subscription, onPlanSelect }) => (
<div
style={{
display: 'flex',
justifyContent: 'space-around',
flexWrap: 'wrap',
minHeight: '212px',
margin: '5px 0',
}}>
{plans
?.filter((plan) => plan.period === planPeriod)
?.map((plan) => (
<PlanCard
key={plan.stripeID}
isUserSubscribedPlan={isUserSubscribedPlan}
plan={plan}
subscription={subscription}
onPlanSelect={onPlanSelect}
convertBytesToGBs={convertBytesToGBs}
/>
))}
</div>
);
export default Plans;

View file

@ -0,0 +1,72 @@
import ArrowEast from 'components/icons/ArrowEast';
import React from 'react';
import { Button } from 'react-bootstrap';
import constants from 'utils/strings/constants';
import { PlanTile } from './planTile';
export function PlanCard({
isUserSubscribedPlan,
plan,
subscription,
onPlanSelect,
convertBytesToGBs,
}) {
return (
<PlanTile
key={plan.stripeID}
className="subscription-plan-selector"
currentlySubscribed={isUserSubscribedPlan(plan, subscription)}
onClick={
isUserSubscribedPlan(plan, subscription)
? () => {}
: async () => await onPlanSelect(plan)
}>
<div>
<span
style={{
color: '#ECECEC',
fontWeight: 900,
fontSize: '40px',
lineHeight: '40px',
}}>
{convertBytesToGBs(plan.storage, 0)}
</span>
<span
style={{
color: '#858585',
fontSize: '24px',
fontWeight: 900,
}}>
{' '}
GB
</span>
</div>
<div
className="bold-text"
style={{
color: '#aaa',
lineHeight: '36px',
fontSize: '20px',
}}>
{`${plan.price} / ${plan.period}`}
</div>
<Button
variant="outline-success"
block
style={{
marginTop: '20px',
fontSize: '14px',
display: 'flex',
justifyContent: 'center',
}}
disabled={isUserSubscribedPlan(plan, subscription)}>
{constants.CHOOSE_PLAN_BTN}
<ArrowEast
style={{
marginLeft: '5px',
}}
/>
</Button>
</PlanTile>
);
}

View file

@ -0,0 +1,48 @@
import styled, { css } from 'styled-components';
export const PlanTile = styled.div<{ currentlySubscribed: boolean }>`
border-radius: 20px;
width: 220px;
border: 2px solid #333;
padding: 30px;
margin: 10px;
text-align: center;
font-size: 20px;
background-color: #ffffff00;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: ${(props) =>
props.currentlySubscribed ? 'not-allowed' : 'pointer'};
border-color: ${(props) => props.currentlySubscribed && '#56e066'};
transition: all 0.3s ease-out;
overflow: hidden;
position: relative;
& > div:first-child::before {
content: ' ';
height: 600px;
width: 50px;
background-color: #444;
left: 0;
top: -50%;
position: absolute;
transform: rotate(45deg) translateX(-200px);
transition: all 0.5s ease-out;
}
&:hover
${(props) =>
!props.currentlySubscribed &&
css`
{
transform: scale(1.1);
background-color: #ffffff11;
}
`}
&:hover
> div:first-child::before {
transform: rotate(45deg) translateX(300px);
}
`;

View file

@ -149,7 +149,7 @@ export default function Gallery() {
count: 0, count: 0,
collectionID: 0, collectionID: 0,
}); });
const [planModalView, setPlanModalView] = useState(false); const [planModalView, setPlanModalView] = useState(true);
const [blockingLoad, setBlockingLoad] = useState(false); const [blockingLoad, setBlockingLoad] = useState(false);
const [collectionSelectorAttributes, setCollectionSelectorAttributes] = const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
useState<CollectionSelectorAttributes>(null); useState<CollectionSelectorAttributes>(null);