Merge pull request #1003 from automatisch/billing-page
feat: redesign billing page
This commit is contained in:
@@ -39,6 +39,8 @@ type AppConfig = {
|
|||||||
smtpPassword: string;
|
smtpPassword: string;
|
||||||
fromEmail: string;
|
fromEmail: string;
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
|
paddleVendorId: string;
|
||||||
|
paddleVendorAuthCode: string;
|
||||||
stripeSecretKey: string;
|
stripeSecretKey: string;
|
||||||
stripeSigningSecret: string;
|
stripeSigningSecret: string;
|
||||||
stripeStarterPriceKey: string;
|
stripeStarterPriceKey: string;
|
||||||
@@ -111,6 +113,8 @@ const appConfig: AppConfig = {
|
|||||||
smtpPassword: process.env.SMTP_PASSWORD,
|
smtpPassword: process.env.SMTP_PASSWORD,
|
||||||
fromEmail: process.env.FROM_EMAIL,
|
fromEmail: process.env.FROM_EMAIL,
|
||||||
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
|
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
|
||||||
|
paddleVendorId: process.env.PADDLE_VENDOR_ID,
|
||||||
|
paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE,
|
||||||
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||||||
stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET,
|
stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET,
|
||||||
stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY,
|
stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY,
|
||||||
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return knex.schema.table('users', (table) => {
|
||||||
|
table.date('trial_expiry_date');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return knex.schema.table('users', (table) => {
|
||||||
|
table.dropColumn('trial_expiry_date');
|
||||||
|
});
|
||||||
|
}
|
10
packages/backend/src/graphql/queries/get-payment-plans.ee.ts
Normal file
10
packages/backend/src/graphql/queries/get-payment-plans.ee.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import appConfig from '../../config/app';
|
||||||
|
import Billing from '../../helpers/billing/index.ee';
|
||||||
|
|
||||||
|
const getPaymentPlans = async () => {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return Billing.paddlePlans;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPaymentPlans;
|
@@ -12,6 +12,7 @@ import getDynamicData from './queries/get-dynamic-data';
|
|||||||
import getDynamicFields from './queries/get-dynamic-fields';
|
import getDynamicFields from './queries/get-dynamic-fields';
|
||||||
import getCurrentUser from './queries/get-current-user';
|
import getCurrentUser from './queries/get-current-user';
|
||||||
import getUsageData from './queries/get-usage-data.ee';
|
import getUsageData from './queries/get-usage-data.ee';
|
||||||
|
import getPaymentPlans from './queries/get-payment-plans.ee';
|
||||||
import getPaymentPortalUrl from './queries/get-payment-portal-url.ee';
|
import getPaymentPortalUrl from './queries/get-payment-portal-url.ee';
|
||||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||||
import healthcheck from './queries/healthcheck';
|
import healthcheck from './queries/healthcheck';
|
||||||
@@ -31,6 +32,7 @@ const queryResolvers = {
|
|||||||
getDynamicFields,
|
getDynamicFields,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getUsageData,
|
getUsageData,
|
||||||
|
getPaymentPlans,
|
||||||
getPaymentPortalUrl,
|
getPaymentPortalUrl,
|
||||||
getAutomatischInfo,
|
getAutomatischInfo,
|
||||||
healthcheck,
|
healthcheck,
|
||||||
|
@@ -36,6 +36,7 @@ type Query {
|
|||||||
getCurrentUser: User
|
getCurrentUser: User
|
||||||
getUsageData: GetUsageData
|
getUsageData: GetUsageData
|
||||||
getPaymentPortalUrl: GetPaymentPortalUrl
|
getPaymentPortalUrl: GetPaymentPortalUrl
|
||||||
|
getPaymentPlans: [PaymentPlan]
|
||||||
getAutomatischInfo: GetAutomatischInfo
|
getAutomatischInfo: GetAutomatischInfo
|
||||||
healthcheck: AppHealth
|
healthcheck: AppHealth
|
||||||
}
|
}
|
||||||
@@ -481,6 +482,13 @@ type GetPaymentPortalUrl {
|
|||||||
url: String
|
url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaymentPlan {
|
||||||
|
name: String
|
||||||
|
limit: String
|
||||||
|
price: String
|
||||||
|
productId: String
|
||||||
|
}
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
mutation: Mutation
|
mutation: Mutation
|
||||||
|
@@ -4,6 +4,7 @@ import PaymentPlan from '../../models/payment-plan.ee';
|
|||||||
import UsageData from '../../models/usage-data.ee';
|
import UsageData from '../../models/usage-data.ee';
|
||||||
import appConfig from '../../config/app';
|
import appConfig from '../../config/app';
|
||||||
import handleWebhooks from './webhooks.ee';
|
import handleWebhooks from './webhooks.ee';
|
||||||
|
import paddlePlans from './plans.ee';
|
||||||
|
|
||||||
const plans = [
|
const plans = [
|
||||||
{
|
{
|
||||||
@@ -95,6 +96,7 @@ const billing = {
|
|||||||
handleWebhooks,
|
handleWebhooks,
|
||||||
stripe,
|
stripe,
|
||||||
plans,
|
plans,
|
||||||
|
paddlePlans,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default billing;
|
export default billing;
|
||||||
|
16
packages/backend/src/helpers/billing/plans.ee.ts
Normal file
16
packages/backend/src/helpers/billing/plans.ee.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
name: '10k - monthly',
|
||||||
|
limit: '10,000',
|
||||||
|
price: '€20',
|
||||||
|
productId: '47384',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '30k - monthly',
|
||||||
|
limit: '30,000',
|
||||||
|
price: '€50',
|
||||||
|
productId: '47419',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default plans;
|
@@ -1,4 +1,6 @@
|
|||||||
import { QueryContext, ModelOptions } from 'objection';
|
import { QueryContext, ModelOptions } from 'objection';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import appConfig from '../config/app';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import Connection from './connection';
|
import Connection from './connection';
|
||||||
import Flow from './flow';
|
import Flow from './flow';
|
||||||
@@ -17,6 +19,7 @@ class User extends Base {
|
|||||||
role: string;
|
role: string;
|
||||||
resetPasswordToken: string;
|
resetPasswordToken: string;
|
||||||
resetPasswordTokenSentAt: string;
|
resetPasswordTokenSentAt: string;
|
||||||
|
trialExpiryDate: string;
|
||||||
connections?: Connection[];
|
connections?: Connection[];
|
||||||
flows?: Flow[];
|
flows?: Flow[];
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
@@ -133,9 +136,17 @@ class User extends Base {
|
|||||||
this.password = await bcrypt.hash(this.password, 10);
|
this.password = await bcrypt.hash(this.password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startTrialPeriod() {
|
||||||
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toFormat('D');
|
||||||
|
}
|
||||||
|
|
||||||
async $beforeInsert(queryContext: QueryContext) {
|
async $beforeInsert(queryContext: QueryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
await this.generateHash();
|
await this.generateHash();
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
await this.startTrialPeriod();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
|
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
|
||||||
|
8
packages/types/index.d.ts
vendored
8
packages/types/index.d.ts
vendored
@@ -321,6 +321,13 @@ export type IGlobalVariable = {
|
|||||||
setActionItem?: (actionItem: IActionItem) => void;
|
setActionItem?: (actionItem: IActionItem) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TPaymentPlan = {
|
||||||
|
price: string;
|
||||||
|
name: string;
|
||||||
|
limit: string;
|
||||||
|
productId: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'axios' {
|
declare module 'axios' {
|
||||||
interface AxiosResponse {
|
interface AxiosResponse {
|
||||||
httpError?: IJSONObject;
|
httpError?: IJSONObject;
|
||||||
@@ -335,4 +342,3 @@ export interface IRequest extends Request {
|
|||||||
rawBody?: Buffer;
|
rawBody?: Buffer;
|
||||||
currentUser?: IUser;
|
currentUser?: IUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,28 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
|
|
||||||
import PageTitle from 'components/PageTitle';
|
|
||||||
import { generateExternalLink } from 'helpers/translation-values';
|
|
||||||
import usePaymentPortalUrl from 'hooks/usePaymentPortalUrl.ee';
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
|
||||||
|
|
||||||
export default function PaymentInformation() {
|
|
||||||
const paymentPortal = usePaymentPortalUrl();
|
|
||||||
const formatMessage = useFormatMessage();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<PageTitle
|
|
||||||
gutterBottom
|
|
||||||
>
|
|
||||||
{formatMessage('billingAndUsageSettings.paymentInformation')}
|
|
||||||
</PageTitle>
|
|
||||||
|
|
||||||
<Typography>
|
|
||||||
{formatMessage(
|
|
||||||
'billingAndUsageSettings.paymentPortalInformation',
|
|
||||||
{ link: generateExternalLink(paymentPortal.url) })}
|
|
||||||
</Typography>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
152
packages/web/src/components/UpgradeFreeTrial/index.ee.tsx
Normal file
152
packages/web/src/components/UpgradeFreeTrial/index.ee.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
|
||||||
|
import usePaymentPlans from 'hooks/usePaymentPlans.ee';
|
||||||
|
|
||||||
|
export default function UpgradeFreeTrial() {
|
||||||
|
const { plans, loading } = usePaymentPlans();
|
||||||
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
||||||
|
const selectedPlan = plans?.[selectedIndex];
|
||||||
|
|
||||||
|
const updateSelection = (index: number) => setSelectedIndex(index);
|
||||||
|
|
||||||
|
if (loading || !plans.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Card sx={{ mb: 3, p: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
Upgrade your free trial
|
||||||
|
</Typography>
|
||||||
|
{/* <Chip label="Active" color="success" /> */}
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
spacing={1}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
alignItems="stretch"
|
||||||
|
>
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table aria-label="simple table">
|
||||||
|
<TableHead
|
||||||
|
sx={{
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{ pt: 0, pb: 2 }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
Monthly Tasks
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ py: 0, pb: 2 }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
Price
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{plans.map((plan, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={plan.productId}
|
||||||
|
onClick={() => updateSelection(index)}
|
||||||
|
sx={{
|
||||||
|
'&:hover': { cursor: 'pointer' },
|
||||||
|
backgroundColor: selectedIndex === index ? '#f1f3fa' : 'white',
|
||||||
|
border: selectedIndex === index ? '2px solid #0059f7' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell component="th" scope="row" sx={{ py: 2 }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: selectedIndex === index ? 'bold' : 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{plan.limit}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ py: 2 }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: selectedIndex === index ? 'bold' : 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{plan.price} / month
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Due today:
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedPlan.price}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="subtitle2" sx={{ fontSize: '12px', mt: 0 }}>
|
||||||
|
+ VAT if applicable
|
||||||
|
</Typography>
|
||||||
|
<Button size="small" variant="contained" sx={{ mt: 2 }}>
|
||||||
|
<LockIcon fontSize="small" sx={{ mr: 1 }} />
|
||||||
|
Pay securely via Paddle
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,13 +1,15 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { DateTime } from 'luxon';
|
import { Link } from 'react-router-dom';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
import Paper from '@mui/material/Paper';
|
import Button from '@mui/material/Button';
|
||||||
import Table from '@mui/material/Table';
|
import Card from '@mui/material/Card';
|
||||||
import TableBody from '@mui/material/TableBody';
|
import CardActions from '@mui/material/CardActions';
|
||||||
import TableCell from '@mui/material/TableCell';
|
import CardContent from '@mui/material/CardContent';
|
||||||
import TableContainer from '@mui/material/TableContainer';
|
import Divider from '@mui/material/Divider';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
import useUsageData from 'hooks/useUsageData.ee';
|
import useUsageData from 'hooks/useUsageData.ee';
|
||||||
|
|
||||||
export default function UsageDataInformation() {
|
export default function UsageDataInformation() {
|
||||||
@@ -15,43 +17,143 @@ export default function UsageDataInformation() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TableContainer component={Paper}>
|
<Card sx={{ mb: 3, p: 2 }}>
|
||||||
<Table>
|
<CardContent>
|
||||||
<TableBody>
|
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<TableRow>
|
<Typography variant="h6" fontWeight="bold">
|
||||||
<TableCell component="td" scope="row">
|
Subscription plan
|
||||||
Current plan
|
</Typography>
|
||||||
</TableCell>
|
{/* <Chip label="Active" color="success" /> */}
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
spacing={1}
|
||||||
|
sx={{ mb: [2, 2, 8] }}
|
||||||
|
alignItems="stretch"
|
||||||
|
>
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: (theme) => theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.name}</TableCell>
|
<Grid item xs={12} md={4}>
|
||||||
</TableRow>
|
<Card
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: (theme) => theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
|
||||||
|
Next bill amount
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<TableRow>
|
<Typography variant="h6" fontWeight="bold">
|
||||||
<TableCell component="td" scope="row">
|
---
|
||||||
Total allowed task count
|
</Typography>
|
||||||
</TableCell>
|
</CardContent>
|
||||||
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.allowedTaskCount}</TableCell>
|
<CardActions>
|
||||||
</TableRow>
|
{/* <Button size="small">Update billing info</Button> */}
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<TableRow>
|
<Grid item xs={12} md={4}>
|
||||||
<TableCell component="td" scope="row">
|
<Card
|
||||||
Consumed task count
|
sx={{
|
||||||
</TableCell>
|
height: '100%',
|
||||||
|
backgroundColor: (theme) => theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
|
||||||
|
Next bill date
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
---
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.consumedTaskCount}</TableCell>
|
<CardActions>
|
||||||
</TableRow>
|
{/* <Button disabled size="small">
|
||||||
|
monthly billing
|
||||||
|
</Button> */}
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
Your usage
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<TableRow sx={{ 'td': { border: 0 } }}>
|
<Box>
|
||||||
<TableCell component="td" scope="row">
|
<Typography
|
||||||
Next billing date
|
variant="subtitle2"
|
||||||
</TableCell>
|
sx={{ color: 'text.secondary', mt: 1 }}
|
||||||
|
>
|
||||||
|
Last 30 days total usage
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.nextResetAt?.toLocaleString(DateTime.DATE_FULL)}</TableCell>
|
<Divider sx={{ mt: 2 }} />
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
<Box
|
||||||
</Table>
|
sx={{
|
||||||
</TableContainer>
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
Tasks
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
12300
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mt: 2 }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={URLS.SETTINGS_PLAN_UPGRADE}
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -65,9 +65,11 @@ export const SETTINGS = '/settings';
|
|||||||
export const SETTINGS_DASHBOARD = SETTINGS;
|
export const SETTINGS_DASHBOARD = SETTINGS;
|
||||||
export const PROFILE = 'profile';
|
export const PROFILE = 'profile';
|
||||||
export const BILLING_AND_USAGE = 'billing';
|
export const BILLING_AND_USAGE = 'billing';
|
||||||
|
export const PLAN_UPGRADE = 'upgrade';
|
||||||
export const UPDATES = '/updates';
|
export const UPDATES = '/updates';
|
||||||
export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`;
|
export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`;
|
||||||
export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`;
|
export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`;
|
||||||
|
export const SETTINGS_PLAN_UPGRADE = `${SETTINGS_BILLING_AND_USAGE}/${PLAN_UPGRADE}`;
|
||||||
|
|
||||||
export const DASHBOARD = FLOWS;
|
export const DASHBOARD = FLOWS;
|
||||||
|
|
||||||
|
12
packages/web/src/graphql/queries/get-payment-plans.ee.ts
Normal file
12
packages/web/src/graphql/queries/get-payment-plans.ee.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_PAYMENT_PLANS = gql`
|
||||||
|
query GetPaymentPlans {
|
||||||
|
getPaymentPlans {
|
||||||
|
name
|
||||||
|
limit
|
||||||
|
price
|
||||||
|
productId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
18
packages/web/src/hooks/usePaymentPlans.ee.ts
Normal file
18
packages/web/src/hooks/usePaymentPlans.ee.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
|
||||||
|
import { TPaymentPlan } from '@automatisch/types';
|
||||||
|
import { GET_PAYMENT_PLANS } from 'graphql/queries/get-payment-plans.ee';
|
||||||
|
|
||||||
|
type UsePaymentPlansReturn = {
|
||||||
|
plans: TPaymentPlan[];
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function usePaymentPlans(): UsePaymentPlansReturn {
|
||||||
|
const { data, loading } = useQuery(GET_PAYMENT_PLANS);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plans: data?.getPaymentPlans || [],
|
||||||
|
loading
|
||||||
|
};
|
||||||
|
}
|
@@ -139,5 +139,6 @@
|
|||||||
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.",
|
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,9 @@ import * as React from 'react';
|
|||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
|
|
||||||
import * as URLS from 'config/urls'
|
import * as URLS from 'config/urls';
|
||||||
import PaymentInformation from 'components/PaymentInformation/index.ee';
|
|
||||||
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';
|
||||||
@@ -16,27 +16,25 @@ function BillingAndUsageSettings() {
|
|||||||
|
|
||||||
// redirect to the initial settings page
|
// redirect to the initial settings page
|
||||||
if (isCloud === false) {
|
if (isCloud === false) {
|
||||||
return (<Navigate to={URLS.SETTINGS} replace={true} />)
|
return <Navigate to={URLS.SETTINGS} replace={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// render nothing until we know if it's cloud or not
|
// render nothing until we know if it's cloud or not
|
||||||
// here, `isCloud` is not `false`, but `undefined`
|
// here, `isCloud` is not `false`, but `undefined`
|
||||||
if (!isCloud) return <React.Fragment />
|
if (!isCloud) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
<Grid container item xs={12} sm={9} md={8}>
|
||||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
<PageTitle>{formatMessage('billingAndUsageSettings.title')}</PageTitle>
|
<PageTitle>
|
||||||
|
{formatMessage('billingAndUsageSettings.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sx={{ mb: 6 }}>
|
<Grid item xs={12} sx={{ mb: 6 }}>
|
||||||
<UsageDataInformation />
|
<UsageDataInformation />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<PaymentInformation />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
42
packages/web/src/pages/PlanUpgrade/index.ee.tsx
Normal file
42
packages/web/src/pages/PlanUpgrade/index.ee.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import UpgradeFreeTrial from 'components/UpgradeFreeTrial/index.ee';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useCloud from 'hooks/useCloud';
|
||||||
|
|
||||||
|
function PlanUpgrade() {
|
||||||
|
const isCloud = useCloud();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
// redirect to the initial settings page
|
||||||
|
if (isCloud === false) {
|
||||||
|
return <Navigate to={URLS.SETTINGS} replace={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// render nothing until we know if it's cloud or not
|
||||||
|
// here, `isCloud` is not `false`, but `undefined`
|
||||||
|
if (!isCloud) return <React.Fragment />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Grid container item xs={12} sm={9} md={8}>
|
||||||
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
|
<PageTitle>
|
||||||
|
{formatMessage('planUpgrade.title')}
|
||||||
|
</PageTitle>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sx={{ mb: 6 }}>
|
||||||
|
<UpgradeFreeTrial />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlanUpgrade;
|
@@ -2,6 +2,7 @@ import { Route, Navigate } from 'react-router-dom';
|
|||||||
import SettingsLayout from 'components/SettingsLayout';
|
import SettingsLayout from 'components/SettingsLayout';
|
||||||
import ProfileSettings from 'pages/ProfileSettings';
|
import ProfileSettings from 'pages/ProfileSettings';
|
||||||
import BillingAndUsageSettings from 'pages/BillingAndUsageSettings/index.ee';
|
import BillingAndUsageSettings from 'pages/BillingAndUsageSettings/index.ee';
|
||||||
|
import PlanUpgrade from 'pages/PlanUpgrade/index.ee';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
|
||||||
@@ -25,6 +26,15 @@ export default (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={URLS.SETTINGS_PLAN_UPGRADE}
|
||||||
|
element={
|
||||||
|
<SettingsLayout>
|
||||||
|
<PlanUpgrade />
|
||||||
|
</SettingsLayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={URLS.SETTINGS}
|
path={URLS.SETTINGS}
|
||||||
element={<Navigate to={URLS.SETTINGS_PROFILE} replace />}
|
element={<Navigate to={URLS.SETTINGS_PROFILE} replace />}
|
||||||
|
Reference in New Issue
Block a user