Merge pull request #1048 from automatisch/aut-92

feat: add trial status badge in appbar
This commit is contained in:
Ömer Faruk Aydın
2023-04-09 15:30:04 +02:00
committed by GitHub
12 changed files with 204 additions and 29 deletions

View File

@@ -9,7 +9,9 @@ const getTrialStatus = async (
if (!appConfig.isCloud) return;
const inTrial = await context.currentUser.inTrial();
if (!inTrial) return;
const hasActiveSubscription = await context.currentUser.hasActiveSubscription();
if (!inTrial && hasActiveSubscription) return;
return {
expireAt: context.currentUser.trialExpiryDate,

View File

@@ -165,6 +165,16 @@ class User extends Base {
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
}
async hasActiveSubscription() {
if (!appConfig.isCloud) {
return false;
}
const subscription = await this.$relatedQuery('currentSubscription');
return subscription?.isActive;
}
async inTrial() {
if (!appConfig.isCloud) {
return false;
@@ -174,9 +184,7 @@ class User extends Base {
return false;
}
const subscription = await this.$relatedQuery('currentSubscription');
if (subscription?.isActive) {
if (await this.hasActiveSubscription()) {
return false;
}

View File

@@ -12,6 +12,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import * as URLS from 'config/urls';
import AccountDropdownMenu from 'components/AccountDropdownMenu';
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
import Container from 'components/Container';
import { FormattedMessage } from 'react-intl';
import { Link } from './style';
@@ -67,9 +68,10 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
</Link>
</div>
<TrialStatusBadge />
<IconButton
size="large"
edge="start"
color="inherit"
onClick={handleAccountMenuOpen}
aria-controls={accountMenuId}

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import * as URLS from 'config/urls';
import { generateInternalLink } from 'helpers/translation-values';
import useTrialStatus from 'hooks/useTrialStatus.ee';
import useFormatMessage from 'hooks/useFormatMessage';
export default function TrialOverAlert() {
const formatMessage = useFormatMessage();
const trialStatus = useTrialStatus();
if (!trialStatus || !trialStatus.over) return <React.Fragment />;
return (
<Alert
severity="error"
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<Typography variant="subtitle2" sx={{ lineHeight: 1.5 }}>
{formatMessage('trialOverAlert.text', {
link: generateInternalLink(URLS.SETTINGS_PLAN_UPGRADE)
})}
</Typography>
</Alert>
);
}

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Chip } from './style.ee';
import * as URLS from 'config/urls';
import useTrialStatus from 'hooks/useTrialStatus.ee';
export default function TrialStatusBadge(): React.ReactElement {
const data = useTrialStatus();
if (!data) return <React.Fragment />;
const { message, status } = data;
return (
<Chip
component={Link}
to={URLS.SETTINGS_BILLING_AND_USAGE}
clickable
label={message}
color={status}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { styled } from '@mui/material/styles';
import MuiChip, { chipClasses } from '@mui/material/Chip';
export const Chip = styled(MuiChip)`
&.${chipClasses.root} {
font-weight: 500;
}
&.${chipClasses.colorWarning} {
background: #fef3c7;
color: #78350f;
}
` as typeof MuiChip;

View File

@@ -1,5 +1,7 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import Alert from '@mui/material/Alert';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
@@ -11,11 +13,13 @@ import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { TBillingCardAction } from '@automatisch/types';
import TrialOverAlert from 'components/TrialOverAlert/index.ee';
import * as URLS from 'config/urls';
import useBillingAndUsageData from 'hooks/useBillingAndUsageData.ee';
import useFormatMessage from 'hooks/useFormatMessage';
const capitalize = (str: string) => str[0].toUpperCase() + str.slice(1, str.length);
const capitalize = (str: string) =>
str[0].toUpperCase() + str.slice(1, str.length);
type BillingCardProps = {
name: string;
@@ -62,21 +66,13 @@ function Action(props: { action?: TBillingCardAction }) {
if (type === 'link') {
if (action.src.startsWith('http')) {
return (
<Button
size="small"
href={action.src}
target="_blank"
>
<Button size="small" href={action.src} target="_blank">
{text}
</Button>
)
);
} else {
return (
<Button
size="small"
component={Link}
to={action.src}
>
<Button size="small" component={Link} to={action.src}>
{text}
</Button>
);
@@ -100,6 +96,9 @@ export default function UsageDataInformation() {
return (
<React.Fragment>
<Stack sx={{ width: '100%', mb: 2 }} spacing={2}>
<TrialOverAlert />
</Stack>
<Card sx={{ mb: 3, p: 2 }}>
<CardContent sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}>
@@ -137,7 +136,9 @@ export default function UsageDataInformation() {
<BillingCard
name={formatMessage('usageDataInformation.nextBillAmount')}
title={billingAndUsageData?.subscription?.nextBillAmount.title}
action={billingAndUsageData?.subscription?.nextBillAmount.action}
action={
billingAndUsageData?.subscription?.nextBillAmount.action
}
/>
</Grid>
@@ -192,15 +193,17 @@ export default function UsageDataInformation() {
</Box>
{/* free plan has `null` status so that we can show the upgrade button */}
{billingAndUsageData?.subscription?.status === null && <Button
component={Link}
to={URLS.SETTINGS_PLAN_UPGRADE}
size="small"
variant="contained"
sx={{ mt: 2, alignSelf: 'flex-end' }}
>
{formatMessage('usageDataInformation.upgrade')}
</Button>}
{billingAndUsageData?.subscription?.status === null && (
<Button
component={Link}
to={URLS.SETTINGS_PLAN_UPGRADE}
size="small"
variant="contained"
sx={{ mt: 2, alignSelf: 'flex-end' }}
>
{formatMessage('usageDataInformation.upgrade')}
</Button>
)}
</CardContent>
</Card>
</React.Fragment>

View File

@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const GET_TRIAL_STATUS = gql`
query GetTrialStatus {
getTrialStatus {
expireAt
}
}
`;

View File

@@ -1,5 +1,13 @@
import { Link as RouterLink } from 'react-router-dom';
import Link from '@mui/material/Link';
export const generateInternalLink = (link: string) => (str: string) =>
(
<Link component={RouterLink} to={link}>
{str}
</Link>
);
export const generateExternalLink = (link: string) => (str: string) =>
(
<Link href={link} target="_blank">

View File

@@ -23,7 +23,7 @@ function transform(billingAndUsageData: NonNullable<UseBillingAndUsageDataReturn
}
type UseBillingAndUsageDataReturn = {
subscription: TSubscription,
subscription: TSubscription;
usage: {
task: number;
}

View File

@@ -0,0 +1,70 @@
import { useQuery } from '@apollo/client';
import { DateTime } from 'luxon';
import { GET_TRIAL_STATUS } from 'graphql/queries/get-trial-status.ee';
import useFormatMessage from './useFormatMessage';
type UseTrialStatusReturn = {
expireAt: DateTime;
message: string;
over: boolean;
status: 'error' | 'warning';
} | null;
function getDiffInDays(date: DateTime) {
const today = DateTime.now().startOf('day');
const diffInDays = date.diff(today, 'days').days;
const roundedDiffInDays = Math.round(diffInDays);
return roundedDiffInDays;
}
function getFeedbackPayload(date: DateTime) {
const diffInDays = getDiffInDays(date);
if (diffInDays <= -1) {
return {
translationEntryId: 'trialBadge.over',
status: 'error' as const,
over: true,
};
} else if (diffInDays <= 0) {
return {
translationEntryId: 'trialBadge.endsToday',
status: 'warning' as const,
over: false,
}
} else {
return {
translationEntryId: 'trialBadge.xDaysLeft',
translationEntryValues: {
remainingDays: diffInDays
},
status: 'warning' as const,
over: false,
}
}
}
export default function useTrialStatus(): UseTrialStatusReturn {
const formatMessage = useFormatMessage();
const { data, loading } = useQuery(GET_TRIAL_STATUS);
if (loading || !data.getTrialStatus) return null;
const expireAt = DateTime.fromMillis(Number(data.getTrialStatus.expireAt)).startOf('day');
const {
translationEntryId,
translationEntryValues,
status,
over,
} = getFeedbackPayload(expireAt);
return {
message: formatMessage(translationEntryId, translationEntryValues),
expireAt,
over,
status
};
}

View File

@@ -152,5 +152,9 @@
"invoices.date": "Date",
"invoices.amount": "Amount",
"invoices.invoice": "Invoice",
"invoices.link": "Link"
"invoices.link": "Link",
"trialBadge.xDaysLeft": "{remainingDays} trial {remainingDays, plural, one {day} other {days}} left",
"trialBadge.endsToday": "Trial ends today",
"trialBadge.over": "Trial is over",
"trialOverAlert.text": "Your free trial is over. Please <link>upgrade</link> your plan to continue using Automatisch."
}