Merge pull request #608 from ente-io/family-member-screen
Family member screen
This commit is contained in:
commit
ae3f64745d
BIN
public/images/family_plan_leave@3x.png
Normal file
BIN
public/images/family_plan_leave@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 408 KiB |
73
src/components/MemberSubscriptionManage.tsx
Normal file
73
src/components/MemberSubscriptionManage.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { Button, DialogContent, Typography } from '@mui/material';
|
||||||
|
import VerticallyCentered from 'components/Container';
|
||||||
|
import DialogBoxBase from 'components/DialogBox/base';
|
||||||
|
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
|
||||||
|
import { AppContext } from 'pages/_app';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import billingService from 'services/billingService';
|
||||||
|
import { getFamilyPlanAdmin } from 'utils/billing';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
export function MemberSubscriptionManage({ open, userDetails, onClose }) {
|
||||||
|
const { setDialogMessage } = useContext(AppContext);
|
||||||
|
|
||||||
|
async function onLeaveFamilyClick() {
|
||||||
|
try {
|
||||||
|
await billingService.leaveFamily();
|
||||||
|
} catch (e) {
|
||||||
|
setDialogMessage({
|
||||||
|
title: constants.ERROR,
|
||||||
|
close: { variant: 'danger' },
|
||||||
|
content: constants.UNKNOWN_ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const confirmLeaveFamily = () =>
|
||||||
|
setDialogMessage({
|
||||||
|
title: `${constants.LEAVE_FAMILY}`,
|
||||||
|
content: constants.LEAVE_FAMILY_CONFIRM,
|
||||||
|
proceed: {
|
||||||
|
text: constants.LEAVE_FAMILY,
|
||||||
|
action: onLeaveFamilyClick,
|
||||||
|
variant: 'danger',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
text: constants.CANCEL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userDetails) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogBoxBase open={open} onClose={onClose} maxWidth="xs">
|
||||||
|
<DialogTitleWithCloseButton onClose={onClose}>
|
||||||
|
<Typography variant="h3">{constants.SUBSCRIPTION}</Typography>
|
||||||
|
<Typography color={'text.secondary'}>
|
||||||
|
{constants.FAMILY_PLAN}
|
||||||
|
</Typography>
|
||||||
|
</DialogTitleWithCloseButton>
|
||||||
|
<DialogContent>
|
||||||
|
<VerticallyCentered>
|
||||||
|
<Typography color="text.secondary ">
|
||||||
|
{constants.FAMILY_SUBSCRIPTION_INFO(
|
||||||
|
getFamilyPlanAdmin(userDetails.familyData)?.email
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<img
|
||||||
|
height="267px"
|
||||||
|
width="256px"
|
||||||
|
src="/images/family_plan_leave@3x.png"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
color="danger"
|
||||||
|
onClick={confirmLeaveFamily}>
|
||||||
|
{constants.LEAVE_FAMILY}
|
||||||
|
</Button>
|
||||||
|
</VerticallyCentered>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogBoxBase>
|
||||||
|
);
|
||||||
|
}
|
|
@ -12,10 +12,13 @@ import {
|
||||||
import { SubscriptionCardContentOverlay } from './contentOverlay';
|
import { SubscriptionCardContentOverlay } from './contentOverlay';
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
userDetails: UserDetails;
|
userDetails: UserDetails;
|
||||||
closeSidebar: () => void;
|
openMemberSubscriptionDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SubscriptionCard({ userDetails }: Iprops) {
|
export default function SubscriptionCard({
|
||||||
|
userDetails,
|
||||||
|
openMemberSubscriptionDialog,
|
||||||
|
}: Iprops) {
|
||||||
const { showPlanSelectorModal } = useContext(GalleryContext);
|
const { showPlanSelectorModal } = useContext(GalleryContext);
|
||||||
|
|
||||||
if (!userDetails) {
|
if (!userDetails) {
|
||||||
|
@ -30,9 +33,9 @@ export default function SubscriptionCard({ userDetails }: Iprops) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowClick =
|
const isMemberSubscription =
|
||||||
!isPartOfFamily(userDetails.familyData) ||
|
isPartOfFamily(userDetails.familyData) &&
|
||||||
isFamilyAdmin(userDetails.familyData);
|
!isFamilyAdmin(userDetails.familyData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative">
|
<Box position="relative">
|
||||||
|
@ -43,7 +46,13 @@ export default function SubscriptionCard({ userDetails }: Iprops) {
|
||||||
}}
|
}}
|
||||||
src="/images/subscription-card-background.png"
|
src="/images/subscription-card-background.png"
|
||||||
/>
|
/>
|
||||||
{allowClick && <ClickOverlay onClick={showPlanSelectorModal} />}
|
<ClickOverlay
|
||||||
|
onClick={
|
||||||
|
isMemberSubscription
|
||||||
|
? openMemberSubscriptionDialog
|
||||||
|
: showPlanSelectorModal
|
||||||
|
}
|
||||||
|
/>
|
||||||
<SubscriptionCardContentOverlay
|
<SubscriptionCardContentOverlay
|
||||||
hasNonAdminFamilyMembers={hasNonAdminFamilyMembers}
|
hasNonAdminFamilyMembers={hasNonAdminFamilyMembers}
|
||||||
userDetails={userDetails}
|
userDetails={userDetails}
|
||||||
|
|
|
@ -15,7 +15,8 @@ export function AdminSubscriptionStatus({
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color={'text.secondary'}
|
color={'text.secondary'}
|
||||||
onClick={showPlanSelectorModal}>
|
onClick={showPlanSelectorModal}
|
||||||
|
sx={{ cursor: 'pointer' }}>
|
||||||
{isSubscriptionActive(userDetails.subscription)
|
{isSubscriptionActive(userDetails.subscription)
|
||||||
? isOnFreePlan(userDetails.subscription)
|
? isOnFreePlan(userDetails.subscription)
|
||||||
? constants.FREE_SUBSCRIPTION_INFO(
|
? constants.FREE_SUBSCRIPTION_INFO(
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { MemberSubscriptionStatus } from './member';
|
|
||||||
import { AdminSubscriptionStatus as AdminSubscriptionStatus } from './admin';
|
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import React, { useContext, useMemo } from 'react';
|
import React, { useContext, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
hasNonAdminFamilyMembers,
|
hasNonAdminFamilyMembers,
|
||||||
|
hasPaidSubscription,
|
||||||
isFamilyAdmin,
|
isFamilyAdmin,
|
||||||
isOnFreePlan,
|
isOnFreePlan,
|
||||||
isSubscriptionActive,
|
isSubscriptionActive,
|
||||||
|
@ -11,6 +10,8 @@ import {
|
||||||
} from 'utils/billing';
|
} from 'utils/billing';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { UserDetails } from 'types/user';
|
import { UserDetails } from 'types/user';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
|
||||||
export default function SubscriptionStatus({
|
export default function SubscriptionStatus({
|
||||||
userDetails,
|
userDetails,
|
||||||
|
@ -34,14 +35,29 @@ export default function SubscriptionStatus({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={1}>
|
<Box px={1}>
|
||||||
{!hasNonAdminFamilyMembers(userDetails.familyData) ||
|
{(!hasNonAdminFamilyMembers(userDetails.familyData) ||
|
||||||
isFamilyAdmin(userDetails.familyData) ? (
|
isFamilyAdmin(userDetails.familyData) ||
|
||||||
<AdminSubscriptionStatus
|
hasPaidSubscription(userDetails.subscription)) && (
|
||||||
userDetails={userDetails}
|
<Typography
|
||||||
showPlanSelectorModal={showPlanSelectorModal}
|
variant="body2"
|
||||||
/>
|
color={'text.secondary'}
|
||||||
) : (
|
onClick={showPlanSelectorModal}
|
||||||
<MemberSubscriptionStatus userDetails={userDetails} />
|
sx={{ cursor: 'pointer' }}>
|
||||||
|
{isSubscriptionActive(userDetails.subscription)
|
||||||
|
? isOnFreePlan(userDetails.subscription)
|
||||||
|
? constants.FREE_SUBSCRIPTION_INFO(
|
||||||
|
userDetails.subscription?.expiryTime
|
||||||
|
)
|
||||||
|
: isSubscriptionCancelled(
|
||||||
|
userDetails.subscription
|
||||||
|
) &&
|
||||||
|
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
|
||||||
|
userDetails.subscription?.expiryTime
|
||||||
|
)
|
||||||
|
: constants.SUBSCRIPTION_EXPIRED_MESSAGE(
|
||||||
|
showPlanSelectorModal
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { Button, Stack, Typography } from '@mui/material';
|
|
||||||
import { AppContext } from 'pages/_app';
|
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import billingService from 'services/billingService';
|
|
||||||
import { getFamilyPlanAdmin } from 'utils/billing';
|
|
||||||
import constants from 'utils/strings/constants';
|
|
||||||
export function MemberSubscriptionStatus({ userDetails }) {
|
|
||||||
const { setDialogMessage } = useContext(AppContext);
|
|
||||||
|
|
||||||
async function onLeaveFamilyClick() {
|
|
||||||
try {
|
|
||||||
await billingService.leaveFamily();
|
|
||||||
} catch (e) {
|
|
||||||
setDialogMessage({
|
|
||||||
title: constants.ERROR,
|
|
||||||
close: { variant: 'danger' },
|
|
||||||
content: constants.UNKNOWN_ERROR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const confirmLeaveFamily = () =>
|
|
||||||
setDialogMessage({
|
|
||||||
title: `${constants.LEAVE_FAMILY}`,
|
|
||||||
content: constants.LEAVE_FAMILY_CONFIRM,
|
|
||||||
proceed: {
|
|
||||||
text: constants.LEAVE_FAMILY,
|
|
||||||
action: onLeaveFamilyClick,
|
|
||||||
variant: 'danger',
|
|
||||||
},
|
|
||||||
close: {
|
|
||||||
text: constants.CANCEL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Stack spacing={1}>
|
|
||||||
<Typography variant="body2" color="text.secondary ">
|
|
||||||
{constants.FAMILY_PLAN_MANAGE_ADMIN_ONLY(
|
|
||||||
getFamilyPlanAdmin(userDetails.familyData)?.email
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
<Button onClick={confirmLeaveFamily}>
|
|
||||||
{constants.LEAVE_FAMILY}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -24,10 +24,7 @@ export default function Sidebar({
|
||||||
<DrawerSidebar open={sidebarView} onClose={closeSidebar}>
|
<DrawerSidebar open={sidebarView} onClose={closeSidebar}>
|
||||||
<HeaderSection closeSidebar={closeSidebar} />
|
<HeaderSection closeSidebar={closeSidebar} />
|
||||||
<PaddedDivider spaced />
|
<PaddedDivider spaced />
|
||||||
<UserDetailsSection
|
<UserDetailsSection sidebarView={sidebarView} />
|
||||||
sidebarView={sidebarView}
|
|
||||||
closeSidebar={closeSidebar}
|
|
||||||
/>
|
|
||||||
<PaddedDivider invisible />
|
<PaddedDivider invisible />
|
||||||
<ShortcutSection
|
<ShortcutSection
|
||||||
closeSidebar={closeSidebar}
|
closeSidebar={closeSidebar}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import SubscriptionCard from './SubscriptionCard';
|
import SubscriptionCard from './SubscriptionCard';
|
||||||
import { getUserDetailsV2 } from 'services/userService';
|
import { getUserDetailsV2 } from 'services/userService';
|
||||||
import { UserDetails } from 'types/user';
|
import { UserDetails } from 'types/user';
|
||||||
|
@ -8,12 +8,21 @@ import Typography from '@mui/material/Typography';
|
||||||
import SubscriptionStatus from './SubscriptionStatus';
|
import SubscriptionStatus from './SubscriptionStatus';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { Skeleton } from '@mui/material';
|
import { Skeleton } from '@mui/material';
|
||||||
|
import { MemberSubscriptionManage } from '../MemberSubscriptionManage';
|
||||||
|
|
||||||
export default function UserDetailsSection({ sidebarView, closeSidebar }) {
|
export default function UserDetailsSection({ sidebarView }) {
|
||||||
const [userDetails, setUserDetails] = useLocalState<UserDetails>(
|
const [userDetails, setUserDetails] = useLocalState<UserDetails>(
|
||||||
LS_KEYS.USER_DETAILS
|
LS_KEYS.USER_DETAILS
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [memberSubscriptionManageView, setMemberSubscriptionManageView] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const openMemberSubscriptionManage = () =>
|
||||||
|
setMemberSubscriptionManageView(true);
|
||||||
|
const closeMemberSubscriptionManage = () =>
|
||||||
|
setMemberSubscriptionManageView(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sidebarView) {
|
if (!sidebarView) {
|
||||||
return;
|
return;
|
||||||
|
@ -26,6 +35,7 @@ export default function UserDetailsSection({ sidebarView, closeSidebar }) {
|
||||||
}, [sidebarView]);
|
}, [sidebarView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Typography px={1} color="text.secondary">
|
<Typography px={1} color="text.secondary">
|
||||||
{userDetails ? (
|
{userDetails ? (
|
||||||
|
@ -37,9 +47,16 @@ export default function UserDetailsSection({ sidebarView, closeSidebar }) {
|
||||||
|
|
||||||
<SubscriptionCard
|
<SubscriptionCard
|
||||||
userDetails={userDetails}
|
userDetails={userDetails}
|
||||||
closeSidebar={closeSidebar}
|
openMemberSubscriptionDialog={openMemberSubscriptionManage}
|
||||||
/>
|
/>
|
||||||
<SubscriptionStatus userDetails={userDetails} />
|
<SubscriptionStatus userDetails={userDetails} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<MemberSubscriptionManage
|
||||||
|
userDetails={userDetails}
|
||||||
|
open={memberSubscriptionManageView}
|
||||||
|
onClose={closeMemberSubscriptionManage}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,6 @@ const darkThemeOptions = createTheme({
|
||||||
},
|
},
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
textTransform: 'none',
|
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
},
|
},
|
||||||
|
@ -171,9 +170,27 @@ const darkThemeOptions = createTheme({
|
||||||
display: 'block',
|
display: 'block',
|
||||||
},
|
},
|
||||||
caption: {
|
caption: {
|
||||||
|
display: 'block',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
lineHeight: '15px',
|
lineHeight: '15px',
|
||||||
},
|
},
|
||||||
|
h1: {
|
||||||
|
fontSize: '36px',
|
||||||
|
lineHeight: '44px',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '30px',
|
||||||
|
lineHeight: '36px',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '24px',
|
||||||
|
lineHeight: '29px',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: '18px',
|
||||||
|
lineHeight: '22px',
|
||||||
|
},
|
||||||
|
|
||||||
fontFamily: ['Inter', 'sans-serif'].join(','),
|
fontFamily: ['Inter', 'sans-serif'].join(','),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -252,6 +252,7 @@ const englishConstants = {
|
||||||
// ========================
|
// ========================
|
||||||
// Subscription
|
// Subscription
|
||||||
// ========================
|
// ========================
|
||||||
|
SUBSCRIPTION: 'Subscription',
|
||||||
SUBSCRIBE: 'Subscribe',
|
SUBSCRIBE: 'Subscribe',
|
||||||
SUBSCRIPTION_PLAN: 'Subscription plan',
|
SUBSCRIPTION_PLAN: 'Subscription plan',
|
||||||
USAGE_DETAILS: 'Usage',
|
USAGE_DETAILS: 'Usage',
|
||||||
|
@ -273,10 +274,9 @@ const englishConstants = {
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
||||||
FAMILY_PLAN_MANAGE_ADMIN_ONLY: (adminEmail) => (
|
FAMILY_SUBSCRIPTION_INFO: (adminEmail) => (
|
||||||
<>
|
<>
|
||||||
Only your family plan admin <strong>{adminEmail}</strong> can change
|
You are on a family plan managed by <strong>{adminEmail}</strong>
|
||||||
the plan
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
RENEWAL_ACTIVE_SUBSCRIPTION_INFO: (expiryTime) => (
|
RENEWAL_ACTIVE_SUBSCRIPTION_INFO: (expiryTime) => (
|
||||||
|
@ -777,6 +777,7 @@ const englishConstants = {
|
||||||
OF: 'of',
|
OF: 'of',
|
||||||
MONTH_SHORT: 'mo',
|
MONTH_SHORT: 'mo',
|
||||||
YEAR_SHORT: 'yr',
|
YEAR_SHORT: 'yr',
|
||||||
|
FAMILY_PLAN: 'Family plan',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default englishConstants;
|
export default englishConstants;
|
||||||
|
|
Loading…
Reference in a new issue