Fix/alpha/int tests (#5311)
* chore: converts dynamic imports to esm require * chore: adjusts require and import usage * chore: reverts bin script change * chore: adjust dataloaded tests to use slate editor * fix: converts custom auth strategy * chore: fixes to form builder int test config * chore: adjusts plugin-stripe and int tests --------- Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
import type { Config } from 'payload/config'
|
|
||||||
import type { Configuration as WebpackConfig } from 'webpack'
|
|
||||||
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
export const extendWebpackConfig =
|
|
||||||
(config: Config): ((webpackConfig: WebpackConfig) => WebpackConfig) =>
|
|
||||||
(webpackConfig) => {
|
|
||||||
const existingWebpackConfig =
|
|
||||||
typeof config.admin?.webpack === 'function'
|
|
||||||
? config.admin.webpack(webpackConfig)
|
|
||||||
: webpackConfig
|
|
||||||
|
|
||||||
const mockModulePath = path.resolve(__dirname, './mocks/mockFile.js')
|
|
||||||
|
|
||||||
return {
|
|
||||||
...existingWebpackConfig,
|
|
||||||
resolve: {
|
|
||||||
...(existingWebpackConfig.resolve || {}),
|
|
||||||
alias: {
|
|
||||||
...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}),
|
|
||||||
'@payloadcms/plugin-stripe': path.resolve(__dirname, './admin.js'),
|
|
||||||
express: mockModulePath,
|
|
||||||
[path.resolve(__dirname, './hooks/createNewInStripe')]: mockModulePath,
|
|
||||||
[path.resolve(__dirname, './hooks/deleteFromStripe')]: mockModulePath,
|
|
||||||
[path.resolve(__dirname, './hooks/syncExistingWithStripe')]: mockModulePath,
|
|
||||||
[path.resolve(__dirname, './routes/rest')]: mockModulePath,
|
|
||||||
[path.resolve(__dirname, './routes/webhooks')]: mockModulePath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
import type { NextFunction, Response } from 'express'
|
|
||||||
import type { Config, Endpoint } from 'payload/config'
|
import type { Config, Endpoint } from 'payload/config'
|
||||||
import type { PayloadRequest } from 'payload/types'
|
|
||||||
|
|
||||||
import express from 'express'
|
import type { SanitizedStripeConfig, StripeConfig } from './types.js'
|
||||||
|
|
||||||
import type { SanitizedStripeConfig, StripeConfig } from './types'
|
import { getFields } from './fields/getFields.js'
|
||||||
|
import { createNewInStripe } from './hooks/createNewInStripe.js'
|
||||||
import { extendWebpackConfig } from './extendWebpackConfig'
|
import { deleteFromStripe } from './hooks/deleteFromStripe.js'
|
||||||
import { getFields } from './fields/getFields'
|
import { syncExistingWithStripe } from './hooks/syncExistingWithStripe.js'
|
||||||
import { createNewInStripe } from './hooks/createNewInStripe'
|
import { stripeREST } from './routes/rest.js'
|
||||||
import { deleteFromStripe } from './hooks/deleteFromStripe'
|
import { stripeWebhooks } from './routes/webhooks.js'
|
||||||
import { syncExistingWithStripe } from './hooks/syncExistingWithStripe'
|
|
||||||
import { stripeREST } from './routes/rest'
|
|
||||||
import { stripeWebhooks } from './routes/webhooks'
|
|
||||||
|
|
||||||
const stripePlugin =
|
const stripePlugin =
|
||||||
(incomingStripeConfig: StripeConfig) =>
|
(incomingStripeConfig: StripeConfig) =>
|
||||||
@@ -31,12 +26,40 @@ const stripePlugin =
|
|||||||
// unfortunately we must set the 'isTestKey' property on the config instead of using the following code:
|
// unfortunately we must set the 'isTestKey' property on the config instead of using the following code:
|
||||||
// const isTestKey = stripeConfig.stripeSecretKey?.startsWith('sk_test_');
|
// const isTestKey = stripeConfig.stripeSecretKey?.startsWith('sk_test_');
|
||||||
|
|
||||||
|
const endpoints: Endpoint[] = [
|
||||||
|
...(config?.endpoints || []),
|
||||||
|
{
|
||||||
|
handler: async (req) => {
|
||||||
|
const res = await stripeWebhooks({
|
||||||
|
config,
|
||||||
|
req,
|
||||||
|
stripeConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
path: '/stripe/webhooks',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (incomingStripeConfig?.rest) {
|
||||||
|
endpoints.push({
|
||||||
|
handler: async (req) => {
|
||||||
|
const res = await stripeREST({
|
||||||
|
req,
|
||||||
|
stripeConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
method: 'post' as Endpoint['method'],
|
||||||
|
path: '/stripe/rest',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
admin: {
|
|
||||||
...config.admin,
|
|
||||||
webpack: extendWebpackConfig(config),
|
|
||||||
},
|
|
||||||
collections: collections?.map((collection) => {
|
collections: collections?.map((collection) => {
|
||||||
const { hooks: existingHooks } = collection
|
const { hooks: existingHooks } = collection
|
||||||
|
|
||||||
@@ -86,42 +109,7 @@ const stripePlugin =
|
|||||||
|
|
||||||
return collection
|
return collection
|
||||||
}),
|
}),
|
||||||
endpoints: [
|
endpoints,
|
||||||
...(config?.endpoints || []),
|
|
||||||
{
|
|
||||||
handler: [
|
|
||||||
express.raw({ type: 'application/json' }),
|
|
||||||
async (req, res, next) => {
|
|
||||||
await stripeWebhooks({
|
|
||||||
config,
|
|
||||||
next,
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
stripeConfig,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
],
|
|
||||||
method: 'post',
|
|
||||||
path: '/stripe/webhooks',
|
|
||||||
root: true,
|
|
||||||
},
|
|
||||||
...(incomingStripeConfig?.rest
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
handler: async (req: PayloadRequest, res: Response, next: NextFunction) => {
|
|
||||||
await stripeREST({
|
|
||||||
next,
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
stripeConfig,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
method: 'post' as Endpoint['method'],
|
|
||||||
path: '/stripe/rest',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import type { Response } from 'express'
|
|
||||||
import type { PayloadRequest } from 'payload/types'
|
import type { PayloadRequest } from 'payload/types'
|
||||||
|
|
||||||
import { Forbidden } from 'payload/errors'
|
import { Forbidden } from 'payload/errors'
|
||||||
|
|
||||||
import type { StripeConfig } from '../types'
|
import type { StripeConfig } from '../types.js'
|
||||||
|
|
||||||
import { stripeProxy } from '../utilities/stripeProxy'
|
import { stripeProxy } from '../utilities/stripeProxy.js'
|
||||||
|
|
||||||
export const stripeREST = async (args: {
|
export const stripeREST = async (args: {
|
||||||
next: any
|
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
res: Response
|
|
||||||
stripeConfig: StripeConfig
|
stripeConfig: StripeConfig
|
||||||
}): Promise<any> => {
|
}): Promise<any> => {
|
||||||
const { req, res, stripeConfig } = args
|
let responseStatus = 200
|
||||||
|
let responseJSON
|
||||||
|
|
||||||
|
const { req, stripeConfig } = args
|
||||||
|
|
||||||
const {
|
const {
|
||||||
body: {
|
data: {
|
||||||
stripeArgs, // example: ['cus_MGgt3Tuj3D66f2'] or [{ limit: 100 }, { stripeAccount: 'acct_1J9Z4pKZ4Z4Z4Z4Z' }]
|
stripeArgs, // example: ['cus_MGgt3Tuj3D66f2'] or [{ limit: 100 }, { stripeAccount: 'acct_1J9Z4pKZ4Z4Z4Z4Z' }]
|
||||||
stripeMethod, // example: 'subscriptions.list',
|
stripeMethod, // example: 'subscriptions.list',
|
||||||
},
|
},
|
||||||
@@ -32,20 +32,26 @@ export const stripeREST = async (args: {
|
|||||||
throw new Forbidden(req.t)
|
throw new Forbidden(req.t)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginRes = await stripeProxy({
|
responseJSON = await stripeProxy({
|
||||||
stripeArgs,
|
stripeArgs,
|
||||||
stripeMethod,
|
stripeMethod,
|
||||||
stripeSecretKey,
|
stripeSecretKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { status } = pluginRes
|
const { status } = responseJSON
|
||||||
|
responseStatus = status
|
||||||
res.status(status).json(pluginRes)
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = `An error has occurred in the Stripe plugin REST handler: '${error}'`
|
const message = `An error has occurred in the Stripe plugin REST handler: '${JSON.stringify(
|
||||||
|
error,
|
||||||
|
)}'`
|
||||||
payload.logger.error(message)
|
payload.logger.error(message)
|
||||||
return res.status(500).json({
|
responseStatus = 500
|
||||||
|
responseJSON = {
|
||||||
message,
|
message,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Response.json(responseJSON, {
|
||||||
|
status: responseStatus,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import type { Response } from 'express'
|
|
||||||
import type { Config as PayloadConfig } from 'payload/config'
|
import type { Config as PayloadConfig } from 'payload/config'
|
||||||
import type { PayloadRequest } from 'payload/types'
|
import type { PayloadRequest } from 'payload/types'
|
||||||
|
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
|
|
||||||
import type { StripeConfig } from '../types'
|
import type { StripeConfig } from '../types.js'
|
||||||
|
|
||||||
import { handleWebhooks } from '../webhooks'
|
import { handleWebhooks } from '../webhooks/index.js'
|
||||||
|
|
||||||
export const stripeWebhooks = async (args: {
|
export const stripeWebhooks = async (args: {
|
||||||
config: PayloadConfig
|
config: PayloadConfig
|
||||||
next: any
|
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
res: Response
|
|
||||||
stripeConfig: StripeConfig
|
stripeConfig: StripeConfig
|
||||||
}): Promise<any> => {
|
}): Promise<any> => {
|
||||||
const { config, req, res, stripeConfig } = args
|
const { config, req, stripeConfig } = args
|
||||||
|
let returnStatus = 200
|
||||||
|
|
||||||
const { stripeSecretKey, stripeWebhooksEndpointSecret, webhooks } = stripeConfig
|
const { stripeSecretKey, stripeWebhooksEndpointSecret, webhooks } = stripeConfig
|
||||||
|
|
||||||
@@ -28,21 +26,21 @@ export const stripeWebhooks = async (args: {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const stripeSignature = req.headers['stripe-signature']
|
const stripeSignature = req.headers.get('stripe-signature')
|
||||||
|
|
||||||
if (stripeSignature) {
|
if (stripeSignature) {
|
||||||
let event: Stripe.Event | undefined
|
let event: Stripe.Event | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
event = stripe.webhooks.constructEvent(
|
event = stripe.webhooks.constructEvent(
|
||||||
req.body,
|
await req.text(),
|
||||||
stripeSignature,
|
stripeSignature,
|
||||||
stripeWebhooksEndpointSecret,
|
stripeWebhooksEndpointSecret,
|
||||||
)
|
)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : err
|
const msg: string = err instanceof Error ? err.message : JSON.stringify(err)
|
||||||
req.payload.logger.error(`Error constructing Stripe event: ${msg}`)
|
req.payload.logger.error(`Error constructing Stripe event: ${msg}`)
|
||||||
res.status(400)
|
returnStatus = 400
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
@@ -81,5 +79,10 @@ export const stripeWebhooks = async (args: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ received: true })
|
return Response.json(
|
||||||
|
{ received: true },
|
||||||
|
{
|
||||||
|
status: returnStatus,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,30 @@
|
|||||||
import { Strategy } from 'passport-strategy'
|
import type { AuthStrategyFunction } from 'payload/auth.js'
|
||||||
|
|
||||||
import type { Payload } from '../../../packages/payload/src/index.js'
|
|
||||||
|
|
||||||
import { buildConfigWithDefaults } from '../../buildConfigWithDefaults.js'
|
import { buildConfigWithDefaults } from '../../buildConfigWithDefaults.js'
|
||||||
import { usersSlug } from './shared.js'
|
import { usersSlug } from './shared.js'
|
||||||
|
|
||||||
export const strategyName = 'test-local'
|
export const strategyName = 'test-local'
|
||||||
|
|
||||||
export class CustomStrategy extends Strategy {
|
const customAuthenticationStrategy: AuthStrategyFunction = async ({ headers, payload }) => {
|
||||||
ctx: Payload
|
const usersQuery = await payload.find({
|
||||||
|
collection: usersSlug,
|
||||||
|
where: {
|
||||||
|
code: {
|
||||||
|
equals: headers.get('code'),
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
equals: headers.get('secret'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
constructor(ctx: Payload) {
|
const user = usersQuery.docs[0] || null
|
||||||
super()
|
if (!user) return null
|
||||||
this.ctx = ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticate(req: Request, options?: any): void {
|
return {
|
||||||
if (!req.headers.code && !req.headers.secret) {
|
...user,
|
||||||
return this.success(null)
|
_strategy: `${usersSlug}-${strategyName}`,
|
||||||
}
|
collection: usersSlug,
|
||||||
this.ctx
|
|
||||||
.find({
|
|
||||||
collection: usersSlug,
|
|
||||||
where: {
|
|
||||||
code: {
|
|
||||||
equals: req.headers.code,
|
|
||||||
},
|
|
||||||
secret: {
|
|
||||||
equals: req.headers.secret,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((users) => {
|
|
||||||
if (users.docs && users.docs.length) {
|
|
||||||
const user = users.docs[0]
|
|
||||||
user.collection = usersSlug
|
|
||||||
user._strategy = `${usersSlug}-${strategyName}`
|
|
||||||
this.success(user)
|
|
||||||
} else {
|
|
||||||
this.error(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,45 +35,45 @@ export default buildConfigWithDefaults({
|
|||||||
collections: [
|
collections: [
|
||||||
{
|
{
|
||||||
slug: usersSlug,
|
slug: usersSlug,
|
||||||
|
access: {
|
||||||
|
create: () => true,
|
||||||
|
},
|
||||||
auth: {
|
auth: {
|
||||||
disableLocalStrategy: true,
|
disableLocalStrategy: true,
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
name: strategyName,
|
name: strategyName,
|
||||||
strategy: (ctx) => new CustomStrategy(ctx),
|
authenticate: customAuthenticationStrategy,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
access: {
|
|
||||||
create: () => true,
|
|
||||||
},
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'code',
|
name: 'code',
|
||||||
label: 'Code',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
unique: true,
|
|
||||||
index: true,
|
index: true,
|
||||||
|
label: 'Code',
|
||||||
|
unique: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'secret',
|
name: 'secret',
|
||||||
label: 'Secret',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
label: 'Secret',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
label: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'roles',
|
name: 'roles',
|
||||||
label: 'Role',
|
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: ['admin', 'editor', 'moderator', 'user', 'viewer'],
|
|
||||||
defaultValue: 'user',
|
defaultValue: 'user',
|
||||||
|
hasMany: true,
|
||||||
|
label: 'Role',
|
||||||
|
options: ['admin', 'editor', 'moderator', 'user', 'viewer'],
|
||||||
required: true,
|
required: true,
|
||||||
saveToJWT: true,
|
saveToJWT: true,
|
||||||
hasMany: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
UploadFeature,
|
UploadFeature,
|
||||||
lexicalEditor,
|
lexicalEditor,
|
||||||
} from '../packages/richtext-lexical/src/index.js'
|
} from '../packages/richtext-lexical/src/index.js'
|
||||||
//import { slateEditor } from '../packages/richtext-slate/src/index.js'
|
// import { slateEditor } from '../packages/richtext-slate/src/index.js'
|
||||||
// process.env.PAYLOAD_DATABASE = 'postgres'
|
// process.env.PAYLOAD_DATABASE = 'postgres'
|
||||||
|
|
||||||
const databaseAdapters = {
|
const databaseAdapters = {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { slateEditor } from '../../packages/richtext-slate/src/index.js'
|
||||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
import { devUser } from '../credentials.js'
|
import { devUser } from '../credentials.js'
|
||||||
|
|
||||||
@@ -14,19 +15,15 @@ export default buildConfigWithDefaults({
|
|||||||
{
|
{
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationTo: 'users',
|
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeChange: [({ req: { user } }) => user?.id],
|
beforeChange: [({ req: { user } }) => user?.id],
|
||||||
},
|
},
|
||||||
|
relationTo: 'users',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'relation-a',
|
slug: 'relation-a',
|
||||||
labels: {
|
|
||||||
singular: 'Relation A',
|
|
||||||
plural: 'Relation As',
|
|
||||||
},
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relationship',
|
name: 'relationship',
|
||||||
@@ -36,15 +33,16 @@ export default buildConfigWithDefaults({
|
|||||||
{
|
{
|
||||||
name: 'richText',
|
name: 'richText',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
editor: slateEditor({}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
labels: {
|
||||||
|
plural: 'Relation As',
|
||||||
|
singular: 'Relation A',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'relation-b',
|
slug: 'relation-b',
|
||||||
labels: {
|
|
||||||
singular: 'Relation B',
|
|
||||||
plural: 'Relation Bs',
|
|
||||||
},
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relationship',
|
name: 'relationship',
|
||||||
@@ -54,8 +52,13 @@ export default buildConfigWithDefaults({
|
|||||||
{
|
{
|
||||||
name: 'richText',
|
name: 'richText',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
editor: slateEditor({}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
labels: {
|
||||||
|
plural: 'Relation Bs',
|
||||||
|
singular: 'Relation B',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
@@ -68,9 +71,9 @@ export default buildConfigWithDefaults({
|
|||||||
})
|
})
|
||||||
|
|
||||||
await payload.create({
|
await payload.create({
|
||||||
user,
|
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
data: postDoc,
|
data: postDoc,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Block } from '../../packages/payload/src/fields/config/types.js'
|
import type { Block } from '../../packages/payload/src/fields/config/types.js'
|
||||||
|
|
||||||
import formBuilder, { fields as formFields } from '../../packages/plugin-form-builder/src/index.js'
|
import formBuilder, { fields as formFields } from '../../packages/plugin-form-builder/src/index.js'
|
||||||
|
import { slateEditor } from '../../packages/richtext-slate/src/index.js'
|
||||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
import { devUser } from '../credentials.js'
|
import { devUser } from '../credentials.js'
|
||||||
import { Pages } from './collections/Pages.js'
|
import { Pages } from './collections/Pages.js'
|
||||||
@@ -9,20 +10,21 @@ import { seed } from './seed/index.js'
|
|||||||
|
|
||||||
const colorField: Block = {
|
const colorField: Block = {
|
||||||
slug: 'color',
|
slug: 'color',
|
||||||
labels: {
|
|
||||||
singular: 'Color',
|
|
||||||
plural: 'Colors',
|
|
||||||
},
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'value',
|
name: 'value',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
labels: {
|
||||||
|
plural: 'Colors',
|
||||||
|
singular: 'Color',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
collections: [Pages, Users],
|
collections: [Pages, Users],
|
||||||
|
editor: slateEditor({}),
|
||||||
localization: {
|
localization: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
fallback: true,
|
fallback: true,
|
||||||
@@ -43,27 +45,14 @@ export default buildConfigWithDefaults({
|
|||||||
formBuilder({
|
formBuilder({
|
||||||
// handlePayment: handleFormPayments,
|
// handlePayment: handleFormPayments,
|
||||||
// beforeEmail: prepareFormEmails,
|
// beforeEmail: prepareFormEmails,
|
||||||
redirectRelationships: ['pages'],
|
|
||||||
formOverrides: {
|
|
||||||
// labels: {
|
|
||||||
// singular: 'Contact Form',
|
|
||||||
// plural: 'Contact Forms'
|
|
||||||
// },
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'custom',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
fields: {
|
fields: {
|
||||||
payment: true,
|
|
||||||
colorField,
|
colorField,
|
||||||
|
payment: true,
|
||||||
text: {
|
text: {
|
||||||
...formFields.text,
|
...formFields.text,
|
||||||
labels: {
|
labels: {
|
||||||
singular: 'Custom Text Field',
|
|
||||||
plural: 'Custom Text Fields',
|
plural: 'Custom Text Fields',
|
||||||
|
singular: 'Custom Text Field',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// payment: {
|
// payment: {
|
||||||
@@ -78,6 +67,19 @@ export default buildConfigWithDefaults({
|
|||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
|
formOverrides: {
|
||||||
|
// labels: {
|
||||||
|
// singular: 'Contact Form',
|
||||||
|
// plural: 'Contact Forms'
|
||||||
|
// },
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'custom',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
redirectRelationships: ['pages'],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
payload = await getPayload({ config })
|
payload = await getPayload({ config })
|
||||||
|
|
||||||
const formConfig: Omit<Form, 'createdAt' | 'id' | 'updatedAt'> = {
|
const formConfig: Omit<Form, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||||
title: 'Test Form',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
blockType: 'text',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
confirmationMessage: [
|
confirmationMessage: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Confirmed.',
|
text: 'Confirmed.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
blockType: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: 'Test Form',
|
||||||
}
|
}
|
||||||
|
|
||||||
form = (await payload.create({
|
form = (await payload.create({
|
||||||
@@ -54,19 +54,19 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
describe('form building', () => {
|
describe('form building', () => {
|
||||||
it('can create a simple form', async () => {
|
it('can create a simple form', async () => {
|
||||||
const formConfig: Omit<Form, 'createdAt' | 'id' | 'updatedAt'> = {
|
const formConfig: Omit<Form, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||||
title: 'Test Form',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
blockType: 'text',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
confirmationMessage: [
|
confirmationMessage: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Confirmed.',
|
text: 'Confirmed.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
blockType: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: 'Test Form',
|
||||||
}
|
}
|
||||||
|
|
||||||
const testForm = await payload.create({
|
const testForm = await payload.create({
|
||||||
@@ -81,14 +81,14 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
|
|
||||||
it('can use form overrides', async () => {
|
it('can use form overrides', async () => {
|
||||||
const formConfig: Omit<Form, 'createdAt' | 'id' | 'updatedAt'> = {
|
const formConfig: Omit<Form, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||||
custom: 'custom',
|
|
||||||
title: 'Test Form',
|
|
||||||
confirmationMessage: [
|
confirmationMessage: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Confirmed.',
|
text: 'Confirmed.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
custom: 'custom',
|
||||||
|
title: 'Test Form',
|
||||||
}
|
}
|
||||||
|
|
||||||
const testForm = await payload.create({
|
const testForm = await payload.create({
|
||||||
@@ -142,7 +142,7 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
await expect(req).rejects.toThrow(ValidationError)
|
await expect(req).rejects.toThrow(ValidationError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replaces curly braces with data when using slate serializer', async () => {
|
it('replaces curly braces with data when using slate serializer', () => {
|
||||||
const mockName = 'Test Submission'
|
const mockName = 'Test Submission'
|
||||||
const mockEmail = 'dev@payloadcms.com'
|
const mockEmail = 'dev@payloadcms.com'
|
||||||
|
|
||||||
@@ -150,12 +150,12 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
[
|
[
|
||||||
{ text: 'Welcome {{name}}. Here is a dynamic ' },
|
{ text: 'Welcome {{name}}. Here is a dynamic ' },
|
||||||
{
|
{
|
||||||
|
type: 'link',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'link',
|
text: 'link',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
type: 'link',
|
|
||||||
url: 'www.test.com?email={{email}}',
|
url: 'www.test.com?email={{email}}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -177,24 +177,17 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
{
|
{
|
||||||
root: {
|
root: {
|
||||||
type: 'root',
|
type: 'root',
|
||||||
format: '',
|
|
||||||
indent: 0,
|
|
||||||
version: 1,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
direction: 'ltr',
|
|
||||||
format: '',
|
|
||||||
indent: 0,
|
|
||||||
version: 1,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
type: 'text',
|
||||||
detail: 0,
|
detail: 0,
|
||||||
format: 0,
|
format: 0,
|
||||||
mode: 'normal',
|
mode: 'normal',
|
||||||
style: '',
|
style: '',
|
||||||
text: 'Name: {{name}}',
|
text: 'Name: {{name}}',
|
||||||
type: 'text',
|
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -202,18 +195,25 @@ describe('@payloadcms/plugin-form-builder', () => {
|
|||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: 'text',
|
||||||
detail: 0,
|
detail: 0,
|
||||||
format: 0,
|
format: 0,
|
||||||
mode: 'normal',
|
mode: 'normal',
|
||||||
style: '',
|
style: '',
|
||||||
text: 'Email: {{email}}',
|
text: 'Email: {{email}}',
|
||||||
type: 'text',
|
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { customersSlug } from '../shared.js'
|
|||||||
|
|
||||||
export const Customers: CollectionConfig = {
|
export const Customers: CollectionConfig = {
|
||||||
slug: customersSlug,
|
slug: customersSlug,
|
||||||
timestamps: true,
|
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'email',
|
|
||||||
defaultColumns: ['email', 'name'],
|
defaultColumns: ['email', 'name'],
|
||||||
|
useAsTitle: 'email',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
useAPIKey: true,
|
useAPIKey: true,
|
||||||
@@ -16,12 +15,11 @@ export const Customers: CollectionConfig = {
|
|||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
label: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'subscriptions',
|
name: 'subscriptions',
|
||||||
label: 'Subscriptions',
|
|
||||||
type: 'array',
|
type: 'array',
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
@@ -30,7 +28,6 @@ export const Customers: CollectionConfig = {
|
|||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'link',
|
name: 'link',
|
||||||
label: 'Link',
|
|
||||||
type: 'ui',
|
type: 'ui',
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
@@ -38,43 +35,44 @@ export const Customers: CollectionConfig = {
|
|||||||
LinkToDoc({
|
LinkToDoc({
|
||||||
...args,
|
...args,
|
||||||
isTestKey: process.env.PAYLOAD_PUBLIC_IS_STRIPE_TEST_KEY === 'true',
|
isTestKey: process.env.PAYLOAD_PUBLIC_IS_STRIPE_TEST_KEY === 'true',
|
||||||
stripeResourceType: 'subscriptions',
|
|
||||||
nameOfIDField: `${args.path}.stripeSubscriptionID`,
|
nameOfIDField: `${args.path}.stripeSubscriptionID`,
|
||||||
|
stripeResourceType: 'subscriptions',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
label: 'Link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stripeSubscriptionID',
|
name: 'stripeSubscriptionID',
|
||||||
label: 'Stripe ID',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
admin: {
|
admin: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
|
label: 'Stripe ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stripeProductID',
|
name: 'stripeProductID',
|
||||||
label: 'Product ID',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
admin: {
|
admin: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
|
label: 'Product ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'product',
|
name: 'product',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationTo: 'products',
|
|
||||||
admin: {
|
admin: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
|
relationTo: 'products',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
label: 'Status',
|
|
||||||
type: 'select',
|
type: 'select',
|
||||||
admin: {
|
admin: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
|
label: 'Status',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Active',
|
label: 'Active',
|
||||||
@@ -107,6 +105,8 @@ export const Customers: CollectionConfig = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
label: 'Subscriptions',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
timestamps: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,36 @@ import { productsSlug } from '../shared.js'
|
|||||||
|
|
||||||
export const Products: CollectionConfig = {
|
export const Products: CollectionConfig = {
|
||||||
slug: productsSlug,
|
slug: productsSlug,
|
||||||
timestamps: true,
|
|
||||||
admin: {
|
admin: {
|
||||||
defaultColumns: ['name'],
|
defaultColumns: ['name'],
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
label: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'price',
|
name: 'price',
|
||||||
label: 'Price',
|
|
||||||
type: 'group',
|
type: 'group',
|
||||||
admin: {
|
admin: {
|
||||||
readOnly: true,
|
|
||||||
description: 'All pricing information is managed in Stripe and will be reflected here.',
|
description: 'All pricing information is managed in Stripe and will be reflected here.',
|
||||||
|
readOnly: true,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'stripePriceID',
|
name: 'stripePriceID',
|
||||||
label: 'Stripe Price ID',
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
label: 'Stripe Price ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stripeJSON',
|
name: 'stripeJSON',
|
||||||
label: 'Stripe JSON',
|
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
label: 'Stripe JSON',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
label: 'Price',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
timestamps: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections
|
|||||||
|
|
||||||
export const Users: CollectionConfig = {
|
export const Users: CollectionConfig = {
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
auth: true,
|
|
||||||
admin: {
|
|
||||||
useAsTitle: 'email',
|
|
||||||
},
|
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
auth: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
stripePlugin({
|
stripePlugin({
|
||||||
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
|
||||||
isTestKey: true,
|
isTestKey: true,
|
||||||
logs: true,
|
logs: true,
|
||||||
|
rest: false,
|
||||||
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||||||
|
stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET,
|
||||||
sync: [
|
sync: [
|
||||||
{
|
{
|
||||||
collection: 'customers',
|
collection: 'customers',
|
||||||
stripeResourceType: 'customers',
|
|
||||||
stripeResourceTypeSingular: 'customer',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldPath: 'name',
|
fieldPath: 'name',
|
||||||
@@ -57,11 +57,11 @@ export default buildConfigWithDefaults({
|
|||||||
// property: 'plan.name',
|
// property: 'plan.name',
|
||||||
// }
|
// }
|
||||||
],
|
],
|
||||||
|
stripeResourceType: 'customers',
|
||||||
|
stripeResourceTypeSingular: 'customer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
stripeResourceType: 'products',
|
|
||||||
stripeResourceTypeSingular: 'product',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldPath: 'name',
|
fieldPath: 'name',
|
||||||
@@ -72,17 +72,17 @@ export default buildConfigWithDefaults({
|
|||||||
stripeProperty: 'default_price',
|
stripeProperty: 'default_price',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
stripeResourceType: 'products',
|
||||||
|
stripeResourceTypeSingular: 'product',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rest: false,
|
|
||||||
webhooks: {
|
webhooks: {
|
||||||
'customer.subscription.created': subscriptionCreatedOrUpdated,
|
'customer.subscription.created': subscriptionCreatedOrUpdated,
|
||||||
'customer.subscription.updated': subscriptionCreatedOrUpdated,
|
|
||||||
'customer.subscription.deleted': subscriptionDeleted,
|
'customer.subscription.deleted': subscriptionDeleted,
|
||||||
|
'customer.subscription.updated': subscriptionCreatedOrUpdated,
|
||||||
'product.created': syncPriceJSON,
|
'product.created': syncPriceJSON,
|
||||||
'product.updated': syncPriceJSON,
|
'product.updated': syncPriceJSON,
|
||||||
},
|
},
|
||||||
stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET,
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
raw: () => {},
|
|
||||||
url: () => {},
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = {}
|
|
||||||
@@ -72,28 +72,28 @@ export const subscriptionCreatedOrUpdated = async (args) => {
|
|||||||
payload.logger.info(`- Subscription already exists, now updating.`)
|
payload.logger.info(`- Subscription already exists, now updating.`)
|
||||||
// update existing subscription
|
// update existing subscription
|
||||||
subscriptions[indexOfSubscription] = {
|
subscriptions[indexOfSubscription] = {
|
||||||
stripeProductID: plan.product,
|
|
||||||
product: payloadProductID,
|
product: payloadProductID,
|
||||||
status: subscriptionStatus,
|
status: subscriptionStatus,
|
||||||
|
stripeProductID: plan.product,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload.logger.info(`- This is a new subscription, now adding.`)
|
payload.logger.info(`- This is a new subscription, now adding.`)
|
||||||
// create new subscription
|
// create new subscription
|
||||||
subscriptions.push({
|
subscriptions.push({
|
||||||
stripeSubscriptionID: eventID,
|
|
||||||
stripeProductID: plan.product,
|
|
||||||
product: payloadProductID,
|
product: payloadProductID,
|
||||||
status: subscriptionStatus,
|
status: subscriptionStatus,
|
||||||
|
stripeProductID: plan.product,
|
||||||
|
stripeSubscriptionID: eventID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: 'customers',
|
|
||||||
id: foundCustomer.id,
|
id: foundCustomer.id,
|
||||||
|
collection: 'customers',
|
||||||
data: {
|
data: {
|
||||||
subscriptions,
|
|
||||||
skipSync: true,
|
skipSync: true,
|
||||||
|
subscriptions,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ export const subscriptionDeleted = async (args) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: 'customers',
|
|
||||||
id: foundCustomer.id,
|
id: foundCustomer.id,
|
||||||
|
collection: 'customers',
|
||||||
data: {
|
data: {
|
||||||
subscriptions,
|
|
||||||
skipSync: true,
|
skipSync: true,
|
||||||
|
subscriptions,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export const syncPriceJSON = async (args) => {
|
|||||||
const stripePrice = await stripe.prices.retrieve(default_price)
|
const stripePrice = await stripe.prices.retrieve(default_price)
|
||||||
|
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: 'products',
|
|
||||||
id: payloadProductID,
|
id: payloadProductID,
|
||||||
|
collection: 'products',
|
||||||
data: {
|
data: {
|
||||||
price: {
|
price: {
|
||||||
stripeJSON: JSON.stringify(stripePrice),
|
stripeJSON: JSON.stringify(stripePrice),
|
||||||
|
|||||||
Reference in New Issue
Block a user