From 3e0149c058cd75807bdd9add96fef080e59a1e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Fri, 10 Mar 2023 16:27:32 +0300 Subject: [PATCH 01/18] feat: add searchable json viewer component --- .../src/components/ExecutionStep/index.tsx | 8 +- .../components/SearchableJSONViewer/index.tsx | 78 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 packages/web/src/components/SearchableJSONViewer/index.tsx diff --git a/packages/web/src/components/ExecutionStep/index.tsx b/packages/web/src/components/ExecutionStep/index.tsx index 106d03db..3c011fa2 100644 --- a/packages/web/src/components/ExecutionStep/index.tsx +++ b/packages/web/src/components/ExecutionStep/index.tsx @@ -10,7 +10,7 @@ import Box from '@mui/material/Box'; import type { IApp, IExecutionStep, IStep } from '@automatisch/types'; import TabPanel from 'components/TabPanel'; -import JSONViewer from 'components/JSONViewer'; +import SearchableJSONViewer from 'components/SearchableJSONViewer'; import AppIcon from 'components/AppIcon'; import { GET_APPS } from 'graphql/queries/get-apps'; import useFormatMessage from 'hooks/useFormatMessage'; @@ -92,16 +92,16 @@ export default function ExecutionStep( - + - + {hasError && ( - + )} diff --git a/packages/web/src/components/SearchableJSONViewer/index.tsx b/packages/web/src/components/SearchableJSONViewer/index.tsx new file mode 100644 index 00000000..acfcdd79 --- /dev/null +++ b/packages/web/src/components/SearchableJSONViewer/index.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import get from 'lodash/get'; +import set from 'lodash/set'; +import throttle from 'lodash/throttle'; +import { Box } from '@mui/material'; + +import { IJSONObject, IJSONValue } from '@automatisch/types'; +import JSONViewer from 'components/JSONViewer'; +import SearchInput from 'components/SearchInput'; + +type JSONViewerProps = { + data: IJSONObject; +}; + +type Entry = [string, IJSONValue]; + +const SearchableJSONViewer = ({ data }: JSONViewerProps) => { + const [filteredData, setFilteredData] = React.useState(data); + + const allEntries = React.useMemo(() => { + const entries: Entry[] = []; + const collectEntries = (obj: IJSONObject, prefix?: string) => { + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]); + + collectEntries( + obj[key] as IJSONObject, + [prefix, key].filter(Boolean).join('.') + ); + } else { + entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]); + } + } + }; + + collectEntries(data); + return entries; + }, [data]); + + const onSearchChange = React.useMemo( + () => + throttle((event: React.ChangeEvent) => { + const search = (event.target as HTMLInputElement).value.toLowerCase(); + const newFilteredData: IJSONObject = {}; + + if (!search) { + setFilteredData(data); + return; + } + + allEntries.forEach(([key, value]) => { + if ( + key.toLowerCase().includes(search) || + (typeof value !== 'object' && + value.toString().toLowerCase().includes(search)) + ) { + const value = get(filteredData, key); + set(newFilteredData, key, value); + } + }); + + setFilteredData(newFilteredData); + }, 400), + [allEntries] + ); + + return ( + <> + + + + + + ); +}; + +export default SearchableJSONViewer; From aebfcc38dd722643db575c6eb78bca64f63b6ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Fri, 10 Mar 2023 17:58:06 +0300 Subject: [PATCH 02/18] feat(SearchableJSONViewer): cover no result case --- .../components/SearchableJSONViewer/index.tsx | 23 ++++++++++++++----- packages/web/src/locales/en.json | 5 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/web/src/components/SearchableJSONViewer/index.tsx b/packages/web/src/components/SearchableJSONViewer/index.tsx index acfcdd79..c381c2bd 100644 --- a/packages/web/src/components/SearchableJSONViewer/index.tsx +++ b/packages/web/src/components/SearchableJSONViewer/index.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import get from 'lodash/get'; import set from 'lodash/set'; import throttle from 'lodash/throttle'; -import { Box } from '@mui/material'; +import isEmpty from 'lodash/isEmpty'; +import { Box, Typography } from '@mui/material'; import { IJSONObject, IJSONValue } from '@automatisch/types'; import JSONViewer from 'components/JSONViewer'; import SearchInput from 'components/SearchInput'; +import useFormatMessage from 'hooks/useFormatMessage'; type JSONViewerProps = { data: IJSONObject; @@ -15,7 +17,10 @@ type JSONViewerProps = { type Entry = [string, IJSONValue]; const SearchableJSONViewer = ({ data }: JSONViewerProps) => { - const [filteredData, setFilteredData] = React.useState(data); + const [filteredData, setFilteredData] = React.useState( + data + ); + const formatMessage = useFormatMessage(); const allEntries = React.useMemo(() => { const entries: Entry[] = []; @@ -23,7 +28,6 @@ const SearchableJSONViewer = ({ data }: JSONViewerProps) => { for (const key in obj) { if (typeof obj[key] === 'object' && obj[key] !== null) { entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]); - collectEntries( obj[key] as IJSONObject, [prefix, key].filter(Boolean).join('.') @@ -60,17 +64,24 @@ const SearchableJSONViewer = ({ data }: JSONViewerProps) => { } }); - setFilteredData(newFilteredData); + if (isEmpty(newFilteredData)) { + setFilteredData(null); + } else { + setFilteredData(newFilteredData); + } }, 400), [allEntries] ); return ( <> - + - + {filteredData && } + {!filteredData && ( + {formatMessage('jsonViewer.noDataFound')} + )} ); }; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index dee7e9cd..bb127ad5 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -138,5 +138,6 @@ "resetPasswordForm.confirmPasswordFieldLabel": "Confirm password", "resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.", "usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})", - "usageAlert.viewPlans": "View plans" -} \ No newline at end of file + "usageAlert.viewPlans": "View plans", + "jsonViewer.noDataFound": "We couldn't find anything matching your search" +} From f7753aa1b41bb00d02981c76acee7ae9f0b9fb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Thu, 16 Mar 2023 18:38:39 +0300 Subject: [PATCH 03/18] fix(SearchableJSONViewer): remove undefined values from filtered arrays --- .../web/src/components/SearchableJSONViewer/index.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/web/src/components/SearchableJSONViewer/index.tsx b/packages/web/src/components/SearchableJSONViewer/index.tsx index c381c2bd..fdfa9cba 100644 --- a/packages/web/src/components/SearchableJSONViewer/index.tsx +++ b/packages/web/src/components/SearchableJSONViewer/index.tsx @@ -3,6 +3,7 @@ import get from 'lodash/get'; import set from 'lodash/set'; import throttle from 'lodash/throttle'; import isEmpty from 'lodash/isEmpty'; +import toPath from 'lodash/toPath'; import { Box, Typography } from '@mui/material'; import { IJSONObject, IJSONValue } from '@automatisch/types'; @@ -61,6 +62,16 @@ const SearchableJSONViewer = ({ data }: JSONViewerProps) => { ) { const value = get(filteredData, key); set(newFilteredData, key, value); + const keyPath = toPath(key); + const parentKeyPath = keyPath.slice(0, keyPath.length - 1); + const parentKey = parentKeyPath.join('.'); + const parentValue = get(newFilteredData, parentKey); + if (Array.isArray(parentValue)) { + const filteredParentValue = parentValue.filter( + (item) => item !== undefined + ); + set(newFilteredData, parentKey, filteredParentValue); + } } }); From 0a5912eb8e71ecf8c6b0b83537708c2e9c0abffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Sun, 19 Mar 2023 13:23:22 +0300 Subject: [PATCH 04/18] refactor(SearchableJSONViewer): rewrite collecting keys and values of data --- .../components/SearchableJSONViewer/index.tsx | 98 ++++++++++--------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/web/src/components/SearchableJSONViewer/index.tsx b/packages/web/src/components/SearchableJSONViewer/index.tsx index fdfa9cba..13195849 100644 --- a/packages/web/src/components/SearchableJSONViewer/index.tsx +++ b/packages/web/src/components/SearchableJSONViewer/index.tsx @@ -3,10 +3,11 @@ import get from 'lodash/get'; import set from 'lodash/set'; import throttle from 'lodash/throttle'; import isEmpty from 'lodash/isEmpty'; -import toPath from 'lodash/toPath'; +import forIn from 'lodash/forIn'; +import isPlainObject from 'lodash/isPlainObject'; import { Box, Typography } from '@mui/material'; -import { IJSONObject, IJSONValue } from '@automatisch/types'; +import { IJSONObject } from '@automatisch/types'; import JSONViewer from 'components/JSONViewer'; import SearchInput from 'components/SearchInput'; import useFormatMessage from 'hooks/useFormatMessage'; @@ -15,7 +16,54 @@ type JSONViewerProps = { data: IJSONObject; }; -type Entry = [string, IJSONValue]; +function aggregate( + data: any, + searchTerm: string, + result = {}, + prefix: string[] = [], + withinArray = false +) { + if (withinArray) { + const containerValue = get(result, prefix, []); + + result = aggregate( + data, + searchTerm, + result, + prefix.concat(containerValue.length.toString()) + ); + + return result; + } + + if (isPlainObject(data)) { + forIn(data, (value, key) => { + const fullKey = [...prefix, key]; + + if (key.toLowerCase().includes(searchTerm)) { + set(result, fullKey, value); + return; + } + + result = aggregate(value, searchTerm, result, fullKey); + }); + } + + if (Array.isArray(data)) { + forIn(data, (value) => { + result = aggregate(value, searchTerm, result, prefix, true); + }); + } + + if ( + ['string', 'number'].includes(typeof data) && + String(data).toLowerCase().includes(searchTerm) + ) { + set(result, prefix, data); + } + + return result; +} const SearchableJSONViewer = ({ data }: JSONViewerProps) => { const [filteredData, setFilteredData] = React.useState( @@ -23,57 +71,17 @@ const SearchableJSONViewer = ({ data }: JSONViewerProps) => { ); const formatMessage = useFormatMessage(); - const allEntries = React.useMemo(() => { - const entries: Entry[] = []; - const collectEntries = (obj: IJSONObject, prefix?: string) => { - for (const key in obj) { - if (typeof obj[key] === 'object' && obj[key] !== null) { - entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]); - collectEntries( - obj[key] as IJSONObject, - [prefix, key].filter(Boolean).join('.') - ); - } else { - entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]); - } - } - }; - - collectEntries(data); - return entries; - }, [data]); - const onSearchChange = React.useMemo( () => throttle((event: React.ChangeEvent) => { const search = (event.target as HTMLInputElement).value.toLowerCase(); - const newFilteredData: IJSONObject = {}; if (!search) { setFilteredData(data); return; } - allEntries.forEach(([key, value]) => { - if ( - key.toLowerCase().includes(search) || - (typeof value !== 'object' && - value.toString().toLowerCase().includes(search)) - ) { - const value = get(filteredData, key); - set(newFilteredData, key, value); - const keyPath = toPath(key); - const parentKeyPath = keyPath.slice(0, keyPath.length - 1); - const parentKey = parentKeyPath.join('.'); - const parentValue = get(newFilteredData, parentKey); - if (Array.isArray(parentValue)) { - const filteredParentValue = parentValue.filter( - (item) => item !== undefined - ); - set(newFilteredData, parentKey, filteredParentValue); - } - } - }); + const newFilteredData = aggregate(data, search); if (isEmpty(newFilteredData)) { setFilteredData(null); @@ -81,7 +89,7 @@ const SearchableJSONViewer = ({ data }: JSONViewerProps) => { setFilteredData(newFilteredData); } }, 400), - [allEntries] + [data] ); return ( From 08918282a77c9e2e980fb161912ee5d8da516c83 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sat, 18 Mar 2023 21:21:06 +0300 Subject: [PATCH 05/18] feat: redesign billing page --- .../PaymentInformation/index.ee.tsx | 28 --- .../UsageDataInformation/index.ee.tsx | 159 ++++++++++++++---- .../BillingAndUsageSettings/index.ee.tsx | 17 +- 3 files changed, 129 insertions(+), 75 deletions(-) delete mode 100644 packages/web/src/components/PaymentInformation/index.ee.tsx 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/UsageDataInformation/index.ee.tsx b/packages/web/src/components/UsageDataInformation/index.ee.tsx index f655a692..96814ee4 100644 --- a/packages/web/src/components/UsageDataInformation/index.ee.tsx +++ b/packages/web/src/components/UsageDataInformation/index.ee.tsx @@ -1,12 +1,21 @@ import * as React from 'react'; import { DateTime } from 'luxon'; +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 Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; 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 Typography from '@mui/material/Typography'; import useUsageData from 'hooks/useUsageData.ee'; @@ -15,43 +24,119 @@ export default function UsageDataInformation() { return ( - - - - - - Current plan - - - {usageData.name} - - - - - Total allowed task count - - - {usageData.allowedTaskCount} - - - - - Consumed task count - - - {usageData.consumedTaskCount} - - - - - Next billing date - - - {usageData.nextResetAt?.toLocaleString(DateTime.DATE_FULL)} - - -
-
+ + + + + Subscription plan + + {/* */} + + + + + + + + Monthly quota + + + Free trial + + + + + + + + + + + + Next bill amount + + + --- + + + + {/* */} + + + + + + + + Next bill date + + + --- + + + + {/* */} + + + + + + + Your usage + + + + Last 30 days total usage + + + + + + Tasks + + + 12300 + + + + + + +
); } diff --git a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx index af12fbdf..62a7de4f 100644 --- a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx +++ b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx @@ -2,8 +2,7 @@ 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 PageTitle from 'components/PageTitle'; import Container from 'components/Container'; @@ -16,27 +15,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')} + - - - - ); From b1138dbf05e88889e061f64b8c59811d02cc3265 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Sat, 18 Mar 2023 20:43:08 +0000 Subject: [PATCH 06/18] feat: make billings page responsive --- .../UsageDataInformation/index.ee.tsx | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/web/src/components/UsageDataInformation/index.ee.tsx b/packages/web/src/components/UsageDataInformation/index.ee.tsx index 96814ee4..623ada51 100644 --- a/packages/web/src/components/UsageDataInformation/index.ee.tsx +++ b/packages/web/src/components/UsageDataInformation/index.ee.tsx @@ -6,15 +6,8 @@ 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 Chip from '@mui/material/Chip'; import Divider from '@mui/material/Divider'; import Grid from '@mui/material/Grid'; -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 Typography from '@mui/material/Typography'; import useUsageData from 'hooks/useUsageData.ee'; @@ -24,7 +17,7 @@ export default function UsageDataInformation() { return ( - + @@ -33,12 +26,19 @@ export default function UsageDataInformation() { {/* */} - - + + theme.palette.background.default, }} > @@ -54,31 +54,35 @@ export default function UsageDataInformation() { - + + theme.palette.background.default, }} > Next bill amount + --- + {/* */} - + + theme.palette.background.default, }} > @@ -89,6 +93,7 @@ export default function UsageDataInformation() { --- + {/* From 722c39590ff92697773fea973689d6264e6af230 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 19 Mar 2023 01:24:41 +0300 Subject: [PATCH 07/18] feat: Start trial period on cloud --- packages/backend/package.json | 1 + ...318220822_add_trial_expiry_date_to_users.ts | 18 ++++++++++++++++++ packages/backend/src/models/user.ts | 11 +++++++++++ yarn.lock | 5 +++++ 4 files changed, 35 insertions(+) create mode 100644 packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index a3e2a4db..2c0ed6e8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -54,6 +54,7 @@ "lodash.get": "^4.4.2", "luxon": "2.5.2", "memory-cache": "^0.2.0", + "moment": "^2.29.4", "morgan": "^1.10.0", "multer": "1.4.5-lts.1", "nodemailer": "6.7.0", 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/models/user.ts b/packages/backend/src/models/user.ts index 57b07e89..fc880ab5 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 moment from 'moment'; +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 = moment().add(30, 'days').calendar(); + } + 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/yarn.lock b/yarn.lock index 2a0b5d32..c5bc147b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12251,6 +12251,11 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + morgan@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" From 280d16f3d9230024126c8f504e3cbf8d3b9e41e2 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 19 Mar 2023 13:39:06 +0300 Subject: [PATCH 08/18] chore: Use luxon instead moment as date utility --- packages/backend/package.json | 1 - packages/backend/src/models/user.ts | 4 ++-- yarn.lock | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 2c0ed6e8..a3e2a4db 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -54,7 +54,6 @@ "lodash.get": "^4.4.2", "luxon": "2.5.2", "memory-cache": "^0.2.0", - "moment": "^2.29.4", "morgan": "^1.10.0", "multer": "1.4.5-lts.1", "nodemailer": "6.7.0", diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts index fc880ab5..bac17b40 100644 --- a/packages/backend/src/models/user.ts +++ b/packages/backend/src/models/user.ts @@ -1,5 +1,5 @@ import { QueryContext, ModelOptions } from 'objection'; -import moment from 'moment'; +import { DateTime } from 'luxon'; import appConfig from '../config/app'; import Base from './base'; import Connection from './connection'; @@ -137,7 +137,7 @@ class User extends Base { } async startTrialPeriod() { - this.trialExpiryDate = moment().add(30, 'days').calendar(); + this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toFormat('D'); } async $beforeInsert(queryContext: QueryContext) { diff --git a/yarn.lock b/yarn.lock index c5bc147b..2a0b5d32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12251,11 +12251,6 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== - morgan@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" From 189432c228aa449e6dc15dc580cf029f575a91fc Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 19 Mar 2023 15:53:34 +0300 Subject: [PATCH 09/18] feat: Implement draft version of UpgradeFreeTrial component --- .../components/UpgradeFreeTrial/index.ee.tsx | 146 ++++++++++++++++++ .../BillingAndUsageSettings/index.ee.tsx | 5 + 2 files changed, 151 insertions(+) create mode 100644 packages/web/src/components/UpgradeFreeTrial/index.ee.tsx 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..ef68864d --- /dev/null +++ b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx @@ -0,0 +1,146 @@ +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'; + +const rows = [ + { tasks: '10,000', price: '€20 / month', selected: true }, + { tasks: '30,000', price: '€50 / month', selected: false }, +]; + +export default function UpgradeFreeTrial() { + return ( + + + + + + Upgrade your free trial + + {/* */} + + + + + + + theme.palette.background.default, + }} + > + + + + Monthly Tasks + + + + + Price + + + + + + {rows.map((row) => ( + + + + {row.tasks} + + + + + {row.price} + + + + ))} + +
+
+
+ + + + + Due today:  + + + €20 + + + + + + VAT if applicable + + + +
+
+
+ ); +} diff --git a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx index 62a7de4f..b6aa962c 100644 --- a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx +++ b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx @@ -4,6 +4,7 @@ import Grid from '@mui/material/Grid'; 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'; @@ -34,6 +35,10 @@ function BillingAndUsageSettings() { + + + +
); From 40862fcd019ea585f51929fca0da989802033516 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Sun, 19 Mar 2023 22:22:11 +0000 Subject: [PATCH 10/18] feat: move plan upgrade to its page --- .../components/UpgradeFreeTrial/index.ee.tsx | 29 ++++++++----- .../UsageDataInformation/index.ee.tsx | 12 ++++-- packages/web/src/config/urls.ts | 2 + packages/web/src/locales/en.json | 3 +- .../BillingAndUsageSettings/index.ee.tsx | 4 -- .../web/src/pages/PlanUpgrade/index.ee.tsx | 42 +++++++++++++++++++ packages/web/src/settingsRoutes.tsx | 10 +++++ 7 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 packages/web/src/pages/PlanUpgrade/index.ee.tsx diff --git a/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx index ef68864d..9608e637 100644 --- a/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx +++ b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx @@ -16,12 +16,17 @@ import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import LockIcon from '@mui/icons-material/Lock'; -const rows = [ - { tasks: '10,000', price: '€20 / month', selected: true }, - { tasks: '30,000', price: '€50 / month', selected: false }, +const plans = [ + { tasks: '10,000', price: '€20' }, + { tasks: '30,000', price: '€50' }, ]; export default function UpgradeFreeTrial() { + const [selectedIndex, setSelectedIndex] = React.useState(0); + const selectedPlan = plans[selectedIndex]; + + const updateSelection = (index: number) => setSelectedIndex(index); + return ( @@ -42,7 +47,7 @@ export default function UpgradeFreeTrial() { alignItems="stretch" > - +
@@ -69,19 +74,21 @@ export default function UpgradeFreeTrial() { - {rows.map((row) => ( + {plans.map((row, index) => ( updateSelection(index)} sx={{ - backgroundColor: row.selected ? '#f1f3fa' : 'white', - border: row.selected ? '2px solid #0059f7' : 'none', + '&:hover': { cursor: 'pointer' }, + backgroundColor: selectedIndex === index ? '#f1f3fa' : 'white', + border: selectedIndex === index ? '2px solid #0059f7' : 'none', }} > {row.tasks} @@ -91,10 +98,10 @@ export default function UpgradeFreeTrial() { - {row.price} + {row.price} / month @@ -127,7 +134,7 @@ export default function UpgradeFreeTrial() { fontWeight: 'bold', }} > - €20 + {selectedPlan.price} diff --git a/packages/web/src/components/UsageDataInformation/index.ee.tsx b/packages/web/src/components/UsageDataInformation/index.ee.tsx index 623ada51..296ef5b6 100644 --- a/packages/web/src/components/UsageDataInformation/index.ee.tsx +++ b/packages/web/src/components/UsageDataInformation/index.ee.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { DateTime } from 'luxon'; - +import { Link } from 'react-router-dom'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -10,6 +9,7 @@ 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() { @@ -143,7 +143,13 @@ export default function UsageDataInformation() { - 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/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 b6aa962c..3b5383e4 100644 --- a/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx +++ b/packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx @@ -35,10 +35,6 @@ function BillingAndUsageSettings() { - - - - ); 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 ( } /> + + + + } + /> + } From f1358c7ad1691974d11de7003dd9af9424c516fe Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 20 Mar 2023 19:12:21 +0300 Subject: [PATCH 11/18] feat: add GetPaymentPlans graphQL query --- packages/backend/src/config/app.ts | 4 ++++ .../src/graphql/queries/get-payment-plans.ee.ts | 10 ++++++++++ packages/backend/src/graphql/query-resolvers.ts | 2 ++ packages/backend/src/graphql/schema.graphql | 8 ++++++++ packages/backend/src/helpers/billing/index.ee.ts | 2 ++ packages/backend/src/helpers/billing/plans.ee.ts | 16 ++++++++++++++++ 6 files changed, 42 insertions(+) create mode 100644 packages/backend/src/graphql/queries/get-payment-plans.ee.ts create mode 100644 packages/backend/src/helpers/billing/plans.ee.ts 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/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; From 3598d439386198e9c79288f3479b99b5a3d2c6d4 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 20 Mar 2023 21:46:20 +0000 Subject: [PATCH 12/18] feat: make payment plans dynamic --- packages/types/index.d.ts | 8 +++++++- .../components/UpgradeFreeTrial/index.ee.tsx | 19 +++++++++---------- .../graphql/queries/get-payment-plans.ee.ts | 12 ++++++++++++ packages/web/src/hooks/usePaymentPlans.ee.ts | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 packages/web/src/graphql/queries/get-payment-plans.ee.ts create mode 100644 packages/web/src/hooks/usePaymentPlans.ee.ts 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/UpgradeFreeTrial/index.ee.tsx b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx index 9608e637..4fca59a0 100644 --- a/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx +++ b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; - import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -16,17 +15,17 @@ import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import LockIcon from '@mui/icons-material/Lock'; -const plans = [ - { tasks: '10,000', price: '€20' }, - { tasks: '30,000', price: '€50' }, -]; +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 selectedPlan = plans?.[selectedIndex]; const updateSelection = (index: number) => setSelectedIndex(index); + if (loading || !plans.length) return null; + return ( @@ -74,9 +73,9 @@ export default function UpgradeFreeTrial() { - {plans.map((row, index) => ( + {plans.map((plan, index) => ( updateSelection(index)} sx={{ '&:hover': { cursor: 'pointer' }, @@ -91,7 +90,7 @@ export default function UpgradeFreeTrial() { fontWeight: selectedIndex === index ? 'bold' : 'normal', }} > - {row.tasks} + {plan.limit} @@ -101,7 +100,7 @@ export default function UpgradeFreeTrial() { fontWeight: selectedIndex === index ? 'bold' : 'normal', }} > - {row.price} / month + {plan.price} / month 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 + }; +} From 040ad9edb0b68811ca969102359be06929a5dff4 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 21 Mar 2023 00:55:27 +0300 Subject: [PATCH 13/18] feat: Implement getPaddleInfo graphQL query --- .../backend/src/graphql/queries/get-paddle-info.ee.ts | 10 ++++++++++ packages/backend/src/graphql/query-resolvers.ts | 2 ++ packages/backend/src/graphql/schema.graphql | 6 ++++++ packages/backend/src/helpers/billing/index.ee.ts | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 packages/backend/src/graphql/queries/get-paddle-info.ee.ts diff --git a/packages/backend/src/graphql/queries/get-paddle-info.ee.ts b/packages/backend/src/graphql/queries/get-paddle-info.ee.ts new file mode 100644 index 00000000..35fc3c64 --- /dev/null +++ b/packages/backend/src/graphql/queries/get-paddle-info.ee.ts @@ -0,0 +1,10 @@ +import appConfig from '../../config/app'; +import Billing from '../../helpers/billing/index.ee'; + +const getPaddleInfo = async () => { + if (!appConfig.isCloud) return; + + return Billing.paddleInfo; +}; + +export default getPaddleInfo; diff --git a/packages/backend/src/graphql/query-resolvers.ts b/packages/backend/src/graphql/query-resolvers.ts index 267b3356..e8ae123f 100644 --- a/packages/backend/src/graphql/query-resolvers.ts +++ b/packages/backend/src/graphql/query-resolvers.ts @@ -13,6 +13,7 @@ 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 getPaddleInfo from './queries/get-paddle-info.ee'; import getPaymentPortalUrl from './queries/get-payment-portal-url.ee'; import getAutomatischInfo from './queries/get-automatisch-info'; import healthcheck from './queries/healthcheck'; @@ -33,6 +34,7 @@ const queryResolvers = { getCurrentUser, getUsageData, getPaymentPlans, + getPaddleInfo, getPaymentPortalUrl, getAutomatischInfo, healthcheck, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 63817641..872a0556 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -37,6 +37,7 @@ type Query { getUsageData: GetUsageData getPaymentPortalUrl: GetPaymentPortalUrl getPaymentPlans: [PaymentPlan] + getPaddleInfo: GetPaddleInfo getAutomatischInfo: GetAutomatischInfo healthcheck: AppHealth } @@ -482,6 +483,11 @@ type GetPaymentPortalUrl { url: String } +type GetPaddleInfo { + sandbox: Boolean + vendorId: String +} + type PaymentPlan { name: String limit: String diff --git a/packages/backend/src/helpers/billing/index.ee.ts b/packages/backend/src/helpers/billing/index.ee.ts index 863e68e7..6dc5b3dc 100644 --- a/packages/backend/src/helpers/billing/index.ee.ts +++ b/packages/backend/src/helpers/billing/index.ee.ts @@ -90,6 +90,11 @@ const createPaymentPortalUrl = async (user: User) => { return userSession.url; }; +const paddleInfo = { + sandbox: appConfig.isDev ? true : false, + vendorId: appConfig.paddleVendorId, +}; + const billing = { createSubscription, createPaymentPortalUrl, @@ -97,6 +102,7 @@ const billing = { stripe, plans, paddlePlans, + paddleInfo, }; export default billing; From 66d7baa1267d1154d5ce86ee05cc43f0228697e6 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 20 Mar 2023 23:23:44 +0000 Subject: [PATCH 14/18] fix: make Paddle vendor id number --- packages/backend/src/config/app.ts | 4 ++-- packages/backend/src/graphql/schema.graphql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 3364b834..a18038b1 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -39,7 +39,7 @@ type AppConfig = { smtpPassword: string; fromEmail: string; isCloud: boolean; - paddleVendorId: string; + paddleVendorId: number; paddleVendorAuthCode: string; stripeSecretKey: string; stripeSigningSecret: string; @@ -113,7 +113,7 @@ 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, + paddleVendorId: Number(process.env.PADDLE_VENDOR_ID), paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE, stripeSecretKey: process.env.STRIPE_SECRET_KEY, stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 872a0556..36f7de89 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -485,7 +485,7 @@ type GetPaymentPortalUrl { type GetPaddleInfo { sandbox: Boolean - vendorId: String + vendorId: Int } type PaymentPlan { From b5ed984f058097564b8e80574f00a5142acbb64f Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 20 Mar 2023 23:24:04 +0000 Subject: [PATCH 15/18] feat: add checkout process --- .../components/UpgradeFreeTrial/index.ee.tsx | 20 +++++- packages/web/src/contexts/Paddle.ee.tsx | 72 +++++++++++++++++++ .../src/graphql/queries/get-paddle-info.ee.ts | 10 +++ packages/web/src/hooks/usePaddle.ee.ts | 14 ++++ packages/web/src/hooks/usePaddleInfo.ee.ts | 19 +++++ packages/web/src/index.tsx | 11 +-- 6 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/contexts/Paddle.ee.tsx create mode 100644 packages/web/src/graphql/queries/get-paddle-info.ee.ts create mode 100644 packages/web/src/hooks/usePaddle.ee.ts create mode 100644 packages/web/src/hooks/usePaddleInfo.ee.ts diff --git a/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx index 4fca59a0..9a0cfa90 100644 --- a/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx +++ b/packages/web/src/components/UpgradeFreeTrial/index.ee.tsx @@ -16,14 +16,26 @@ import Paper from '@mui/material/Paper'; import LockIcon from '@mui/icons-material/Lock'; import usePaymentPlans from 'hooks/usePaymentPlans.ee'; +import useCurrentUser from 'hooks/useCurrentUser'; +import usePaddle from 'hooks/usePaddle.ee'; export default function UpgradeFreeTrial() { const { plans, loading } = usePaymentPlans(); + const currentUser = useCurrentUser(); + const { loaded: paddleLoaded } = usePaddle(); const [selectedIndex, setSelectedIndex] = React.useState(0); const selectedPlan = plans?.[selectedIndex]; const updateSelection = (index: number) => setSelectedIndex(index); + const handleCheckout = React.useCallback(() => { + window.Paddle.Checkout?.open({ + product: selectedPlan.productId, + email: currentUser.email, + passthrough: JSON.stringify({ id: currentUser.id, email: currentUser.email }) + }) + }, [selectedPlan, currentUser]); + if (loading || !plans.length) return null; return ( @@ -140,7 +152,13 @@ export default function UpgradeFreeTrial() { + VAT if applicable - diff --git a/packages/web/src/contexts/Paddle.ee.tsx b/packages/web/src/contexts/Paddle.ee.tsx new file mode 100644 index 00000000..e4c0d5b7 --- /dev/null +++ b/packages/web/src/contexts/Paddle.ee.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; + +import useCloud from 'hooks/useCloud'; +import usePaddleInfo from 'hooks/usePaddleInfo.ee'; + +declare global { + interface Window { + Paddle: any; + } +} + +export type PaddleContextParams = { + loaded: boolean; +}; + +export const PaddleContext = + React.createContext({ + loaded: false, + }); + +type PaddleProviderProps = { + children: React.ReactNode; +}; + +export const PaddleProvider = ( + props: PaddleProviderProps +): React.ReactElement => { + const { children } = props; + const isCloud = useCloud(); + const { sandbox, vendorId } = usePaddleInfo(); + const [loaded, setLoaded] = React.useState(false); + + const value = React.useMemo(() => { + return { + loaded, + }; + }, [loaded]); + + React.useEffect(function loadPaddleScript() { + if (!isCloud) return; + + const g = document.createElement('script') + const s = document.getElementsByTagName('script')[0]; + g.src = 'https://cdn.paddle.com/paddle/paddle.js'; + g.defer = true; + g.async = true; + + if (s.parentNode) { + s.parentNode.insertBefore(g, s); + } + + g.onload = function () { + setLoaded(true); + } + }, [isCloud]); + + React.useEffect(function initPaddleScript() { + if (!loaded || !vendorId) return; + + if (sandbox) { + window.Paddle.Environment.set('sandbox'); + } + + window.Paddle.Setup({ vendor: vendorId }); + }, [loaded, sandbox, vendorId]) + + return ( + + {children} + + ); +}; diff --git a/packages/web/src/graphql/queries/get-paddle-info.ee.ts b/packages/web/src/graphql/queries/get-paddle-info.ee.ts new file mode 100644 index 00000000..4e801865 --- /dev/null +++ b/packages/web/src/graphql/queries/get-paddle-info.ee.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const GET_PADDLE_INFO = gql` + query GetPaddleInfo { + getPaddleInfo { + sandbox + vendorId + } + } +`; diff --git a/packages/web/src/hooks/usePaddle.ee.ts b/packages/web/src/hooks/usePaddle.ee.ts new file mode 100644 index 00000000..747f0661 --- /dev/null +++ b/packages/web/src/hooks/usePaddle.ee.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { PaddleContext } from 'contexts/Paddle.ee'; + +type UsePaddleReturn = { + loaded: boolean; +}; + +export default function usePaddle(): UsePaddleReturn { + const paddleContext = React.useContext(PaddleContext); + + return { + loaded: paddleContext.loaded, + }; +} diff --git a/packages/web/src/hooks/usePaddleInfo.ee.ts b/packages/web/src/hooks/usePaddleInfo.ee.ts new file mode 100644 index 00000000..85ff5905 --- /dev/null +++ b/packages/web/src/hooks/usePaddleInfo.ee.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@apollo/client'; + +import { GET_PADDLE_INFO } from 'graphql/queries/get-paddle-info.ee'; + +type UsePaddleInfoReturn = { + sandbox: boolean; + vendorId: string; + loading: boolean; +}; + +export default function usePaddleInfo(): UsePaddleInfoReturn { + const { data, loading } = useQuery(GET_PADDLE_INFO); + + return { + sandbox: data?.getPaddleInfo?.sandbox, + vendorId: data?.getPaddleInfo?.vendorId, + loading + }; +} diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx index c2cd7099..bc657214 100644 --- a/packages/web/src/index.tsx +++ b/packages/web/src/index.tsx @@ -6,6 +6,7 @@ import ApolloProvider from 'components/ApolloProvider'; import SnackbarProvider from 'components/SnackbarProvider'; import { AuthenticationProvider } from 'contexts/Authentication'; import { AutomatischInfoProvider } from 'contexts/AutomatischInfo'; +import { PaddleProvider } from 'contexts/Paddle.ee'; import Router from 'components/Router'; import LiveChat from 'components/LiveChat/index.ee'; import routes from 'routes'; @@ -18,11 +19,13 @@ ReactDOM.render( - - {routes} + + + {routes} - - + + + From d3ef45db1b2e2a397be0289ab7a3340ac4ae0321 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 21 Mar 2023 11:49:44 +0300 Subject: [PATCH 16/18] chore: Remove stripe-related functionality --- packages/backend/src/config/app.ts | 8 -- .../src/controllers/stripe/webhooks.ee.ts | 27 ------ .../src/graphql/mutations/create-user.ee.ts | 6 -- .../queries/get-payment-portal-url.ee.ts | 16 ---- .../backend/src/graphql/query-resolvers.ts | 2 - packages/backend/src/graphql/schema.graphql | 5 - .../backend/src/helpers/billing/index.ee.ts | 94 ------------------- .../src/helpers/billing/webhooks.ee.ts | 42 --------- packages/backend/src/routes/index.ts | 2 - packages/backend/src/routes/stripe.ee.ts | 23 ----- 10 files changed, 225 deletions(-) delete mode 100644 packages/backend/src/controllers/stripe/webhooks.ee.ts delete mode 100644 packages/backend/src/graphql/queries/get-payment-portal-url.ee.ts delete mode 100644 packages/backend/src/helpers/billing/webhooks.ee.ts delete mode 100644 packages/backend/src/routes/stripe.ee.ts diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index a18038b1..89cb8f9d 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -41,10 +41,6 @@ type AppConfig = { isCloud: boolean; paddleVendorId: number; paddleVendorAuthCode: string; - stripeSecretKey: string; - stripeSigningSecret: string; - stripeStarterPriceKey: string; - stripeGrowthPriceKey: string; licenseKey: string; sentryDsn: string; }; @@ -115,10 +111,6 @@ const appConfig: AppConfig = { isCloud: process.env.AUTOMATISCH_CLOUD === 'true', paddleVendorId: Number(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, - stripeGrowthPriceKey: process.env.STRIPE_GROWTH_PRICE_KEY, licenseKey: process.env.LICENSE_KEY, sentryDsn: process.env.SENTRY_DSN, }; diff --git a/packages/backend/src/controllers/stripe/webhooks.ee.ts b/packages/backend/src/controllers/stripe/webhooks.ee.ts deleted file mode 100644 index bd398dfe..00000000 --- a/packages/backend/src/controllers/stripe/webhooks.ee.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Response } from 'express'; -import { IRequest } from '@automatisch/types'; - -import * as Sentry from '../../helpers/sentry.ee'; -import Billing from '../../helpers/billing/index.ee'; -import appConfig from '../../config/app'; -import logger from '../../helpers/logger'; - -export default async (request: IRequest, response: Response) => { - const signature = request.headers['stripe-signature']; - - try { - const event = Billing.stripe.webhooks.constructEvent( - request.rawBody, - signature, - appConfig.stripeSigningSecret - ); - - await Billing.handleWebhooks(event); - return response.sendStatus(200); - } catch (error) { - logger.error(`Webhook Error: ${error.message}`); - - Sentry.captureException(error); - return response.sendStatus(400); - } -}; diff --git a/packages/backend/src/graphql/mutations/create-user.ee.ts b/packages/backend/src/graphql/mutations/create-user.ee.ts index 3f875ce7..a6f7d12a 100644 --- a/packages/backend/src/graphql/mutations/create-user.ee.ts +++ b/packages/backend/src/graphql/mutations/create-user.ee.ts @@ -1,6 +1,4 @@ import User from '../../models/user'; -import Billing from '../../helpers/billing/index.ee'; -import appConfig from '../../config/app'; type Params = { input: { @@ -26,10 +24,6 @@ const createUser = async (_parent: unknown, params: Params) => { role: 'user', }); - if (appConfig.isCloud) { - await Billing.createSubscription(user); - } - return user; }; diff --git a/packages/backend/src/graphql/queries/get-payment-portal-url.ee.ts b/packages/backend/src/graphql/queries/get-payment-portal-url.ee.ts deleted file mode 100644 index 9b17b00f..00000000 --- a/packages/backend/src/graphql/queries/get-payment-portal-url.ee.ts +++ /dev/null @@ -1,16 +0,0 @@ -import appConfig from '../../config/app'; -import Context from '../../types/express/context'; -import Billing from '../../helpers/billing/index.ee'; - -const getPaymentPortalUrl = async ( - _parent: unknown, - _params: unknown, - context: Context -) => { - if (!appConfig.isCloud) return; - - const url = Billing.createPaymentPortalUrl(context.currentUser); - return { url }; -}; - -export default getPaymentPortalUrl; diff --git a/packages/backend/src/graphql/query-resolvers.ts b/packages/backend/src/graphql/query-resolvers.ts index e8ae123f..1c16b8d0 100644 --- a/packages/backend/src/graphql/query-resolvers.ts +++ b/packages/backend/src/graphql/query-resolvers.ts @@ -14,7 +14,6 @@ import getCurrentUser from './queries/get-current-user'; import getUsageData from './queries/get-usage-data.ee'; import getPaymentPlans from './queries/get-payment-plans.ee'; import getPaddleInfo from './queries/get-paddle-info.ee'; -import getPaymentPortalUrl from './queries/get-payment-portal-url.ee'; import getAutomatischInfo from './queries/get-automatisch-info'; import healthcheck from './queries/healthcheck'; @@ -35,7 +34,6 @@ const queryResolvers = { getUsageData, getPaymentPlans, getPaddleInfo, - getPaymentPortalUrl, getAutomatischInfo, healthcheck, }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 36f7de89..9d9abf6d 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -35,7 +35,6 @@ type Query { ): [SubstepArgument] getCurrentUser: User getUsageData: GetUsageData - getPaymentPortalUrl: GetPaymentPortalUrl getPaymentPlans: [PaymentPlan] getPaddleInfo: GetPaddleInfo getAutomatischInfo: GetAutomatischInfo @@ -479,10 +478,6 @@ type GetUsageData { nextResetAt: String } -type GetPaymentPortalUrl { - url: String -} - type GetPaddleInfo { sandbox: Boolean vendorId: Int diff --git a/packages/backend/src/helpers/billing/index.ee.ts b/packages/backend/src/helpers/billing/index.ee.ts index 6dc5b3dc..0e684efc 100644 --- a/packages/backend/src/helpers/billing/index.ee.ts +++ b/packages/backend/src/helpers/billing/index.ee.ts @@ -1,106 +1,12 @@ -import Stripe from 'stripe'; -import User from '../../models/user'; -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 = [ - { - price: appConfig.stripeStarterPriceKey, - name: 'Starter', - taskCount: 1000, - default: true, - }, - { - price: appConfig.stripeGrowthPriceKey, - name: 'Growth', - taskCount: 10000, - default: false, - }, -]; - -const stripe = new Stripe(appConfig.stripeSecretKey, { - apiVersion: '2022-11-15', -}); - -const createStripeCustomer = async (user: User) => { - const params: Stripe.CustomerCreateParams = { - email: user.email, - name: user.fullName, - description: `User ID: ${user.id}`, - }; - - return await stripe.customers.create(params); -}; - -const defaultPlan = plans.find((plan) => plan.default); - -const createStripeSubscription = async ( - user: User, - stripeCustomer: Stripe.Customer -) => { - const params: Stripe.SubscriptionCreateParams = { - customer: stripeCustomer.id, - items: [{ price: defaultPlan.price }], - }; - - return await stripe.subscriptions.create(params); -}; - -const createSubscription = async (user: User) => { - const stripeCustomer = await createStripeCustomer(user); - const stripeSubscription = await createStripeSubscription( - user, - stripeCustomer - ); - - await PaymentPlan.query().insert({ - name: defaultPlan.name, - taskCount: defaultPlan.taskCount, - userId: user.id, - stripeCustomerId: stripeCustomer.id, - stripeSubscriptionId: stripeSubscription.id, - currentPeriodStartedAt: new Date( - stripeSubscription.current_period_start * 1000 - ).toISOString(), - currentPeriodEndsAt: new Date( - stripeSubscription.current_period_end * 1000 - ).toISOString(), - }); - - await UsageData.query().insert({ - userId: user.id, - consumedTaskCount: 0, - nextResetAt: new Date( - stripeSubscription.current_period_end * 1000 - ).toISOString(), - }); -}; - -const createPaymentPortalUrl = async (user: User) => { - const paymentPlan = await user.$relatedQuery('paymentPlan'); - - const userSession = await stripe.billingPortal.sessions.create({ - customer: paymentPlan.stripeCustomerId, - return_url: 'https://cloud.automatisch.io/settings/billing', - }); - - return userSession.url; -}; - const paddleInfo = { sandbox: appConfig.isDev ? true : false, vendorId: appConfig.paddleVendorId, }; const billing = { - createSubscription, - createPaymentPortalUrl, - handleWebhooks, - stripe, - plans, paddlePlans, paddleInfo, }; diff --git a/packages/backend/src/helpers/billing/webhooks.ee.ts b/packages/backend/src/helpers/billing/webhooks.ee.ts deleted file mode 100644 index 8ec18830..00000000 --- a/packages/backend/src/helpers/billing/webhooks.ee.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Stripe from 'stripe'; -import PaymentPlan from '../../models/payment-plan.ee'; -import Billing from './index.ee'; - -const handleWebhooks = async (event: Stripe.Event) => { - const trackedWebhookTypes = [ - 'customer.subscription.created', - 'customer.subscription.updated', - 'customer.subscription.deleted', - ]; - - if (!trackedWebhookTypes.includes(event.type)) { - return; - } - - await updatePaymentPlan(event); -}; - -const updatePaymentPlan = async (event: Stripe.Event) => { - const subscription = event.data.object as Stripe.Subscription; - const priceKey = subscription.items.data[0].plan.id; - const plan = Billing.plans.find((plan) => plan.price === priceKey); - - const paymentPlan = await PaymentPlan.query().findOne({ - stripe_customer_id: subscription.customer, - }); - - await paymentPlan.$query().patchAndFetch({ - name: plan.name, - taskCount: plan.taskCount, - stripeSubscriptionId: subscription.id, - }); - - const user = await paymentPlan.$relatedQuery('user'); - const usageData = await user.$relatedQuery('usageData'); - - await usageData.$query().patchAndFetch({ - nextResetAt: new Date(subscription.current_period_end * 1000).toISOString(), - }); -}; - -export default handleWebhooks; diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 853556ba..1cb26316 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -1,12 +1,10 @@ import { Router } from 'express'; import graphQLInstance from '../helpers/graphql-instance'; import webhooksRouter from './webhooks'; -import stripeRouter from './stripe.ee'; const router = Router(); router.use('/graphql', graphQLInstance); router.use('/webhooks', webhooksRouter); -router.use('/stripe', stripeRouter); export default router; diff --git a/packages/backend/src/routes/stripe.ee.ts b/packages/backend/src/routes/stripe.ee.ts deleted file mode 100644 index f6c71aef..00000000 --- a/packages/backend/src/routes/stripe.ee.ts +++ /dev/null @@ -1,23 +0,0 @@ -import express, { Router } from 'express'; -import multer from 'multer'; -import { IRequest } from '@automatisch/types'; -import appConfig from '../config/app'; -import stripeWebhooksAction from '../controllers/stripe/webhooks.ee'; - -const router = Router(); -const upload = multer(); - -router.use(upload.none()); - -router.use( - express.text({ - limit: appConfig.requestBodySizeLimit, - verify(req, res, buf) { - (req as IRequest).rawBody = buf; - }, - }) -); - -router.post('/webhooks', stripeWebhooksAction); - -export default router; From e1d26325f3a63453e20878e4053a9235a72a2000 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 21 Mar 2023 22:02:11 +0300 Subject: [PATCH 17/18] feat: Verify paddle webhooks --- packages/backend/package.json | 1 + packages/backend/src/app.ts | 16 +- packages/backend/src/config/app.ts | 2 + .../src/controllers/paddle/webhooks.ee.ts | 14 ++ .../backend/src/helpers/billing/index.ee.ts | 2 + .../backend/src/helpers/billing/paddle.ee.ts | 10 ++ packages/backend/src/routes/index.ts | 2 + packages/backend/src/routes/paddle.ee.ts | 19 +++ yarn.lock | 143 +++++++++++++++++- 9 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 packages/backend/src/controllers/paddle/webhooks.ee.ts create mode 100644 packages/backend/src/helpers/billing/paddle.ee.ts create mode 100644 packages/backend/src/routes/paddle.ee.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index a3e2a4db..34fbbe5c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -59,6 +59,7 @@ "nodemailer": "6.7.0", "oauth-1.0a": "^2.2.6", "objection": "^3.0.0", + "paddle-sdk": "^3.3.0", "pg": "^8.7.1", "stripe": "^11.13.0", "winston": "^3.7.1" diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index e12d1c06..bade3e86 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -40,13 +40,15 @@ app.use( }, }) ); -app.use(express.urlencoded({ - extended: false, - limit: appConfig.requestBodySizeLimit, - verify(req, res, buf) { - (req as IRequest).rawBody = buf; - }, -})); +app.use( + express.urlencoded({ + extended: true, + limit: appConfig.requestBodySizeLimit, + verify(req, res, buf) { + (req as IRequest).rawBody = buf; + }, + }) +); app.use(cors(corsOptions)); app.use('/', router); diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 89cb8f9d..ab7fd430 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -41,6 +41,7 @@ type AppConfig = { isCloud: boolean; paddleVendorId: number; paddleVendorAuthCode: string; + paddlePublicKey: string; licenseKey: string; sentryDsn: string; }; @@ -111,6 +112,7 @@ const appConfig: AppConfig = { isCloud: process.env.AUTOMATISCH_CLOUD === 'true', paddleVendorId: Number(process.env.PADDLE_VENDOR_ID), paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE, + paddlePublicKey: process.env.PADDLE_PUBLIC_KEY, licenseKey: process.env.LICENSE_KEY, sentryDsn: process.env.SENTRY_DSN, }; diff --git a/packages/backend/src/controllers/paddle/webhooks.ee.ts b/packages/backend/src/controllers/paddle/webhooks.ee.ts new file mode 100644 index 00000000..7786c59c --- /dev/null +++ b/packages/backend/src/controllers/paddle/webhooks.ee.ts @@ -0,0 +1,14 @@ +import { Response } from 'express'; +import { IRequest } from '@automatisch/types'; +import Billing from '../../helpers/billing/index.ee'; + +export default async (request: IRequest, response: Response) => { + const isVerified = Billing.paddleClient.verifyWebhookData(request.body); + + if (!isVerified) { + return response.sendStatus(401); + } + + // TODO: Handle Paddle webhooks + return response.sendStatus(200); +}; diff --git a/packages/backend/src/helpers/billing/index.ee.ts b/packages/backend/src/helpers/billing/index.ee.ts index 0e684efc..ef882dde 100644 --- a/packages/backend/src/helpers/billing/index.ee.ts +++ b/packages/backend/src/helpers/billing/index.ee.ts @@ -1,4 +1,5 @@ import appConfig from '../../config/app'; +import paddleClient from './paddle.ee'; import paddlePlans from './plans.ee'; const paddleInfo = { @@ -7,6 +8,7 @@ const paddleInfo = { }; const billing = { + paddleClient, paddlePlans, paddleInfo, }; diff --git a/packages/backend/src/helpers/billing/paddle.ee.ts b/packages/backend/src/helpers/billing/paddle.ee.ts new file mode 100644 index 00000000..ed063d93 --- /dev/null +++ b/packages/backend/src/helpers/billing/paddle.ee.ts @@ -0,0 +1,10 @@ +import PaddleSDK from 'paddle-sdk'; +import appConfig from '../../config/app'; + +const paddleClient = new PaddleSDK( + appConfig.paddleVendorId.toString(), + appConfig.paddleVendorAuthCode, + appConfig.paddlePublicKey +); + +export default paddleClient; diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 1cb26316..2a52de7a 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -1,10 +1,12 @@ import { Router } from 'express'; import graphQLInstance from '../helpers/graphql-instance'; import webhooksRouter from './webhooks'; +import paddleRouter from './paddle.ee'; const router = Router(); router.use('/graphql', graphQLInstance); router.use('/webhooks', webhooksRouter); +router.use('/paddle', paddleRouter); export default router; diff --git a/packages/backend/src/routes/paddle.ee.ts b/packages/backend/src/routes/paddle.ee.ts new file mode 100644 index 00000000..46e96b65 --- /dev/null +++ b/packages/backend/src/routes/paddle.ee.ts @@ -0,0 +1,19 @@ +import { Response, Router, NextFunction, RequestHandler } from 'express'; +import { IRequest } from '@automatisch/types'; +import webhooksHandler from '../controllers/paddle/webhooks.ee'; + +const router = Router(); + +const exposeError = + (handler: RequestHandler) => + async (req: IRequest, res: Response, next: NextFunction) => { + try { + await handler(req, res, next); + } catch (err) { + next(err); + } + }; + +router.post('/webhooks', exposeError(webhooksHandler)); + +export default router; diff --git a/yarn.lock b/yarn.lock index 2a0b5d32..e7e25173 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3465,6 +3465,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sindresorhus/is@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1" + integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== + "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -3620,6 +3625,13 @@ dependencies: defer-to-connect "^1.0.1" +"@szmarczak/http-timer@^4.0.0": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + "@testing-library/dom@^7.28.1": version "7.31.2" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a" @@ -3772,6 +3784,16 @@ "@types/ioredis" "*" "@types/redis" "^2.8.0" +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + "@types/chai@*": version "4.3.0" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" @@ -3912,6 +3934,11 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== +"@types/http-cache-semantics@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" + integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + "@types/http-errors@^1.8.1": version "1.8.2" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1" @@ -3988,6 +4015,13 @@ dependencies: "@types/node" "*" +"@types/keyv@^3.1.1", "@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + "@types/lodash.get@^4.4.6": version "4.4.6" resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" @@ -4201,6 +4235,13 @@ dependencies: "@types/node" "*" +"@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/retry@^0.12.0": version "0.12.1" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" @@ -6025,6 +6066,14 @@ cacache@^15.0.3, cacache@^15.0.5, cacache@^15.2.0, cacache@^15.3.0: tar "^6.0.2" unique-filename "^1.1.1" +cacheable-lookup@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" + integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== + dependencies: + "@types/keyv" "^3.1.1" + keyv "^4.0.0" + cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" @@ -6038,6 +6087,19 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +cacheable-request@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" + integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -7358,6 +7420,13 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" + integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== + dependencies: + mimic-response "^2.0.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -7409,6 +7478,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -9487,6 +9561,27 @@ globby@^11, globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" +got@^10.2.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" + integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== + dependencies: + "@sindresorhus/is" "^2.0.0" + "@szmarczak/http-timer" "^4.0.0" + "@types/cacheable-request" "^6.0.1" + cacheable-lookup "^2.0.0" + cacheable-request "^7.0.1" + decompress-response "^5.0.0" + duplexer3 "^0.1.4" + get-stream "^5.0.0" + lowercase-keys "^2.0.0" + mimic-response "^2.1.0" + p-cancelable "^2.0.0" + p-event "^4.0.0" + responselike "^2.0.0" + to-readable-stream "^2.0.0" + type-fest "^0.10.0" + got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -11162,6 +11257,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -11324,6 +11424,13 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" + integrity sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -12072,6 +12179,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0, mimic-response@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -13108,12 +13220,17 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= -p-event@^4.2.0: +p-event@^4.0.0, p-event@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== @@ -13319,6 +13436,13 @@ pad-component@0.0.1: resolved "https://registry.yarnpkg.com/pad-component/-/pad-component-0.0.1.tgz#ad1f22ce1bf0fdc0d6ddd908af17f351a404b8ac" integrity sha1-rR8izhvw/cDW3dkIrxfzUaQEuKw= +paddle-sdk@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/paddle-sdk/-/paddle-sdk-3.3.0.tgz#8830135ecf5014242f318f9a72fe59e12770d93e" + integrity sha512-0EJY3TpMQBCI2lM3eiX6M6Fu0xsEd0AwzUf3ptJTBzWbrVXktwm3Fu23ffHNG35laLZkVaI+PgbL/JtrA0grhg== + dependencies: + got "^10.2.0" + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -15221,6 +15345,13 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -16684,6 +16815,11 @@ to-readable-stream@^1.0.0: resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== +to-readable-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" + integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -16907,6 +17043,11 @@ type-detect@4.0.8, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" + integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== + type-fest@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" From e25aab742b938ab785a58a771ad5e5a7be4c552c Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 21 Mar 2023 20:25:46 +0000 Subject: [PATCH 18/18] feat(UsageAlert): use new plan upgrade page --- .../src/components/UsageAlert/index.ee.tsx | 4 ++- .../queries/get-payment-portal-url.ee.ts | 10 ------- .../web/src/hooks/usePaymentPortalUrl.ee.ts | 16 ---------- packages/web/src/hooks/useUsageAlert.ee.ts | 29 ++++++++++--------- 4 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 packages/web/src/graphql/queries/get-payment-portal-url.ee.ts delete mode 100644 packages/web/src/hooks/usePaymentPortalUrl.ee.ts diff --git a/packages/web/src/components/UsageAlert/index.ee.tsx b/packages/web/src/components/UsageAlert/index.ee.tsx index ee6e391b..1de30f7b 100644 --- a/packages/web/src/components/UsageAlert/index.ee.tsx +++ b/packages/web/src/components/UsageAlert/index.ee.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Link } from 'react-router-dom'; import Alert from '@mui/material/Alert'; import Snackbar from '@mui/material/Snackbar'; import Typography from '@mui/material/Typography'; @@ -34,8 +35,9 @@ export default function UsageAlert() {