diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index fc8ad0b4..3364b834 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -39,6 +39,8 @@ type AppConfig = { smtpPassword: string; fromEmail: string; isCloud: boolean; + paddleVendorId: string; + paddleVendorAuthCode: string; stripeSecretKey: string; stripeSigningSecret: string; stripeStarterPriceKey: string; @@ -111,6 +113,8 @@ const appConfig: AppConfig = { smtpPassword: process.env.SMTP_PASSWORD, fromEmail: process.env.FROM_EMAIL, 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, stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET, stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY, diff --git a/packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.ts b/packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.ts new file mode 100644 index 00000000..e3fa8799 --- /dev/null +++ b/packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex'; +import appConfig from '../../config/app'; + +export async function up(knex: Knex): Promise { + if (!appConfig.isCloud) return; + + return knex.schema.table('users', (table) => { + table.date('trial_expiry_date'); + }); +} + +export async function down(knex: Knex): Promise { + if (!appConfig.isCloud) return; + + return knex.schema.table('users', (table) => { + table.dropColumn('trial_expiry_date'); + }); +} diff --git a/packages/backend/src/graphql/queries/get-payment-plans.ee.ts b/packages/backend/src/graphql/queries/get-payment-plans.ee.ts new file mode 100644 index 00000000..47fa7d91 --- /dev/null +++ b/packages/backend/src/graphql/queries/get-payment-plans.ee.ts @@ -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; diff --git a/packages/backend/src/graphql/query-resolvers.ts b/packages/backend/src/graphql/query-resolvers.ts index 659839aa..267b3356 100644 --- a/packages/backend/src/graphql/query-resolvers.ts +++ b/packages/backend/src/graphql/query-resolvers.ts @@ -12,6 +12,7 @@ import getDynamicData from './queries/get-dynamic-data'; import getDynamicFields from './queries/get-dynamic-fields'; import getCurrentUser from './queries/get-current-user'; 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 getAutomatischInfo from './queries/get-automatisch-info'; import healthcheck from './queries/healthcheck'; @@ -31,6 +32,7 @@ const queryResolvers = { getDynamicFields, getCurrentUser, getUsageData, + getPaymentPlans, getPaymentPortalUrl, getAutomatischInfo, healthcheck, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index c15ccd36..63817641 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -36,6 +36,7 @@ type Query { getCurrentUser: User getUsageData: GetUsageData getPaymentPortalUrl: GetPaymentPortalUrl + getPaymentPlans: [PaymentPlan] getAutomatischInfo: GetAutomatischInfo healthcheck: AppHealth } @@ -481,6 +482,13 @@ type GetPaymentPortalUrl { url: String } +type PaymentPlan { + name: String + limit: String + price: String + productId: String +} + schema { query: Query mutation: Mutation diff --git a/packages/backend/src/helpers/billing/index.ee.ts b/packages/backend/src/helpers/billing/index.ee.ts index 3d11f58b..863e68e7 100644 --- a/packages/backend/src/helpers/billing/index.ee.ts +++ b/packages/backend/src/helpers/billing/index.ee.ts @@ -4,6 +4,7 @@ import PaymentPlan from '../../models/payment-plan.ee'; import UsageData from '../../models/usage-data.ee'; import appConfig from '../../config/app'; import handleWebhooks from './webhooks.ee'; +import paddlePlans from './plans.ee'; const plans = [ { @@ -95,6 +96,7 @@ const billing = { handleWebhooks, stripe, plans, + paddlePlans, }; export default billing; diff --git a/packages/backend/src/helpers/billing/plans.ee.ts b/packages/backend/src/helpers/billing/plans.ee.ts new file mode 100644 index 00000000..98bdcac9 --- /dev/null +++ b/packages/backend/src/helpers/billing/plans.ee.ts @@ -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; diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts index 57b07e89..bac17b40 100644 --- a/packages/backend/src/models/user.ts +++ b/packages/backend/src/models/user.ts @@ -1,4 +1,6 @@ import { QueryContext, ModelOptions } from 'objection'; +import { DateTime } from 'luxon'; +import appConfig from '../config/app'; import Base from './base'; import Connection from './connection'; import Flow from './flow'; @@ -17,6 +19,7 @@ class User extends Base { role: string; resetPasswordToken: string; resetPasswordTokenSentAt: string; + trialExpiryDate: string; connections?: Connection[]; flows?: Flow[]; steps?: Step[]; @@ -133,9 +136,17 @@ class User extends Base { this.password = await bcrypt.hash(this.password, 10); } + async startTrialPeriod() { + this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toFormat('D'); + } + async $beforeInsert(queryContext: QueryContext) { await super.$beforeInsert(queryContext); await this.generateHash(); + + if (appConfig.isCloud) { + await this.startTrialPeriod(); + } } async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) { diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 40ecea8e..6cd0dae2 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -321,6 +321,13 @@ export type IGlobalVariable = { setActionItem?: (actionItem: IActionItem) => void; }; +export type TPaymentPlan = { + price: string; + name: string; + limit: string; + productId: string; +} + declare module 'axios' { interface AxiosResponse { httpError?: IJSONObject; @@ -335,4 +342,3 @@ export interface IRequest extends Request { rawBody?: Buffer; currentUser?: IUser; } - diff --git a/packages/web/src/components/PaymentInformation/index.ee.tsx b/packages/web/src/components/PaymentInformation/index.ee.tsx deleted file mode 100644 index e0f8e22e..00000000 --- a/packages/web/src/components/PaymentInformation/index.ee.tsx +++ /dev/null @@ -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 ( - - - {formatMessage('billingAndUsageSettings.paymentInformation')} - - - - {formatMessage( - 'billingAndUsageSettings.paymentPortalInformation', - { link: generateExternalLink(paymentPortal.url) })} - - - ); -} diff --git a/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx new file mode 100644 index 00000000..4fca59a0 --- /dev/null +++ b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx @@ -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 ( + + + + + + Upgrade your free trial + + {/* */} + + + + + + + theme.palette.background.default, + }} + > + + + + Monthly Tasks + + + + + Price + + + + + + {plans.map((plan, index) => ( + updateSelection(index)} + sx={{ + '&:hover': { cursor: 'pointer' }, + backgroundColor: selectedIndex === index ? '#f1f3fa' : 'white', + border: selectedIndex === index ? '2px solid #0059f7' : 'none', + }} + > + + + {plan.limit} + + + + + {plan.price} / month + + + + ))} + +
+
+
+ + + + + Due today:  + + + {selectedPlan.price} + + + + + + VAT if applicable + + + +
+
+
+ ); +} diff --git a/packages/web/src/components/UsageDataInformation/index.ee.tsx b/packages/web/src/components/UsageDataInformation/index.ee.tsx index f655a692..296ef5b6 100644 --- a/packages/web/src/components/UsageDataInformation/index.ee.tsx +++ b/packages/web/src/components/UsageDataInformation/index.ee.tsx @@ -1,13 +1,15 @@ import * as React from 'react'; -import { DateTime } from 'luxon'; - -import Paper from '@mui/material/Paper'; -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 TableRow from '@mui/material/TableRow'; +import { Link } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +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 * as URLS from 'config/urls'; import useUsageData from 'hooks/useUsageData.ee'; export default function UsageDataInformation() { @@ -15,43 +17,143 @@ export default function UsageDataInformation() { return ( - - - - - - Current plan - + + + + + Subscription plan + + {/* */} + + + + + theme.palette.background.default, + }} + > + + + Monthly quota + + + Free trial + + + + + + + - {usageData.name} - + + theme.palette.background.default, + }} + > + + + Next bill amount + - - - Total allowed task count - + + --- + + - {usageData.allowedTaskCount} - + + {/* */} + + + - - - Consumed task count - + + theme.palette.background.default, + }} + > + + + Next bill date + + + --- + + - {usageData.consumedTaskCount} - + + {/* */} + + + + + + + Your usage + - - - Next billing date - + + + Last 30 days total usage + + - {usageData.nextResetAt?.toLocaleString(DateTime.DATE_FULL)} - - -
-
+ + + + + Tasks + + + + 12300 + + + + + + + + +
); } diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index 07f3c448..174bbc9a 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -65,9 +65,11 @@ export const SETTINGS = '/settings'; export const SETTINGS_DASHBOARD = SETTINGS; export const PROFILE = 'profile'; export const BILLING_AND_USAGE = 'billing'; +export const PLAN_UPGRADE = 'upgrade'; export const UPDATES = '/updates'; export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`; 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; diff --git a/packages/web/src/graphql/queries/get-payment-plans.ee.ts b/packages/web/src/graphql/queries/get-payment-plans.ee.ts new file mode 100644 index 00000000..1714b108 --- /dev/null +++ b/packages/web/src/graphql/queries/get-payment-plans.ee.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const GET_PAYMENT_PLANS = gql` + query GetPaymentPlans { + getPaymentPlans { + name + limit + price + productId + } + } +`; diff --git a/packages/web/src/hooks/usePaymentPlans.ee.ts b/packages/web/src/hooks/usePaymentPlans.ee.ts new file mode 100644 index 00000000..2a66cc39 --- /dev/null +++ b/packages/web/src/hooks/usePaymentPlans.ee.ts @@ -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 + }; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index bb127ad5..c860fb3e 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -139,5 +139,6 @@ "resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.", "usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})", "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" } diff --git a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx index af12fbdf..3b5383e4 100644 --- a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx +++ b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import { Navigate } from 'react-router-dom'; import Grid from '@mui/material/Grid'; -import * as URLS from 'config/urls' -import PaymentInformation from 'components/PaymentInformation/index.ee'; +import * as URLS from 'config/urls'; import UsageDataInformation from 'components/UsageDataInformation/index.ee'; +import UpgradeFreeTrial from 'components/UpgradeFreeTrial/index.ee'; import PageTitle from 'components/PageTitle'; import Container from 'components/Container'; import useFormatMessage from 'hooks/useFormatMessage'; @@ -16,27 +16,25 @@ function BillingAndUsageSettings() { // redirect to the initial settings page if (isCloud === false) { - return () + return ; } // render nothing until we know if it's cloud or not // here, `isCloud` is not `false`, but `undefined` - if (!isCloud) return + if (!isCloud) return ; return ( - + - {formatMessage('billingAndUsageSettings.title')} + + {formatMessage('billingAndUsageSettings.title')} + - - - - ); diff --git a/packages/web/src/pages/PlanUpgrade/index.ee.tsx b/packages/web/src/pages/PlanUpgrade/index.ee.tsx new file mode 100644 index 00000000..e7796e64 --- /dev/null +++ b/packages/web/src/pages/PlanUpgrade/index.ee.tsx @@ -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 ; + } + + // render nothing until we know if it's cloud or not + // here, `isCloud` is not `false`, but `undefined` + if (!isCloud) return ; + + return ( + + + + + {formatMessage('planUpgrade.title')} + + + + + + + + + ); +} + +export default PlanUpgrade; diff --git a/packages/web/src/settingsRoutes.tsx b/packages/web/src/settingsRoutes.tsx index 60147939..454bcb74 100644 --- a/packages/web/src/settingsRoutes.tsx +++ b/packages/web/src/settingsRoutes.tsx @@ -2,6 +2,7 @@ import { Route, Navigate } from 'react-router-dom'; import SettingsLayout from 'components/SettingsLayout'; import ProfileSettings from 'pages/ProfileSettings'; import BillingAndUsageSettings from 'pages/BillingAndUsageSettings/index.ee'; +import PlanUpgrade from 'pages/PlanUpgrade/index.ee'; import * as URLS from 'config/urls'; @@ -25,6 +26,15 @@ export default ( } /> + + + + } + /> + }