Merge branch 'main' into spotify-app
This commit is contained in:
@@ -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"
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -39,10 +39,9 @@ type AppConfig = {
|
||||
smtpPassword: string;
|
||||
fromEmail: string;
|
||||
isCloud: boolean;
|
||||
stripeSecretKey: string;
|
||||
stripeSigningSecret: string;
|
||||
stripeStarterPriceKey: string;
|
||||
stripeGrowthPriceKey: string;
|
||||
paddleVendorId: number;
|
||||
paddleVendorAuthCode: string;
|
||||
paddlePublicKey: string;
|
||||
licenseKey: string;
|
||||
sentryDsn: string;
|
||||
};
|
||||
@@ -111,10 +110,9 @@ const appConfig: AppConfig = {
|
||||
smtpPassword: process.env.SMTP_PASSWORD,
|
||||
fromEmail: process.env.FROM_EMAIL,
|
||||
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
|
||||
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,
|
||||
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,
|
||||
};
|
||||
|
14
packages/backend/src/controllers/paddle/webhooks.ee.ts
Normal file
14
packages/backend/src/controllers/paddle/webhooks.ee.ts
Normal file
@@ -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);
|
||||
};
|
@@ -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);
|
||||
}
|
||||
};
|
@@ -0,0 +1,18 @@
|
||||
import { Knex } from 'knex';
|
||||
import appConfig from '../../config/app';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!appConfig.isCloud) return;
|
||||
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.date('trial_expiry_date');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (!appConfig.isCloud) return;
|
||||
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.dropColumn('trial_expiry_date');
|
||||
});
|
||||
}
|
@@ -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;
|
||||
};
|
||||
|
||||
|
10
packages/backend/src/graphql/queries/get-paddle-info.ee.ts
Normal file
10
packages/backend/src/graphql/queries/get-paddle-info.ee.ts
Normal file
@@ -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;
|
10
packages/backend/src/graphql/queries/get-payment-plans.ee.ts
Normal file
10
packages/backend/src/graphql/queries/get-payment-plans.ee.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import appConfig from '../../config/app';
|
||||
import Billing from '../../helpers/billing/index.ee';
|
||||
|
||||
const getPaymentPlans = async () => {
|
||||
if (!appConfig.isCloud) return;
|
||||
|
||||
return Billing.paddlePlans;
|
||||
};
|
||||
|
||||
export default getPaymentPlans;
|
@@ -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;
|
@@ -12,7 +12,8 @@ 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 getPaymentPortalUrl from './queries/get-payment-portal-url.ee';
|
||||
import getPaymentPlans from './queries/get-payment-plans.ee';
|
||||
import getPaddleInfo from './queries/get-paddle-info.ee';
|
||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||
import healthcheck from './queries/healthcheck';
|
||||
|
||||
@@ -31,7 +32,8 @@ const queryResolvers = {
|
||||
getDynamicFields,
|
||||
getCurrentUser,
|
||||
getUsageData,
|
||||
getPaymentPortalUrl,
|
||||
getPaymentPlans,
|
||||
getPaddleInfo,
|
||||
getAutomatischInfo,
|
||||
healthcheck,
|
||||
};
|
||||
|
@@ -35,7 +35,8 @@ type Query {
|
||||
): [SubstepArgument]
|
||||
getCurrentUser: User
|
||||
getUsageData: GetUsageData
|
||||
getPaymentPortalUrl: GetPaymentPortalUrl
|
||||
getPaymentPlans: [PaymentPlan]
|
||||
getPaddleInfo: GetPaddleInfo
|
||||
getAutomatischInfo: GetAutomatischInfo
|
||||
healthcheck: AppHealth
|
||||
}
|
||||
@@ -477,8 +478,16 @@ type GetUsageData {
|
||||
nextResetAt: String
|
||||
}
|
||||
|
||||
type GetPaymentPortalUrl {
|
||||
url: String
|
||||
type GetPaddleInfo {
|
||||
sandbox: Boolean
|
||||
vendorId: Int
|
||||
}
|
||||
|
||||
type PaymentPlan {
|
||||
name: String
|
||||
limit: String
|
||||
price: String
|
||||
productId: String
|
||||
}
|
||||
|
||||
schema {
|
||||
|
@@ -1,100 +1,16 @@
|
||||
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 paddleClient from './paddle.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,
|
||||
paddleClient,
|
||||
paddlePlans,
|
||||
paddleInfo,
|
||||
};
|
||||
|
||||
export default billing;
|
||||
|
10
packages/backend/src/helpers/billing/paddle.ee.ts
Normal file
10
packages/backend/src/helpers/billing/paddle.ee.ts
Normal file
@@ -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;
|
16
packages/backend/src/helpers/billing/plans.ee.ts
Normal file
16
packages/backend/src/helpers/billing/plans.ee.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const plans = [
|
||||
{
|
||||
name: '10k - monthly',
|
||||
limit: '10,000',
|
||||
price: '€20',
|
||||
productId: '47384',
|
||||
},
|
||||
{
|
||||
name: '30k - monthly',
|
||||
limit: '30,000',
|
||||
price: '€50',
|
||||
productId: '47419',
|
||||
},
|
||||
];
|
||||
|
||||
export default plans;
|
@@ -1,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;
|
@@ -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) {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import graphQLInstance from '../helpers/graphql-instance';
|
||||
import webhooksRouter from './webhooks';
|
||||
import stripeRouter from './stripe.ee';
|
||||
import paddleRouter from './paddle.ee';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/graphql', graphQLInstance);
|
||||
router.use('/webhooks', webhooksRouter);
|
||||
router.use('/stripe', stripeRouter);
|
||||
router.use('/paddle', paddleRouter);
|
||||
|
||||
export default router;
|
||||
|
19
packages/backend/src/routes/paddle.ee.ts
Normal file
19
packages/backend/src/routes/paddle.ee.ts
Normal file
@@ -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;
|
@@ -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;
|
8
packages/types/index.d.ts
vendored
8
packages/types/index.d.ts
vendored
@@ -321,6 +321,13 @@ export type IGlobalVariable = {
|
||||
setActionItem?: (actionItem: IActionItem) => void;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
</Box>
|
||||
|
||||
<TabPanel value={activeTabIndex} index={0}>
|
||||
<JSONViewer data={executionStep.dataIn} />
|
||||
<SearchableJSONViewer data={executionStep.dataIn} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTabIndex} index={1}>
|
||||
<JSONViewer data={executionStep.dataOut} />
|
||||
<SearchableJSONViewer data={executionStep.dataOut} />
|
||||
</TabPanel>
|
||||
|
||||
{hasError && (
|
||||
<TabPanel value={activeTabIndex} index={2}>
|
||||
<JSONViewer data={executionStep.errorDetails} />
|
||||
<SearchableJSONViewer data={executionStep.errorDetails} />
|
||||
</TabPanel>
|
||||
)}
|
||||
</Content>
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import { generateExternalLink } from 'helpers/translation-values';
|
||||
import usePaymentPortalUrl from 'hooks/usePaymentPortalUrl.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
export default function PaymentInformation() {
|
||||
const paymentPortal = usePaymentPortalUrl();
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageTitle
|
||||
gutterBottom
|
||||
>
|
||||
{formatMessage('billingAndUsageSettings.paymentInformation')}
|
||||
</PageTitle>
|
||||
|
||||
<Typography>
|
||||
{formatMessage(
|
||||
'billingAndUsageSettings.paymentPortalInformation',
|
||||
{ link: generateExternalLink(paymentPortal.url) })}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
108
packages/web/src/components/SearchableJSONViewer/index.tsx
Normal file
108
packages/web/src/components/SearchableJSONViewer/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import throttle from 'lodash/throttle';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import forIn from 'lodash/forIn';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import JSONViewer from 'components/JSONViewer';
|
||||
import SearchInput from 'components/SearchInput';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type JSONViewerProps = {
|
||||
data: IJSONObject;
|
||||
};
|
||||
|
||||
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<IJSONObject | null>(
|
||||
data
|
||||
);
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const onSearchChange = React.useMemo(
|
||||
() =>
|
||||
throttle((event: React.ChangeEvent) => {
|
||||
const search = (event.target as HTMLInputElement).value.toLowerCase();
|
||||
|
||||
if (!search) {
|
||||
setFilteredData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilteredData = aggregate(data, search);
|
||||
|
||||
if (isEmpty(newFilteredData)) {
|
||||
setFilteredData(null);
|
||||
} else {
|
||||
setFilteredData(newFilteredData);
|
||||
}
|
||||
}, 400),
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box my={2}>
|
||||
<SearchInput onChange={onSearchChange} />
|
||||
</Box>
|
||||
{filteredData && <JSONViewer data={filteredData} />}
|
||||
{!filteredData && (
|
||||
<Typography>{formatMessage('jsonViewer.noDataFound')}</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchableJSONViewer;
|
170
packages/web/src/components/UpgradeFreeTrial/index.ee.tsx
Normal file
170
packages/web/src/components/UpgradeFreeTrial/index.ee.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
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';
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<Card sx={{ mb: 3, p: 2 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
Upgrade your free trial
|
||||
</Typography>
|
||||
{/* <Chip label="Active" color="success" /> */}
|
||||
</Box>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
xs={12}
|
||||
spacing={1}
|
||||
sx={{ mb: 2 }}
|
||||
alignItems="stretch"
|
||||
>
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="simple table">
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell sx={{ pt: 0, pb: 2 }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||
>
|
||||
Monthly Tasks
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 0, pb: 2 }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||
>
|
||||
Price
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{plans.map((plan, index) => (
|
||||
<TableRow
|
||||
key={plan.productId}
|
||||
onClick={() => updateSelection(index)}
|
||||
sx={{
|
||||
'&:hover': { cursor: 'pointer' },
|
||||
backgroundColor: selectedIndex === index ? '#f1f3fa' : 'white',
|
||||
border: selectedIndex === index ? '2px solid #0059f7' : 'none',
|
||||
}}
|
||||
>
|
||||
<TableCell component="th" scope="row" sx={{ py: 2 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
fontWeight: selectedIndex === index ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
{plan.limit}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 2 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
fontWeight: selectedIndex === index ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
{plan.price} / month
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
Due today:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{selectedPlan.price}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ fontSize: '12px', mt: 0 }}>
|
||||
+ VAT if applicable
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={handleCheckout}
|
||||
disabled={!paddleLoaded}
|
||||
>
|
||||
<LockIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
Pay securely via Paddle
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@@ -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() {
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
size="small"
|
||||
href={usageAlert.url}
|
||||
to={usageAlert.url}
|
||||
sx={{ minWidth: 100 }}
|
||||
>
|
||||
{formatMessage('usageAlert.viewPlans')}
|
||||
|
@@ -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 (
|
||||
<React.Fragment>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell component="td" scope="row">
|
||||
Current plan
|
||||
</TableCell>
|
||||
<Card sx={{ mb: 3, p: 2 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
Subscription plan
|
||||
</Typography>
|
||||
{/* <Chip label="Active" color="success" /> */}
|
||||
</Box>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
xs={12}
|
||||
spacing={1}
|
||||
sx={{ mb: [2, 2, 8] }}
|
||||
alignItems="stretch"
|
||||
>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
backgroundColor: (theme) => theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
|
||||
Monthly quota
|
||||
</Typography>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
Free trial
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button size="small">Upgrade plan</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.name}</TableCell>
|
||||
</TableRow>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
backgroundColor: (theme) => theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
|
||||
Next bill amount
|
||||
</Typography>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="td" scope="row">
|
||||
Total allowed task count
|
||||
</TableCell>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
---
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.allowedTaskCount}</TableCell>
|
||||
</TableRow>
|
||||
<CardActions>
|
||||
{/* <Button size="small">Update billing info</Button> */}
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="td" scope="row">
|
||||
Consumed task count
|
||||
</TableCell>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
backgroundColor: (theme) => theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" sx={{ pb: 0.5 }}>
|
||||
Next bill date
|
||||
</Typography>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
---
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.consumedTaskCount}</TableCell>
|
||||
</TableRow>
|
||||
<CardActions>
|
||||
{/* <Button disabled size="small">
|
||||
monthly billing
|
||||
</Button> */}
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
Your usage
|
||||
</Typography>
|
||||
|
||||
<TableRow sx={{ 'td': { border: 0 } }}>
|
||||
<TableCell component="td" scope="row">
|
||||
Next billing date
|
||||
</TableCell>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: 'text.secondary', mt: 1 }}
|
||||
>
|
||||
Last 30 days total usage
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.nextResetAt?.toLocaleString(DateTime.DATE_FULL)}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Divider sx={{ mt: 2 }} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||
>
|
||||
Tasks
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
|
||||
>
|
||||
12300
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mt: 2 }} />
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to={URLS.SETTINGS_PLAN_UPGRADE}
|
||||
size="small"
|
||||
variant="contained"
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
72
packages/web/src/contexts/Paddle.ee.tsx
Normal file
72
packages/web/src/contexts/Paddle.ee.tsx
Normal file
@@ -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<PaddleContextParams>({
|
||||
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 (
|
||||
<PaddleContext.Provider value={value}>
|
||||
{children}
|
||||
</PaddleContext.Provider>
|
||||
);
|
||||
};
|
10
packages/web/src/graphql/queries/get-paddle-info.ee.ts
Normal file
10
packages/web/src/graphql/queries/get-paddle-info.ee.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_PADDLE_INFO = gql`
|
||||
query GetPaddleInfo {
|
||||
getPaddleInfo {
|
||||
sandbox
|
||||
vendorId
|
||||
}
|
||||
}
|
||||
`;
|
12
packages/web/src/graphql/queries/get-payment-plans.ee.ts
Normal file
12
packages/web/src/graphql/queries/get-payment-plans.ee.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_PAYMENT_PLANS = gql`
|
||||
query GetPaymentPlans {
|
||||
getPaymentPlans {
|
||||
name
|
||||
limit
|
||||
price
|
||||
productId
|
||||
}
|
||||
}
|
||||
`;
|
@@ -1,10 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_PAYMENT_PORTAL_URL = gql`
|
||||
query GetPaymentPortalUrl {
|
||||
getPaymentPortalUrl {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
14
packages/web/src/hooks/usePaddle.ee.ts
Normal file
14
packages/web/src/hooks/usePaddle.ee.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
19
packages/web/src/hooks/usePaddleInfo.ee.ts
Normal file
19
packages/web/src/hooks/usePaddleInfo.ee.ts
Normal file
@@ -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
|
||||
};
|
||||
}
|
18
packages/web/src/hooks/usePaymentPlans.ee.ts
Normal file
18
packages/web/src/hooks/usePaymentPlans.ee.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { TPaymentPlan } from '@automatisch/types';
|
||||
import { GET_PAYMENT_PLANS } from 'graphql/queries/get-payment-plans.ee';
|
||||
|
||||
type UsePaymentPlansReturn = {
|
||||
plans: TPaymentPlan[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function usePaymentPlans(): UsePaymentPlansReturn {
|
||||
const { data, loading } = useQuery(GET_PAYMENT_PLANS);
|
||||
|
||||
return {
|
||||
plans: data?.getPaymentPlans || [],
|
||||
loading
|
||||
};
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_PAYMENT_PORTAL_URL } from 'graphql/queries/get-payment-portal-url.ee';
|
||||
|
||||
type UsePaymentPortalUrlReturn = {
|
||||
url: string;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function usePaymentPortalUrl(): UsePaymentPortalUrlReturn {
|
||||
const { data, loading } = useQuery(GET_PAYMENT_PORTAL_URL);
|
||||
|
||||
return {
|
||||
url: data?.getPaymentPortalUrl?.url,
|
||||
loading
|
||||
};
|
||||
}
|
@@ -1,33 +1,34 @@
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from './useFormatMessage';
|
||||
import useUsageData from './useUsageData.ee';
|
||||
import usePaymentPortalUrl from './usePaymentPortalUrl.ee';
|
||||
|
||||
type UseUsageAlertReturn = {
|
||||
showAlert: boolean;
|
||||
hasExceededLimit?: boolean;
|
||||
alertMessage?: string;
|
||||
url?: string;
|
||||
consumptionPercentage?: number;
|
||||
showAlert: true;
|
||||
hasExceededLimit: boolean;
|
||||
alertMessage: string;
|
||||
url: string;
|
||||
consumptionPercentage: number;
|
||||
};
|
||||
|
||||
export default function useUsageAlert(): UseUsageAlertReturn {
|
||||
const { url, loading: paymentPortalUrlLoading } = usePaymentPortalUrl();
|
||||
type UseUsageNoAlertReturn = {
|
||||
showAlert: false;
|
||||
};
|
||||
|
||||
export default function useUsageAlert(): UseUsageAlertReturn | UseUsageNoAlertReturn {
|
||||
const {
|
||||
allowedTaskCount,
|
||||
consumedTaskCount,
|
||||
nextResetAt,
|
||||
loading: usageDataLoading
|
||||
loading
|
||||
} = useUsageData();
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
if (paymentPortalUrlLoading || usageDataLoading) {
|
||||
if (loading) {
|
||||
return { showAlert: false };
|
||||
}
|
||||
|
||||
const hasLoaded = !paymentPortalUrlLoading || usageDataLoading;
|
||||
const withinUsageThreshold = consumedTaskCount > allowedTaskCount * 0.7;
|
||||
const consumptionPercentage = consumedTaskCount / allowedTaskCount * 100;
|
||||
const showAlert = hasLoaded && withinUsageThreshold;
|
||||
const hasExceededLimit = consumedTaskCount >= allowedTaskCount;
|
||||
|
||||
const alertMessage = formatMessage('usageAlert.informationText', {
|
||||
@@ -37,10 +38,10 @@ export default function useUsageAlert(): UseUsageAlertReturn {
|
||||
});
|
||||
|
||||
return {
|
||||
showAlert,
|
||||
showAlert: withinUsageThreshold,
|
||||
hasExceededLimit,
|
||||
alertMessage,
|
||||
consumptionPercentage,
|
||||
url,
|
||||
url: URLS.SETTINGS_PLAN_UPGRADE,
|
||||
};
|
||||
}
|
||||
|
@@ -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(
|
||||
<ApolloProvider>
|
||||
<AutomatischInfoProvider>
|
||||
<IntlProvider>
|
||||
<ThemeProvider>
|
||||
{routes}
|
||||
<PaddleProvider>
|
||||
<ThemeProvider>
|
||||
{routes}
|
||||
|
||||
<LiveChat />
|
||||
</ThemeProvider>
|
||||
<LiveChat />
|
||||
</ThemeProvider>
|
||||
</PaddleProvider>
|
||||
</IntlProvider>
|
||||
</AutomatischInfoProvider>
|
||||
</ApolloProvider>
|
||||
|
@@ -138,5 +138,7 @@
|
||||
"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"
|
||||
}
|
||||
"usageAlert.viewPlans": "View plans",
|
||||
"jsonViewer.noDataFound": "We couldn't find anything matching your search",
|
||||
"planUpgrade.title": "Upgrade your plan"
|
||||
}
|
||||
|
@@ -2,9 +2,9 @@ import * as React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import 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 (<Navigate to={URLS.SETTINGS} replace={true} />)
|
||||
return <Navigate to={URLS.SETTINGS} replace={true} />;
|
||||
}
|
||||
|
||||
// render nothing until we know if it's cloud or not
|
||||
// here, `isCloud` is not `false`, but `undefined`
|
||||
if (!isCloud) return <React.Fragment />
|
||||
if (!isCloud) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
||||
<Grid container item xs={12} sm={9} md={8}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('billingAndUsageSettings.title')}</PageTitle>
|
||||
<PageTitle>
|
||||
{formatMessage('billingAndUsageSettings.title')}
|
||||
</PageTitle>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sx={{ mb: 6 }}>
|
||||
<UsageDataInformation />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<PaymentInformation />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
|
42
packages/web/src/pages/PlanUpgrade/index.ee.tsx
Normal file
42
packages/web/src/pages/PlanUpgrade/index.ee.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import Grid from '@mui/material/Grid';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import UpgradeFreeTrial from 'components/UpgradeFreeTrial/index.ee';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCloud from 'hooks/useCloud';
|
||||
|
||||
function PlanUpgrade() {
|
||||
const isCloud = useCloud();
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
// redirect to the initial settings page
|
||||
if (isCloud === false) {
|
||||
return <Navigate to={URLS.SETTINGS} replace={true} />;
|
||||
}
|
||||
|
||||
// render nothing until we know if it's cloud or not
|
||||
// here, `isCloud` is not `false`, but `undefined`
|
||||
if (!isCloud) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={9} md={8}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>
|
||||
{formatMessage('planUpgrade.title')}
|
||||
</PageTitle>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sx={{ mb: 6 }}>
|
||||
<UpgradeFreeTrial />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlanUpgrade;
|
@@ -2,6 +2,7 @@ import { Route, Navigate } from 'react-router-dom';
|
||||
import SettingsLayout from 'components/SettingsLayout';
|
||||
import 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 (
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.SETTINGS_PLAN_UPGRADE}
|
||||
element={
|
||||
<SettingsLayout>
|
||||
<PlanUpgrade />
|
||||
</SettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.SETTINGS}
|
||||
element={<Navigate to={URLS.SETTINGS_PROFILE} replace />}
|
||||
|
143
yarn.lock
143
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"
|
||||
|
Reference in New Issue
Block a user