Merge pull request #1025 from automatisch/make-billing-and-usage-dynamic

feat: use actual data in billing and usage
This commit is contained in:
Ömer Faruk Aydın
2023-03-27 00:13:35 +03:00
committed by GitHub
11 changed files with 250 additions and 108 deletions

View File

@@ -8,10 +8,8 @@ echo "Configuring backend environment variables..."
cd packages/backend cd packages/backend
rm -rf .env rm -rf .env
echo " echo "
HOST=localhost
PROTOCOL=http
PORT=$BACKEND_PORT PORT=$BACKEND_PORT
WEB_APP_URL=https://$CODESPACE_NAME-$WEB_PORT.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN WEB_APP_URL=http://localhost:$WEB_PORT
APP_ENV=development APP_ENV=development
POSTGRES_DATABASE=automatisch POSTGRES_DATABASE=automatisch
POSTGRES_PORT=5432 POSTGRES_PORT=5432
@@ -30,8 +28,7 @@ cd packages/web
rm -rf .env rm -rf .env
echo " echo "
PORT=$WEB_PORT PORT=$WEB_PORT
REACT_APP_GRAPHQL_URL=https://$CODESPACE_NAME-$BACKEND_PORT.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/graphql REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
REACT_APP_BASE_URL=https://$CODESPACE_NAME-$WEB_PORT.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
REACT_APP_NOTIFICATIONS_URL=https://notifications.automatisch.io REACT_APP_NOTIFICATIONS_URL=https://notifications.automatisch.io
" >> .env " >> .env
cd $CURRENT_DIR cd $CURRENT_DIR

View File

@@ -21,10 +21,18 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
ports:
- '5432:5432'
expose:
- 5432
redis: redis:
image: 'redis:7.0.4-alpine' image: 'redis:7.0.4-alpine'
volumes: volumes:
- redis_data:/data - redis_data:/data
ports:
- '6379:6379'
expose:
- 6379
volumes: volumes:
postgres_data: postgres_data:

View File

@@ -1,31 +1,11 @@
import { DateTime } from 'luxon';
import { TSubscription } from '@automatisch/types';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Billing from '../../helpers/billing/index.ee'; import Billing from '../../helpers/billing/index.ee';
import Execution from '../../models/execution'; import Execution from '../../models/execution';
import ExecutionStep from '../../models/execution-step'; import ExecutionStep from '../../models/execution-step';
import Subscription from '../../models/subscription.ee'; import Subscription from '../../models/subscription.ee';
import { DateTime } from 'luxon';
type BillingCardAction = {
type: string;
text: string;
src?: string | null;
};
type ComputedSubscription = {
status: string;
monthlyQuota: {
title: string;
action: BillingCardAction;
};
nextBillDate: {
title: string;
action: BillingCardAction;
};
nextBillAmount: {
title: string;
action: BillingCardAction;
};
};
const getBillingAndUsage = async ( const getBillingAndUsage = async (
_parent: unknown, _parent: unknown,
@@ -36,7 +16,7 @@ const getBillingAndUsage = async (
'subscription' 'subscription'
); );
const subscription: ComputedSubscription = persistedSubscription const subscription = persistedSubscription
? paidSubscription(persistedSubscription) ? paidSubscription(persistedSubscription)
: freeTrialSubscription(); : freeTrialSubscription();
@@ -48,7 +28,7 @@ const getBillingAndUsage = async (
}; };
}; };
const paidSubscription = (subscription: Subscription): ComputedSubscription => { const paidSubscription = (subscription: Subscription): TSubscription => {
const currentPlan = Billing.paddlePlans.find( const currentPlan = Billing.paddlePlans.find(
(plan) => plan.productId === subscription.paddlePlanId (plan) => plan.productId === subscription.paddlePlanId
); );
@@ -81,7 +61,7 @@ const paidSubscription = (subscription: Subscription): ComputedSubscription => {
}; };
}; };
const freeTrialSubscription = (): ComputedSubscription => { const freeTrialSubscription = (): TSubscription => {
return { return {
status: null, status: null,
monthlyQuota: { monthlyQuota: {

View File

@@ -339,6 +339,35 @@ export type TPaymentPlan = {
productId: string; productId: string;
} }
export type TSubscription = {
status: string;
monthlyQuota: {
title: string;
action: BillingCardAction;
};
nextBillDate: {
title: string;
action: BillingCardAction;
};
nextBillAmount: {
title: string;
action: BillingCardAction;
};
}
type TBillingCardAction = TBillingTextCardAction | TBillingLinkCardAction;
type TBillingTextCardAction = {
type: 'text';
text: string;
}
type TBillingLinkCardAction = {
type: 'link';
text: string;
src: string;
}
declare module 'axios' { declare module 'axios' {
interface AxiosResponse { interface AxiosResponse {
httpError?: IJSONObject; httpError?: IJSONObject;

View File

@@ -12,7 +12,6 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import AccountDropdownMenu from 'components/AccountDropdownMenu'; import AccountDropdownMenu from 'components/AccountDropdownMenu';
import UsageAlert from 'components/UsageAlert/index.ee';
import Container from 'components/Container'; import Container from 'components/Container';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from './style'; import { Link } from './style';
@@ -82,8 +81,6 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
</Toolbar> </Toolbar>
</Container> </Container>
<UsageAlert />
<AccountDropdownMenu <AccountDropdownMenu
anchorEl={accountMenuAnchorElement} anchorEl={accountMenuAnchorElement}
id={accountMenuId} id={accountMenuId}

View File

@@ -78,7 +78,7 @@ function LoginForm() {
sx={{ mb: 1 }} sx={{ mb: 1 }}
/> />
{isCloud &&<Link {isCloud && <Link
component={RouterLink} component={RouterLink}
to={URLS.FORGOT_PASSWORD} to={URLS.FORGOT_PASSWORD}
underline="none" underline="none"

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions'; import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
@@ -9,23 +10,113 @@ import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { TBillingCardAction } from '@automatisch/types';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useUsageData from 'hooks/useUsageData.ee'; import useBillingAndUsageData from 'hooks/useBillingAndUsageData.ee';
import useFormatMessage from 'hooks/useFormatMessage';
const capitalize = (str: string) => str[0].toUpperCase() + str.slice(1, str.length);
type BillingCardProps = {
name: string;
title?: string;
action?: TBillingCardAction;
};
function BillingCard(props: BillingCardProps) {
const { name, title = '', action } = props;
return (
<Card
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: (theme) => theme.palette.background.default,
}}
>
<CardContent>
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
{name}
</Typography>
<Typography variant="h6" fontWeight="bold">
{title}
</Typography>
</CardContent>
<CardActions>
<Action action={action} />
</CardActions>
</Card>
);
}
function Action(props: { action?: TBillingCardAction }) {
const { action } = props;
if (!action) return <React.Fragment />;
const { text, type } = action;
if (type === 'link') {
if (action.src.startsWith('http')) {
return (
<Button
size="small"
href={action.src}
target="_blank"
>
{text}
</Button>
)
} else {
return (
<Button
size="small"
component={Link}
to={action.src}
>
{text}
</Button>
);
}
}
if (type === 'text') {
return (
<Typography variant="subtitle2" pb={1}>
{text}
</Typography>
);
}
return <React.Fragment />;
}
export default function UsageDataInformation() { export default function UsageDataInformation() {
const usageData = useUsageData(); const formatMessage = useFormatMessage();
const billingAndUsageData = useBillingAndUsageData();
return ( return (
<React.Fragment> <React.Fragment>
<Card sx={{ mb: 3, p: 2 }}> <Card sx={{ mb: 3, p: 2 }}>
<CardContent> <CardContent sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
Subscription plan {formatMessage('usageDataInformation.subscriptionPlan')}
</Typography> </Typography>
{/* <Chip label="Active" color="success" /> */}
{billingAndUsageData?.subscription?.status && (
<Chip
label={capitalize(billingAndUsageData?.subscription?.status)}
color="success"
/>
)}
</Box> </Box>
<Divider sx={{ mb: 3 }} /> <Divider sx={{ mb: 3 }} />
<Grid <Grid
container container
item item
@@ -35,76 +126,33 @@ export default function UsageDataInformation() {
alignItems="stretch" alignItems="stretch"
> >
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<Card <BillingCard
sx={{ name={formatMessage('usageDataInformation.monthlyQuota')}
height: '100%', title={billingAndUsageData?.subscription?.monthlyQuota.title}
backgroundColor: (theme) => theme.palette.background.default, action={billingAndUsageData?.subscription?.monthlyQuota.action}
}} />
>
<CardContent>
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
Monthly quota
</Typography>
<Typography variant="h6" fontWeight="bold">
Free trial
</Typography>
</CardContent>
<CardActions>
<Button size="small">Upgrade plan</Button>
</CardActions>
</Card>
</Grid> </Grid>
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<Card <BillingCard
sx={{ name={formatMessage('usageDataInformation.nextBillAmount')}
height: '100%', title={billingAndUsageData?.subscription?.nextBillAmount.title}
backgroundColor: (theme) => theme.palette.background.default, action={billingAndUsageData?.subscription?.nextBillAmount.action}
}} />
>
<CardContent>
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
Next bill amount
</Typography>
<Typography variant="h6" fontWeight="bold">
---
</Typography>
</CardContent>
<CardActions>
{/* <Button size="small">Update billing info</Button> */}
</CardActions>
</Card>
</Grid> </Grid>
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<Card <BillingCard
sx={{ name={formatMessage('usageDataInformation.nextBillDate')}
height: '100%', title={billingAndUsageData?.subscription?.nextBillDate.title}
backgroundColor: (theme) => theme.palette.background.default, action={billingAndUsageData?.subscription?.nextBillDate.action}
}} />
>
<CardContent>
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
Next bill date
</Typography>
<Typography variant="h6" fontWeight="bold">
---
</Typography>
</CardContent>
<CardActions>
{/* <Button disabled size="small">
monthly billing
</Button> */}
</CardActions>
</Card>
</Grid> </Grid>
</Grid> </Grid>
<Box> <Box>
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
Your usage {formatMessage('usageDataInformation.yourUsage')}
</Typography> </Typography>
<Box> <Box>
@@ -112,7 +160,7 @@ export default function UsageDataInformation() {
variant="subtitle2" variant="subtitle2"
sx={{ color: 'text.secondary', mt: 1 }} sx={{ color: 'text.secondary', mt: 1 }}
> >
Last 30 days total usage {formatMessage('usageDataInformation.yourUsageDescription')}
</Typography> </Typography>
</Box> </Box>
@@ -129,14 +177,14 @@ export default function UsageDataInformation() {
variant="subtitle2" variant="subtitle2"
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }} sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
> >
Tasks {formatMessage('usageDataInformation.yourUsageTasks')}
</Typography> </Typography>
<Typography <Typography
variant="subtitle2" variant="subtitle2"
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }} sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
> >
12300 {billingAndUsageData?.usage.task}
</Typography> </Typography>
</Box> </Box>
@@ -148,9 +196,9 @@ export default function UsageDataInformation() {
to={URLS.SETTINGS_PLAN_UPGRADE} to={URLS.SETTINGS_PLAN_UPGRADE}
size="small" size="small"
variant="contained" variant="contained"
sx={{ mt: 2 }} sx={{ mt: 2, alignSelf: 'flex-end' }}
> >
Upgrade {formatMessage('usageDataInformation.upgrade')}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -0,0 +1,39 @@
import { gql } from '@apollo/client';
export const GET_BILLING_AND_USAGE = gql`
query GetBillingAndUsage {
getBillingAndUsage {
subscription {
status
monthlyQuota {
title
action {
type
text
src
}
}
nextBillDate {
title
action {
type
text
src
}
}
nextBillAmount {
title
action {
type
text
src
}
}
}
usage {
task
}
}
}
`;

View File

@@ -0,0 +1,37 @@
import { useQuery } from '@apollo/client';
import { DateTime } from 'luxon';
import { TSubscription } from '@automatisch/types';
import { GET_BILLING_AND_USAGE } from 'graphql/queries/get-billing-and-usage.ee';
function transform(billingAndUsageData: NonNullable<UseBillingAndUsageDataReturn>) {
const nextBillDate = billingAndUsageData.subscription.nextBillDate;
const nextBillDateTitle = nextBillDate.title;
const relativeNextBillDateTitle = nextBillDateTitle ? DateTime.fromMillis(Number(nextBillDateTitle)).toFormat('LLL dd, yyyy') as string : '';
return {
...billingAndUsageData,
subscription: {
...billingAndUsageData.subscription,
nextBillDate: {
...billingAndUsageData.subscription.nextBillDate,
title: relativeNextBillDateTitle,
}
}
};
}
type UseBillingAndUsageDataReturn = {
subscription: TSubscription,
usage: {
task: number;
}
} | null;
export default function useBillingAndUsageData(): UseBillingAndUsageDataReturn {
const { data, loading } = useQuery(GET_BILLING_AND_USAGE);
if (loading) return null;
return transform(data.getBillingAndUsage);
}

View File

@@ -140,5 +140,13 @@
"usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})", "usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})",
"usageAlert.viewPlans": "View plans", "usageAlert.viewPlans": "View plans",
"jsonViewer.noDataFound": "We couldn't find anything matching your search", "jsonViewer.noDataFound": "We couldn't find anything matching your search",
"planUpgrade.title": "Upgrade your plan" "planUpgrade.title": "Upgrade your plan",
} "usageDataInformation.subscriptionPlan": "Subscription plan",
"usageDataInformation.monthlyQuota": "Monthly quota",
"usageDataInformation.nextBillAmount": "Next bill amount",
"usageDataInformation.nextBillDate": "Next bill date",
"usageDataInformation.yourUsage": "Your usage",
"usageDataInformation.yourUsageDescription": "Last 30 days total usage",
"usageDataInformation.yourUsageTasks": "Tasks",
"usageDataInformation.upgrade": "Upgrade"
}

View File

@@ -4,7 +4,6 @@ import Grid from '@mui/material/Grid';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import UsageDataInformation from 'components/UsageDataInformation/index.ee'; import UsageDataInformation from 'components/UsageDataInformation/index.ee';
import UpgradeFreeTrial from 'components/UpgradeFreeTrial/index.ee';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import Container from 'components/Container'; import Container from 'components/Container';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';