feat: ecommerce plugin and template (#8297)

This PR adds an ecommerce plugin package with both a Payload plugin and
React UI utilities for the frontend. It also adds a new ecommerce
template and new ecommerce test suite.

It also makes a change to the `cpa` package to accept a `--version` flag
to install a specific version of Payload defaulting to the latest.
This commit is contained in:
Paul
2025-09-30 01:05:16 +01:00
committed by GitHub
parent 92a5f075b6
commit ef4874b9a0
372 changed files with 30874 additions and 331 deletions

View File

@@ -61,9 +61,15 @@ When validating Payload-generated JWT tokens in external services, use the proce
```ts
import crypto from 'node:crypto'
const secret = crypto.createHash('sha256').update(process.env.PAYLOAD_SECRET).digest('hex').slice(0, 32)
const secret = crypto
.createHash('sha256')
.update(process.env.PAYLOAD_SECRET)
.digest('hex')
.slice(0, 32)
```
<Banner type="info">
**Note:** Payload processes your secret using SHA-256 hash and takes the first 32 characters. This processed value is what's used for JWT operations, not your original secret.
**Note:** Payload processes your secret using SHA-256 hash and takes the first
32 characters. This processed value is what's used for JWT operations, not
your original secret.
</Banner>

446
docs/ecommerce/advanced.mdx Normal file
View File

@@ -0,0 +1,446 @@
---
title: Advanced uses and examples
label: Advanced
order: 60
desc: Add ecommerce functionality to your Payload CMS application with this plugin.
keywords: plugins, ecommerce, stripe, plugin, payload, cms, shop, payments
---
The plugin also exposes its internal utilities so that you can use only the parts that you need without using the entire plugin. This is useful if you want to build your own ecommerce solution on top of Payload.
## Using only the collections
You can import the collections directly from the plugin and add them to your Payload configuration. This way, you can use the collections without using the entire plugin:
| Name | Collection | Description |
| -------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `createAddressesCollection` | `addresses` | Used for customer addresses (like shipping and billing). [More](#createAddressesCollection) |
| `createCartsCollection` | `carts` | Carts can be used by customers, guests and once purchased are kept for records and analytics. [More](#createCartsCollection) |
| `createOrdersCollection` | `orders` | Orders are used to store customer-side information and are related to at least one transaction. [More](#createOrdersCollection) |
| `createTransactionsCollection` | `transactions` | Handles payment information accessable by admins only, related to Orders. [More](#createTransactionsCollection) |
| `createProductsCollection` | `products` | All the product information lives here, contains prices, relations to Variant Types and joins to Variants. [More](#createProductsCollection) |
| `createVariantsCollection` | `variants` | Product variants, unique purchasable items that are linked to a product and Variant Options. [More](#createVariantsCollection) |
| `createVariantTypesCollection` | `variantTypes` | A taxonomy used by Products to relate Variant Options together. An example of a Variant Type is "size". [More](#createVariantTypesCollection) |
| `createVariantOptionsCollection` | `variantOptions` | Related to a Variant Type to handle a unique property of it. An example of a Variant Option is "small". [More](#createVariantOptionsCollection) |
### createAddressesCollection
Use this to create the `addresses` collection. This collection is used to store customer addresses. It takes the following properties:
| Property | Type | Description |
| -------------------- | --------------- | --------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `addressFields` | `Field[]` | Custom fields to add to the address. |
| `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. |
| `supportedCountries` | `CountryType[]` | (Optional) List of supported countries. Defaults to all countries. |
The access object can contain the following properties:
| Property | Type | Description |
| ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adminOrCustomerOwner` | `Access` | Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Used to limit read, update or delete to only the customers that own this address. |
| `authenticatedOnly` | `Access` | Access control to check if the user is authenticated. Use on the `create` access to allow any customer to create a new address. |
| `customerOnlyFieldAccess` | `FieldAccess` | Field level access control to check if the user has `customer` permissions. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createAddressesCollection } from 'payload-plugin-ecommerce'
const Addresses = createAddressesCollection({
access: {
adminOrCustomerOwner,
authenticatedOnly,
customerOnlyFieldAccess,
},
addressFields: [
{
name: 'company',
type: 'text',
label: 'Company',
},
],
})
```
### createCartsCollection
Use this to create the `carts` collection to store customer carts. It takes the following properties:
| Property | Type | Description |
| ------------------ | ------------------ | ----------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. |
| `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. |
| `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. |
| `enableVariants` | `boolean` | (Optional) Whether to enable variants in the cart. Defaults to `true`. |
| `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable a subtotal to be tracked. |
The access object can contain the following properties:
| Property | Type | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adminOrCustomerOwner` | `Access` | Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Used to limit read, update or delete to only the customers that own this cart. |
| `publicAccess` | `Access` | Allow anyone to create a new cart, useful for guests. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createCartsCollection } from 'payload-plugin-ecommerce'
const Carts = createCartsCollection({
access: {
adminOrCustomerOwner,
publicAccess,
},
enableVariants: true,
currenciesConfig: {
defaultCurrency: 'usd',
currencies: [
{
code: 'usd',
symbol: '$',
},
{
code: 'eur',
symbol: '€',
},
],
},
})
```
### createOrdersCollection
Use this to create the `orders` collection to store customer orders. It takes the following properties:
| Property | Type | Description |
| ------------------ | ------------------ | --------------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. |
| `transactionsSlug` | `string` | (Optional) Slug of the transactions collection. Defaults to `transactions`. |
| `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. |
| `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. |
| `enableVariants` | `boolean` | (Optional) Whether to enable variants in the order. Defaults to `true`. |
| `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable the amount to be tracked. |
| `addressFields` | `Field[]` | (Optional) The fields to be used for the shipping address. |
The access object can contain the following properties:
| Property | Type | Description |
| ---------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adminOrCustomerOwner` | `Access` | Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Used to limit read to only the customers that own this order. |
| `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit create, update and delete access to only admins. |
| `adminOnlyFieldAccess` | `FieldAccess` | Field level access control to check if the user has `admin` permissions. Limits the transaction ID field to admins only. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createOrdersCollection } from 'payload-plugin-ecommerce'
const Orders = createOrdersCollection({
access: {
adminOrCustomerOwner,
adminOnly,
adminOnlyFieldAccess,
},
enableVariants: true,
currenciesConfig: {
defaultCurrency: 'usd',
currencies: [
{
code: 'usd',
symbol: '$',
},
{
code: 'eur',
symbol: '€',
},
],
},
addressFields: [
{
name: 'deliveryInstructions',
type: 'text',
label: 'Delivery Instructions',
},
],
})
```
### createTransactionsCollection
Use this to create the `transactions` collection to store payment transactions. It takes the following properties:
| Property | Type | Description |
| ------------------ | ------------------ | ----------------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. |
| `cartsSlug` | `string` | (Optional) Slug of the carts collection. Defaults to `carts`. |
| `ordersSlug` | `string` | (Optional) Slug of the orders collection. Defaults to `orders`. |
| `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. |
| `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. |
| `enableVariants` | `boolean` | (Optional) Whether to enable variants in the transaction. Defaults to `true`. |
| `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable the amount to be tracked. |
| `addressFields` | `Field[]` | (Optional) The fields to be used for the billing address. |
| `paymentMethods` | `PaymentAdapter[]` | (Optional) The payment methods to be used for the transaction. |
The access object can contain the following properties:
| Property | Type | Description |
| ----------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createTransactionsCollection } from 'payload-plugin-ecommerce'
const Transactions = createTransactionsCollection({
access: {
adminOnly,
},
enableVariants: true,
currenciesConfig: {
defaultCurrency: 'usd',
currencies: [
{
code: 'usd',
symbol: '$',
},
{
code: 'eur',
symbol: '€',
},
],
},
addressFields: [
{
name: 'billingInstructions',
type: 'text',
label: 'Billing Instructions',
},
],
paymentMethods: [
// Add your payment adapters here
],
})
```
### createProductsCollection
Use this to create the `products` collection to store products. It takes the following properties:
| Property | Type | Description |
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. |
| `variantTypesSlug` | `string` | (Optional) Slug of the variant types collection. Defaults to `variantTypes`. |
| `enableVariants` | `boolean` | (Optional) Whether to enable variants on products. Defaults to `true`. |
| `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable price fields. |
| `inventory` | `boolean` `InventoryConfig` | (Optional) Inventory configuration to enable stock tracking. Defaults to `true`. |
The access object can contain the following properties:
| Property | Type | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit create, update or delete to only admins. |
| `adminOrPublishedStatus` | `Access` | Access control to check if the user has `admin` permissions or if the product has a `published` status. Used to limit read access to published products for non-admins. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createProductsCollection } from 'payload-plugin-ecommerce'
const Products = createProductsCollection({
access: {
adminOnly,
adminOrPublishedStatus,
},
enableVariants: true,
currenciesConfig: {
defaultCurrency: 'usd',
currencies: [
{
code: 'usd',
symbol: '$',
},
{
code: 'eur',
symbol: '€',
},
],
},
inventory: {
enabled: true,
trackByVariant: true,
lowStockThreshold: 5,
},
})
```
### createVariantsCollection
Use this to create the `variants` collection to store product variants. It takes the following properties:
| Property | Type | Description |
| -------------------- | --------------------------- | -------------------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. |
| `variantOptionsSlug` | `string` | (Optional) Slug of the variant options collection. Defaults to `variantOptions`. |
| `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable price fields. |
| `inventory` | `boolean` `InventoryConfig` | (Optional) Inventory configuration to enable stock tracking. Defaults to `true`. |
The access object can contain the following properties:
| Property | Type | Description |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. |
| `adminOrPublishedStatus` | `Access` | Access control to check if the user has `admin` permissions or if the related product has a `published` status. Used to limit read access to variants of published products for non-admins. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createVariantsCollection } from 'payload-plugin-ecommerce'
const Variants = createVariantsCollection({
access: {
adminOnly,
adminOrPublishedStatus,
},
currenciesConfig: {
defaultCurrency: 'usd',
currencies: [
{
code: 'usd',
symbol: '$',
},
{
code: 'eur',
symbol: '€',
},
],
},
inventory: {
enabled: true,
lowStockThreshold: 5,
},
})
```
### createVariantTypesCollection
Use this to create the `variantTypes` collection to store variant types. It takes the following properties:
| Property | Type | Description |
| -------------------- | -------- | -------------------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `variantOptionsSlug` | `string` | (Optional) Slug of the variant options collection. Defaults to `variantOptions`. |
The access object can contain the following properties:
| Property | Type | Description |
| -------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. |
| `publicAccess` | `Access` | Allow anyone to read variant types. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createVariantTypesCollection } from 'payload-plugin-ecommerce'
const VariantTypes = createVariantTypesCollection({
access: {
adminOnly,
publicAccess,
},
})
```
### createVariantOptionsCollection
Use this to create the `variantOptions` collection to store variant options. It takes the following properties:
| Property | Type | Description |
| ------------------ | -------- | ---------------------------------------------------------------------------- |
| `access` | `object` | Access control for the collection. |
| `variantTypesSlug` | `string` | (Optional) Slug of the variant types collection. Defaults to `variantTypes`. |
The access object can contain the following properties:
| Property | Type | Description |
| -------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. |
| `publicAccess` | `Access` | Allow anyone to read variant options. |
See the [access control section](./plugin#access) for more details on each of these functions.
Example usage:
```ts
import { createVariantOptionsCollection } from 'payload-plugin-ecommerce'
const VariantOptions = createVariantOptionsCollection({
access: {
adminOnly,
publicAccess,
},
})
```
## Typescript
There are several common types that you'll come across when working with this package. These are export from the package as well and are used across individual utilities as well.
### CurrenciesConfig
Defines the supported currencies in Payload and the frontend. It has the following properties:
| Property | Type | Description |
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
| `defaultCurrency` | `string` | The default currency code. Must match one of the codes in the `currencies` array. |
| `currencies` | `CurrencyType[]` | An array of supported currencies. Each currency must have a unique code. |
### Currency
Defines a currency to be used in the application. It has the following properties:
| Property | Type | Description |
| ---------- | -------- | ------------------------------------------------ |
| `code` | `string` | The ISO 4217 currency code. Example `'usd'`. |
| `symbol` | `string` | The symbol of the currency. Example `'$'` |
| `label` | `string` | The name of the currency. Example `'USD'` |
| `decimals` | `number` | The number of decimal places to use. Example `2` |
The decimals is very important to provide as we store all prices as integers to avoid floating point issues. For example, if you're using USD, you would store a price of $10.00 as `1000` (10 \* 10^2), so when formatting the price for display we need to know how many decimal places the currency supports.
### CountryType
Used to define a country in address fields and supported countries lists. It has the following properties:
| Property | Type | Description |
| -------- | -------- | ---------------------------- |
| `value` | `string` | The ISO 3166-1 alpha-2 code. |
| `label` | `string` | The name of the country. |
### InventoryConfig
It's used to customise the inventory tracking settings on products and variants. It has the following properties:
| Property | Type | Description |
| ----------- | -------- | ------------------------------------------------------------------------------------ |
| `fieldName` | `string` | (Optional) The name of the field to use for tracking stock. Defaults to `inventory`. |

270
docs/ecommerce/frontend.mdx Normal file
View File

@@ -0,0 +1,270 @@
---
title: Ecommerce Frontend
label: Frontend
order: 30
desc: Add ecommerce functionality to your Payload CMS application with this plugin.
keywords: plugins, ecommerce, stripe, plugin, payload, cms, shop, payments
---
The package provides a set of React utilities to help you manage your ecommerce frontend. These include context providers, hooks, and components to handle carts, products, and payments.
The following hooks and components are available:
| Hook / Component | Description |
| ------------------- | ------------------------------------------------------------------------------ |
| `EcommerceProvider` | A context provider to wrap your application and provide the ecommerce context. |
| `useCart` | A hook to manage the cart state and actions. |
| `useAddresses` | A hook to fetch and manage products. |
| `usePayments` | A hook to manage the checkout process. |
| `useCurrency` | A hook to format prices based on the selected currency. |
| `useEcommerce` | A hook that encompasses all of the above in one. |
### EcommerceProvider
The `EcommerceProvider` component is used to wrap your application and provide the ecommerce context. It takes the following props:
| Prop | Type | Description |
| ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------- |
| `addressesSlug` | `string` | The slug of the addresses collection. Defaults to `addresses`. |
| `api` | `object` | API configuration for the internal fetches of the provider. [More](#api) |
| `cartsSlug` | `string` | The slug of the carts collection. Defaults to `carts`. |
| `children` | `ReactNode` | The child components that will have access to the ecommerce context. |
| `currenciesConfig` | `CurrenciesConfig` | Configuration for supported currencies. See [Currencies](./plugin#currencies). |
| `customersSlug` | `string` | The slug of the customers collection. Defaults to `users`. |
| `debug` | `boolean` | Enable or disable debug mode. This will send more information to the console. |
| `enableVariants` | `boolean` | Enable or disable product variants support. Defaults to `true`. |
| `paymentMethods` | `PaymentMethod[]` | An array of payment method adapters for the client side. See [Payment adapters](./plugin#payment-adapters). |
| `syncLocalStorage` | `boolean` `object` | Whether to sync the cart ID to local storage. Defaults to `true`. Takes an object for configuration |
Example usage:
```tsx
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/react'
// Import any payment adapters you want to use on the client side
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
import { USD, EUR } from '@payloadcms/plugin-ecommerce/currencies'
export const Providers = () => (
<EcommerceProvider
enableVariants={true}
currenciesConfig={{
supportedCurrencies: [USD, EUR],
defaultCurrency: 'USD',
}}
>
{children}
</EcommerceProvider>
)
```
#### api
The `api` prop is used to configure the API settings for the internal fetches of the provider. It takes an object with the following properties:
| Property | Type | Description |
| ----------------- | -------- | ----------------------------------------------------------------- |
| `apiRoute` | `string` | The base route for accessing the Payload API. Defaults to `/api`. |
| `serverURL` | `string` | The full URL of your Payload server. |
| `cartsFetchQuery` | `object` | Additional query parameters to include when fetching the cart. |
#### cartsFetchQuery
The `cartsFetchQuery` property allows you to specify additional query parameters to include when fetching the cart. This can be useful for including related data or customizing the response. This accepts:
| Property | Type | Description |
| ---------- | -------------- | --------------------------------------------------------------- |
| `depth` | `string` | Defaults to 0. [See Depth](../queries/depth) |
| `select` | `SelectType` | Select parameters. [See Select](../queries/select) |
| `populate` | `PopulateType` | Populate parameters. [See Populate](../queries/select#populate) |
Example usage:
```tsx
<EcommerceProvider
api={{
cartsFetchQuery: {
depth: 2, // Include related data up to 2 levels deep
},
}}
>
{children}
</EcommerceProvider>
```
#### syncLocalStorage
The `syncLocalStorage` prop is used to enable or disable syncing the cart ID to local storage. This allows the cart to persist across page reloads and sessions. It defaults to `true`.
You can also provide an object with the following properties for more configuration:
| Property | Type | Description |
| -------- | -------- | ---------------------------------------------------------------------------- |
| `key` | `string` | The key to use for storing the cart ID in local storage. Defaults to `cart`. |
### useCart
The `useCart` hook is used to manage the cart state and actions. It provides methods to add, remove, and update items in the cart, as well as to fetch the current cart state. It has the following properties:
| Property | Type | Description |
| --------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `addItem` | `(item: CartItemInput, quantity?: number) => void` | Method to add an item to the cart, optionally accepts a quantity to add multiple at once. |
| `cart` | `Cart` `null` | The current cart state. Null or undefined if it doesn't exist. |
| `clearCart` | `() => void` | Method to clear the cart. |
| `decrementItem` | `(item: IDType) => void` | Method to decrement the quantity of an item. Will remove it entirely if it reaches 0. |
| `incrementItem` | `(item: IDType) => void` | Method to increment the quantity of an item. |
| `removeItem` | `(item: IDType) => void` | Method to remove an item from the cart. |
Example usage:
```tsx
import { useCart } from '@payloadcms/plugin-ecommerce/react'
const CartComponent = () => {
const { addItem, cart, clearCart, decrementItem, incrementItem, removeItem } =
useCart()
// Your component logic here
}
```
### useAddresses
The `useAddresses` hook is used to fetch and manage addresses. It provides methods to create, update, and delete addresses, as well as to fetch the list of addresses. It has the following properties:
| Property | Type | Description |
| --------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| `addresses` | `Address[]` | The list of addresses, if any are available for the current user. |
| `createAddress` | `(data: Address) => Promise<Address>` | Method to create a new address. |
| `updateAddress` | `(addressID: IDType, data: Partial<Address>) => Promise<Address>` | Method to update an existing address by ID. |
Example usage:
```tsx
import { useAddresses } from '@payloadcms/plugin-ecommerce/react'
const AddressesComponent = () => {
const { addresses, createAddress, updateAddress } = useAddresses()
// Your component logic here
}
```
### usePayments
The `usePayments` hook is used to manage the checkout process. It provides methods to initiate payments, confirm orders, and handle payment status. It has the following properties:
| Property | Type | Description |
| ----------------------- | -------------------------- | ------------------------------------------------------------------- |
| `confirmOrder` | `(args) => Promise<Order>` | Method to confirm an order by ID. [More](#confirmOrder) |
| `initiatePayment` | `(args) => Promise<void>` | Method to initiate a payment for an order. [More](#initiatePayment) |
| `paymentMethods` | `PaymentMethod[]` | The list of available payment methods. |
| `selectedPaymentMethod` | `PaymentMethod` | The currently selected payment method, if any. |
Example usage:
```tsx
import { usePayments } from '@payloadcms/plugin-ecommerce/react'
const CheckoutComponent = () => {
const {
confirmOrder,
initiatePayment,
paymentMethods,
selectedPaymentMethod,
} = usePayments()
// Your component logic here
}
```
#### confirmOrder
Use this method to confirm an order by its ID. It requires the payment method ID and will return the order ID.
```ts
try {
const data = await confirmOrder('stripe', {
additionalData: {
paymentIntentID: paymentIntent.id,
customerEmail,
},
})
// Return type will contain `orderID`
// use data to redirect to your order page
} catch (error) {
// handle error
}
```
If the payment gateway requires additional confirmations offsite then you will need another landing page to handle that. For example with Stripe you may need to use a callback URL, just make sure the relevant information is routed back.
<Banner type="info">
This will mark the transaction as complete in the backend and create the order
for the user.
</Banner>
#### initiatePayment
Use this method to initiate a payment for an order. It requires the cart ID and the payment method ID. Depending on the payment method, additional data may be required. Depending on the payment method used you may need to provide additional data, for example with Stripe:
```ts
try {
const data = await initiatePayment('stripe', {
additionalData: {
customerEmail,
billingAddress,
shippingAddress,
},
})
} catch (error) {
// handle error
}
```
This function will hit the Payload API endpoint for `/stripe/initiate` and return the payment data required to complete the payment on the client side, which by default will include a `client_secret` to complete the payment with Stripe.js. The next step is to call the `confirmOrder` once payment is confirmed on the client side by Stripe.
<Banner type="info">
At this step the cart is verified and a transaction is created in the backend
with the address details provided. No order is created yet until you call
`confirmOrder`, which should be done after payment is confirmed on the client
side or via webhooks if you opt for that approach instead.
</Banner>
### useCurrency
The `useCurrency` hook is used to format prices based on the selected currency. It provides methods to format prices and to get the current currency. It has the following properties:
| Property | Type | Description |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `currenciesConfig` | `CurrenciesConfig` | The configuration for supported currencies. Directly matching the config provided to the Context Provider. [More](#ecommerceprovider) |
| `currency` | `Currency` | The currently selected currency. |
| `formatPrice` | `(amount: number) => string` | Method to format a price based on the selected currency. |
| `setCurrency` | `(currencyCode: string) => void` | Method to set the current currency by code. It will update all price formats when used in conjunction with the `formatPrice` utility. |
`formatPrice` in particular is very helpful as all prices are stored as integers to avoid any potential issues with decimal calculations, therefore on the frontend you can use this utility to format your price accounting for the currency and decimals. Example usage:
```tsx
import { useCurrency } from '@payloadcms/plugin-ecommerce/react'
const PriceComponent = ({ amount }) => {
const { currenciesConfig, currency, setCurrency } = useCurrency()
return <div>{formatPrice(amount)}</div>
}
```
### useEcommerce
The `useEcommerce` hook encompasses all of the above hooks in one. It provides access to the cart, addresses, and payments hooks.
Example usage:
```tsx
import { useEcommerce } from '@payloadcms/plugin-ecommerce/react'
const EcommerceComponent = () => {
const { cart, addresses, selectedPaymentMethod } = useEcommerce()
// Your component logic here
}
```

138
docs/ecommerce/overview.mdx Normal file
View File

@@ -0,0 +1,138 @@
---
title: Ecommerce Overview
label: Overview
order: 10
desc: Add ecommerce functionality to your Payload CMS application with this plugin.
keywords: plugins, ecommerce, stripe, plugin, payload, cms, shop, payments
---
![https://www.npmjs.com/package/@payloadcms/plugin-ecommerce](https://img.shields.io/npm/v/@payloadcms/plugin-ecommerce)
<Banner type="warning">
This plugin is currently in Beta and may have breaking changes in future
releases.
</Banner>
Payload provides an Ecommerce Plugin that allows you to add ecommerce functionality to your app. It comes a set of utilities and collections to manage products, orders, and payments. It also integrates with popular payment gateways like Stripe to handle transactions.
<Banner type="info">
This plugin is completely open-source and the [source code can be found
here](https://github.com/payloadcms/payload/tree/main/packages/plugin-ecommerce).
If you need help, check out our [Community
Help](https://payloadcms.com/community-help). If you think you've found a bug,
please [open a new
issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%redirects&template=bug_report.md&title=plugin-ecommerce%3A)
with as much detail as possible.
</Banner>
## Core features
The plugin ships with a wide range of features to help you get started with ecommerce:
- Products with Variants are supported by default
- Carts are tracked in Payload
- Orders and Transactions
- Addresses linked to your Customers
- Payments adapter pattern to create your own integrations (Stripe currently supported)
- Multiple currencies are supported
- React UI utilities to help you manage your frontend logic
_Currently_ the plugin does not handle shipping, taxes or subscriptions natively, but you can implement these features yourself using the provided collections and hooks.
## Installation
Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com):
```bash
pnpm add @payloadcms/plugin-ecommerce
```
## Basic Usage
In the `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin with:
```ts
import { buildConfig } from 'payload'
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
const config = buildConfig({
collections: [
{
slug: 'pages',
fields: [],
},
],
plugins: [
ecommercePlugin({
// You must add your access control functions here
access: {
adminOnly,
adminOnlyFieldAccess,
adminOrCustomerOwner,
adminOrPublishedStatus,
customerOnlyFieldAccess,
},
customers: { slug: 'users' },
}),
],
})
export default config
```
## Concepts
It's important to understand overall how the plugin works and the relationships between the different collections.
**Customers**
Can be any collection of users in your application. You can then limit access control only to customers depending on individual fields such
as roles on the customer collection or by collection slug if you've opted to keep them separate. Customers are linked to Carts and Orders.
**Products and Variants**
Products are the items you are selling and they will contain a price and optionally variants via a join field as well as allowed Variant Types.
Each Variant Type can contain a set of Variant Options. For example, a T-Shirt product can have a Variant Type of Size with options Small, Medium, and Large and each Variant can therefore have those options assigned to it.
**Carts**
Carts are linked to Customers or they're left entirely public for guests users and can contain multiple Products and Variants. Carts are stored in the database and can be retrieved at any time. Carts are automatically created for Customers when they add a product to their cart for the first time.
**Transactions and Orders**
Transactions are created when a payment is initiated. They contain the payment status and are linked to a Cart and Customer. Orders are created when a Transaction is successful and contain the final details of the purchase including the items, total, and customer information.
**Addresses**
Addresses are linked to Customers and can be used for billing and shipping information. They can be reused across multiple Orders.
**Payments**
The plugin uses an adapter pattern to allow for different payment gateways. The default adapter is for Stripe, but you can create your own by implementing the `PaymentAdapter` interface.
**Currencies**
The plugin supports using multiple currencies at the configuration level. Each currency will create a separate price field on the Product and Variants collections.
The package can also be used piece-meal if you only want to re-use certain parts of it, such as just the creation of Products and Variants. See [Advanced uses and examples](./advanced) for more details.
## TypeScript
The plugin will inherit the types from your generated Payload types where possible. We also export the following types:
- `Cart` - The cart type as stored in the React state and local storage and on the client side.
- `CollectionOverride` - Type for overriding collections.
- `CurrenciesConfig` - Type for the currencies configuration.
- `EcommercePluginConfig` - The configuration object for the ecommerce plugin.
- `FieldsOverride` - Type for overriding fields in collections.
All types can be directly imported:
```ts
import { EcommercePluginConfig } from '@payloadcms/plugin-ecommerce/types'
```
## Template
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce), which uses this plugin.

422
docs/ecommerce/payments.mdx Normal file
View File

@@ -0,0 +1,422 @@
---
title: Payment Adapters
label: Payment Adapters
order: 40
desc: Add ecommerce functionality to your Payload CMS application with this plugin.
keywords: plugins, ecommerce, stripe, plugin, payload, cms, shop, payments
---
A deeper look into the payment adapter pattern used by the Ecommerce Plugin, and how to create your own.
The current list of supported payment adapters are:
- [Stripe](#stripe)
## REST API
The plugin will create REST API endpoints for each payment adapter you add to your configuration. The endpoints will be available at `/api/payments/{provider_name}/{action}` where `provider_name` is the name of the payment adapter and `action` is one of the following:
| Action | Method | Description |
| --------------- | ------ | --------------------------------------------------------------------------------------------------- |
| `initiate` | POST | Initiate a payment for an order. See [initiatePayment](#initiatePayment) for more details. |
| `confirm-order` | POST | Confirm an order after a payment has been made. See [confirmOrder](#confirmOrder) for more details. |
## Stripe
Out of the box we integrate with Stripe to handle one-off purchases. To use Stripe, you will need to install the Stripe package:
```bash
pnpm add stripe
```
We recommend at least `18.5.0` to ensure compatibility with the plugin.
Then, in your `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin with:
```ts
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
import { buildConfig } from 'payload'
export default buildConfig({
// Payload config...
plugins: [
ecommercePlugin({
// rest of config...
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
// Optional - only required if you want to use webhooks
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
}),
],
},
}),
],
})
```
### Configuration
The Stripe payment adapter takes the following configuration options:
| Option | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| secretKey | `string` | Your Stripe Secret Key, found in the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). |
| publishableKey | `string` | Your Stripe Publishable Key, found in the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). |
| webhookSecret | `string` | (Optional) Your Stripe Webhooks Signing Secret, found in the [Stripe Dashboard](https://dashboard.stripe.com/webhooks). Required if you want to use webhooks. |
| appInfo | `object` | (Optional) An object containing `name` and `version` properties to identify your application to Stripe. |
| webhooks | `object` | (Optional) An object where the keys are Stripe event types and the values are functions that will be called when that event is received. See [Webhooks](#stripe-webhooks) for more details. |
| groupOverrides | `object` | (Optional) An object to override the default fields of the payment group. See [Payment Fields](./advanced#payment-fields) for more details. |
### Stripe Webhooks
You can also add your own webhooks to handle [events from Stripe](https://docs.stripe.com/api/events). This is optional and the plugin internally does not use webhooks for any core functionality. It receives the following arguments:
| Argument | Type | Description |
| -------- | ---------------- | ------------------------------- |
| event | `Stripe.Event` | The Stripe event object |
| req | `PayloadRequest` | The Payload request object |
| stripe | `Stripe` | The initialized Stripe instance |
You can add a webhook like so:
```ts
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
import { buildConfig } from 'payload'
export default buildConfig({
// Payload config...
plugins: [
ecommercePlugin({
// rest of config...
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
// Required
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
webhooks: {
'payment_intent.succeeded': ({ event, req }) => {
console.log({ event, data: event.data.object })
req.payload.logger.info('Payment succeeded')
},
},
}),
],
},
}),
],
})
```
To use webhooks you also need to have them configured in your Stripe Dashboard.
You can use the [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward webhooks to your local development environment.
### Frontend usage
The most straightforward way to use Stripe on the frontend is with the `EcommerceProvider` component and the `stripeAdapterClient` function. Wrap your application in the provider and pass in the Stripe adapter with your publishable key:
```ts
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react'
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
<EcommerceProvider
paymentMethods={[
stripeAdapterClient({
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
}),
]}
>
{children}
</EcommerceProvider>
```
Then you can use the `usePayments` hook to access the `initiatePayment` and `confirmOrder` functions, see the [Frontend docs](./frontend#usePayments) for more details.
## Making your own Payment Adapter
You can make your own payment adapter by implementing the `PaymentAdapter` interface. This interface requires you to implement the following methods:
| Property | Type | Description |
| ----------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | The name of the payment method. This will be used to identify the payment method in the API and on the frontend. |
| `label` | `string` | (Optional) A human-readable label for the payment method. This will be used in the admin panel and on the frontend. |
| `initiatePayment` | `(args: InitiatePaymentArgs) => Promise<InitiatePaymentResult>` | The function that is called via the `/api/payments/{provider_name}/initiate` endpoint to initiate a payment for an order. [More](#initiatePayment) |
| `confirmOrder` | `(args: ConfirmOrderArgs) => Promise<void>` | The function that is called via the `/api/payments/{provider_name}/confirm-order` endpoint to confirm an order after a payment has been made. [More](#confirmOrder) |
| `endpoints` | `Endpoint[]` | (Optional) An array of endpoints to be bootstrapped to Payload's API in order to support the payment method. All API paths are relative to `/api/payments/{provider_name}` |
| `group` | `GroupField` | A group field config to be used in transactions to track the necessary data for the payment processor, eg. PaymentIntentID for Stripe. See [Payment Fields](#payment-fields) for more details. |
The arguments can be extended but should always include the `PaymentAdapterArgs` type which has the following types:
| Property | Type | Description |
| ---------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `label` | `string` | (Optional) Allow overriding the default UI label for this adaper. |
| `groupOverrides` | `FieldsOverride` | (Optional) Allow overriding the default fields of the payment group. See [Payment Fields](#payment-fields) for more details. |
#### initiatePayment
The `initiatePayment` function is called when a payment is initiated. At this step the transaction is created with a status "Processing", an abandoned purchaase will leave this transaction in this state. It receives an object with the following properties:
| Property | Type | Description |
| ------------------ | ---------------- | --------------------------------------------- |
| `transactionsSlug` | `Transaction` | The transaction being processed. |
| `data` | `object` | The cart associated with the transaction. |
| `customersSlug` | `string` | The customer associated with the transaction. |
| `req` | `PayloadRequest` | The Payload request object. |
The data object will contain the following properties:
| Property | Type | Description |
| ----------------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
| `billingAddress` | `Address` | The billing address associated with the transaction. |
| `shippingAddress` | `Address` | (Optional) The shipping address associated with the transaction. If this is missing then use the billing address. |
| `cart` | `Cart` | The cart collection item. |
| `customerEmail` | `string` | In the case that `req.user` is missing, `customerEmail` should be required in order to process guest checkouts. |
| `currency` | `string` | The currency for the cart associated with the transaction. |
The return type then only needs to contain the following properties though the type supports any additional data returned as needed for the frontend:
| Property | Type | Description |
| --------- | -------- | ----------------------------------------------- |
| `message` | `string` | A success message to be returned to the client. |
At any point in the function you can throw an error to return a 4xx or 5xx response to the client.
A heavily simplified example of implementing `initiatePayment` could look like:
```ts
import {
PaymentAdapter,
PaymentAdapterArgs,
} from '@payloadcms/plugin-ecommerce'
import Stripe from 'stripe'
export const initiatePayment: NonNullable<PaymentAdapter>['initiatePayment'] =
async ({ data, req, transactionsSlug }) => {
const payload = req.payload
// Check for any required data
const currency = data.currency
const cart = data.cart
if (!currency) {
throw new Error('Currency is required.')
}
const stripe = new Stripe(secretKey)
try {
let customer = (
await stripe.customers.list({
email: customerEmail,
})
).data[0]
// Ensure stripe has a customer for this email
if (!customer?.id) {
customer = await stripe.customers.create({
email: customerEmail,
})
}
const shippingAddressAsString = JSON.stringify(shippingAddressFromData)
const paymentIntent = await stripe.paymentIntents.create()
// Create a transaction for the payment intent in the database
const transaction = await payload.create({
collection: transactionsSlug,
data: {},
})
// Return the client_secret so that the client can complete the payment
const returnData: InitiatePaymentReturnType = {
clientSecret: paymentIntent.client_secret || '',
message: 'Payment initiated successfully',
paymentIntentID: paymentIntent.id,
}
return returnData
} catch (error) {
payload.logger.error(error, 'Error initiating payment with Stripe')
throw new Error(
error instanceof Error
? error.message
: 'Unknown error initiating payment',
)
}
}
```
#### confirmOrder
The `confirmOrder` function is called after a payment is completed on the frontend and at this step the order is created in Payload. It receives the following properties:
| Property | Type | Description |
| ------------------ | ---------------- | ----------------------------------------- |
| `ordersSlug` | `string` | The orders collection slug. |
| `transactionsSlug` | `string` | The transactions collection slug. |
| `cartsSlug` | `string` | The carts collection slug. |
| `customersSlug` | `string` | The customers collection slug. |
| `data` | `object` | The cart associated with the transaction. |
| `req` | `PayloadRequest` | The Payload request object. |
The data object will contain any data the frontend chooses to send through and at a minimum the following:
| Property | Type | Description |
| --------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `customerEmail` | `string` | In the case that `req.user` is missing, `customerEmail` should be required in order to process guest checkouts. |
The return type can also contain any additional data with a minimum of the following:
| Property | Type | Description |
| --------------- | -------- | ----------------------------------------------- |
| `message` | `string` | A success message to be returned to the client. |
| `orderID` | `string` | The ID of the created order. |
| `transactionID` | `string` | The ID of the associated transaction. |
A heavily simplified example of implementing `confirmOrder` could look like:
```ts
import {
PaymentAdapter,
PaymentAdapterArgs,
} from '@payloadcms/plugin-ecommerce'
import Stripe from 'stripe'
export const confirmOrder: NonNullable<PaymentAdapter>['confirmOrder'] =
async ({
data,
ordersSlug = 'orders',
req,
transactionsSlug = 'transactions',
}) => {
const payload = req.payload
const customerEmail = data.customerEmail
const paymentIntentID = data.paymentIntentID as string
const stripe = new Stripe(secretKey)
try {
// Find our existing transaction by the payment intent ID
const transactionsResults = await payload.find({
collection: transactionsSlug,
where: {
'stripe.paymentIntentID': {
equals: paymentIntentID,
},
},
})
const transaction = transactionsResults.docs[0]
// Verify the payment intent exists and retrieve it
const paymentIntent =
await stripe.paymentIntents.retrieve(paymentIntentID)
// Create the order in the database
const order = await payload.create({
collection: ordersSlug,
data: {},
})
const timestamp = new Date().toISOString()
// Update the cart to mark it as purchased, this will prevent further updates to the cart
await payload.update({
id: cartID,
collection: 'carts',
data: {
purchasedAt: timestamp,
},
})
// Update the transaction with the order ID and mark as succeeded
await payload.update({
id: transaction.id,
collection: transactionsSlug,
data: {
order: order.id,
status: 'succeeded',
},
})
return {
message: 'Payment initiated successfully',
orderID: order.id,
transactionID: transaction.id,
}
} catch (error) {
payload.logger.error(error, 'Error initiating payment with Stripe')
}
}
```
#### Payment Fields
Payment fields are used primarily on the transactions collection to store information about the payment method used. Each payment adapter must provide a `group` field which will be used to store this information.
For example, the Stripe adapter provides the following group field:
```ts
const groupField: GroupField = {
name: 'stripe',
type: 'group',
admin: {
condition: (data) => {
const path = 'paymentMethod'
return data?.[path] === 'stripe'
},
},
fields: [
{
name: 'customerID',
type: 'text',
label: 'Stripe Customer ID',
},
{
name: 'paymentIntentID',
type: 'text',
label: 'Stripe PaymentIntent ID',
},
],
}
```
### Client side Payment Adapter
The client side adapter should implement the `PaymentAdapterClient` interface:
| Property | Type | Description |
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | The name of the payment method. This will be used to identify the payment method in the API and on the frontend. |
| `label` | `string` | (Optional) A human-readable label for the payment method. This can be used as a human readable format. |
| `initiatePayment` | `boolean` | Flag to toggle on the EcommerceProvider's ability to call the `/api/payments/{provider_name}/initiate` endpoint. If your payment method does not require this step, set this to `false`. |
| `confirmOrder` | `boolean` | Flag to toggle on the EcommerceProvider's ability to call the `/api/payments/{provider_name}/confirm-order` endpoint. If your payment method does not require this step, set this to `false`. |
And for the args use the `PaymentAdapterClientArgs` type:
| Property | Type | Description |
| -------- | -------- | ----------------------------------------------------------------- |
| `label` | `string` | (Optional) Allow overriding the default UI label for this adaper. |
## Best Practices
Always handle sensitive operations like creating payment intents and confirming payments on the server side. Use webhooks to listen for events from Stripe and update your orders accordingly. Never expose your secret key on the frontend. By default Nextjs will only expose environment variables prefixed with `NEXT_PUBLIC_` to the client.
While we validate the products and prices on the server side when creating a payment intent, you should override the validation function to add any additional checks you may need for your specific use case.
You are safe to pass the ID of a transaction to the frontend however you shouldn't pass any sensitive information or the transaction object itself.
When passing price information to your payment provider it should always come from the server and it should be verified against the products in your database. Never trust price information coming from the client.
When using webhooks, ensure that you verify the webhook signatures to confirm that the requests are genuinely from Stripe. This helps prevent unauthorized access and potential security vulnerabilities.

661
docs/ecommerce/plugin.mdx Normal file
View File

@@ -0,0 +1,661 @@
---
title: Ecommerce Plugin
label: Plugin
order: 20
desc: Add ecommerce functionality to your Payload CMS application with this plugin.
keywords: plugins, ecommerce, stripe, plugin, payload, cms, shop, payments
---
## Basic Usage
In the `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin with:
```ts
import { buildConfig } from 'payload'
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
const config = buildConfig({
collections: [
{
slug: 'pages',
fields: [],
},
],
plugins: [
ecommercePlugin({
// You must add your access control functions here
access: {
adminOnly,
adminOnlyFieldAccess,
adminOrCustomerOwner,
adminOrPublishedStatus,
customerOnlyFieldAccess,
},
customers: { slug: 'users' },
}),
],
})
export default config
```
## Options
| Option | Type | Description |
| -------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `access` | `object` | Configuration to override the default access control, use this when checking for roles or multi tenancy. [More](#access) |
| `addresses` | `object` | Configuration for addresses collection and supported fields. [More](#addresses) |
| `carts` | `object` | Configuration for carts collection. [More](#carts) |
| `currencies` | `object` | Supported currencies by the store. [More](#currencies) |
| `customers` | `object` | Used to provide the customers slug. [More](#customers) |
| `inventory` | `boolean` `object` | Enable inventory tracking within Payload. Defaults to `true`. [More](#inventory) |
| `payments` | `object` | Configuring payments and supported payment methods. [More](#payments) |
| `products` | `object` | Configuration for products, variants collections and more. [More](#products) |
| `orders` | `object` | Configuration for orders collection. [More](#orders) |
| `transactions` | `boolean` `object` | Configuration for transactions collection. [More](#transactions) |
Note that the fields in overrides take a function that receives the default fields and returns an array of fields. This allows you to add fields to the collection.
```ts
ecommercePlugin({
access: {
adminOnly,
adminOnlyFieldAccess,
adminOrCustomerOwner,
adminOrPublishedStatus,
customerOnlyFieldAccess,
},
customers: {
slug: 'users',
},
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!,
}),
],
},
products: {
variants: {
variantsCollection: VariantsCollection,
},
productsCollection: ProductsCollection,
},
})
```
## Access
The plugin requires access control functions in order to restrict permissions to certain collections or fields. You can override these functions by providing your own in the `access` option.
| Option | Type | Description |
| ------------------------- | ------------- | ------------------------------------------------------------------------- |
| `authenticatedOnly` | `Access` | Authenticated access only, provided by default. |
| `publicAccess` | `Access` | Public access, provided by default. |
| `adminOnly` | `Access` | Limited to only admin users. |
| `adminOnlyFieldAccess` | `FieldAccess` | Limited to only admin users, specifically for Field level access control. |
| `adminOrCustomerOwner` | `Access` | Is the owner of the document via the `customer` field or is an admin. |
| `adminOrPublishedStatus` | `Access` | The document is published or user is admin. |
| `customerOnlyFieldAccess` | `FieldAccess` | Limited to customers only, specifically for Field level access control. |
The default access control functions are:
```ts
access: {
authenticatedOnly: ({ req: { user } }) => Boolean(user),
publicAccess: () => true,
}
```
### authenticatedOnly
Access control to check if the user is authenticated. By default the following is provided:
```ts
authenticatedOnly: ({ req: { user } }) => Boolean(user)
```
### publicAccess
Access control to allow public access. By default the following is provided:
```ts
publicAccess: () => true
```
### adminOnly
Access control to check if the user has `admin` permissions.
Example:
```ts
adminOnly: ({ req: { user } }) => Boolean(user?.roles?.includes('admin'))
```
### adminOnlyFieldAccess
Field level access control to check if the user has `admin` permissions.
Example:
```ts
adminOnlyFieldAccess: ({ req: { user } }) =>
Boolean(user?.roles?.includes('admin'))
```
### adminOrCustomerOwner
Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field.
Example:
```ts
adminOrCustomerOwner: ({ req: { user } }) => {
if (user && Boolean(user?.roles?.includes('admin'))) {
return true
}
if (user?.id) {
return {
customer: {
equals: user.id,
},
}
}
return false
}
```
### adminOrPublishedStatus
Access control to check if the user has `admin` permissions or if the document is published.
Example:
```ts
adminOrPublishedStatus: ({ req: { user } }) => {
if (user && Boolean(user?.roles?.includes('admin'))) {
return true
}
return {
_status: {
equals: 'published',
},
}
}
```
### customerOnlyFieldAccess
Field level access control to check if the user has `customer` permissions.
Example:
```ts
customerOnlyFieldAccess: ({ req: { user } }) =>
Boolean(user?.roles?.includes('customer'))
```
## Addresses
The `addresses` option is used to configure the addresses collection and supported fields. Defaults to `true` which will create an `addresses` collection with default fields. It also takes an object:
| Option | Type | Description |
| ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `addressFields` | `FieldsOverride` | A function that is given the `defaultFields` as an argument and returns an array of fields. Use this to customise the supported fields for stored addresses. |
| `addressesCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `addresses` with a function where you can access the `defaultCollection` as an argument. |
| `supportedCountries` | `CountryType[]` | An array of supported countries in [ISO 3166-1 alpha-2 format](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). Defaults to all countries. |
You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields:
```ts
addresses: {
addressesCollectionOverride: ({ defaultCollection }) => ({
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'googleMapLocation',
label: 'Google Map Location',
type: 'text',
},
],
})
}
```
### supportedCountries
The `supportedCountries` option is an array of country codes in [ISO 3166-1 alpha-2 format](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). This is used to limit the countries that can be selected when creating or updating an address. If not provided, all countries will be supported. Currently used for storing addresses only.
You can import the default list of countries from the plugin:
```ts
import { defaultCountries } from '@payloadcms/plugin-ecommerce/client/react'
```
## Carts
The `carts` option is used to configure the carts collection. Defaults to `true` which will create a `carts` collection with default fields. It also takes an object:
| Option | Type | Description |
| ------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `cartsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `carts` with a function where you can access the `defaultCollection` as an argument. |
You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields:
```ts
carts: {
cartsCollectionOverride: ({ defaultCollection }) => ({
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'notes',
label: 'Notes',
type: 'textarea',
},
],
})
}
```
Carts are created when a customer adds their first item to the cart. The cart is then updated as they add or remove items. The cart is linked to a _Customer_ via the `customer` field. If the user is authenticated, this will be set to their user ID. If the user is not authenticated, this will be `null`.
If the user is not authenticated, the cart ID is stored in local storage and used to fetch the cart on subsequent requests. Access control by default works so that if the user is not authenticated then they can only access carts that have no customer linked to them.
## Customers
The `customers` option is required and is used to provide the customers collection slug. This collection is used to link orders, carts, and addresses to a customer.
| Option | Type | Description |
| ------ | -------- | ------------------------------------- |
| `slug` | `string` | The slug of the customers collection. |
While it's recommended to use just one collection for customers and your editors, you can use any collection you want for your customers. Just make sure that your access control is checking for the correct collections as well.
## Currencies
The `currencies` option is used to configure the supported currencies by the store. Defaults to `true` which will support `USD`. It also takes an object:
| Option | Type | Description |
| --------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `supportedCurrencies` | `Currency[]` | An array of supported currencies by the store. Defaults to `USD`. See [Currencies](#currencies-list) for available currencies. |
| `defaultCurrency` | `string` | The default currency code to use for the store. Defaults to the first currency. Must be one of the `supportedCurrencies` codes. |
The `Currency` type is as follows:
```ts
type Currency = {
code: string // The currency code in ISO 4217 format, e.g. 'USD'
decimals: number // The number of decimal places for the currency, e.g. 2 for USD
label: string // A human-readable label for the currency, e.g. 'US Dollar'
symbol: string // The currency symbol, e.g. '$'
}
```
For example, to support JYP in addition to USD:
```ts
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { USD } from '@payloadcms/plugin-ecommerce'
ecommercePlugin({
currencies: {
supportedCurrencies: [
USD,
{
code: 'JPY',
decimals: 0,
label: 'Japanese Yen',
symbol: '¥',
},
],
defaultCurrency: 'USD',
},
})
```
Note that adding a new currency could generate a new schema migration as it adds new prices fields in your products.
We currently support the following currencies out of the box:
- `USD`
- `EUR`
- `GBP`
You can import these from the plugin:
```ts
import { EUR } from '@payloadcms/plugin-ecommerce'
```
<Banner type="info">
Note that adding new currencies here does not automatically enable them in
your payment gateway. Make sure to enable the currencies in your payment
gateway dashboard as well.
</Banner>
## Inventory
The `inventory` option is used to enable or disable inventory tracking within Payload. It defaults to `true`. It also takes an object:
| Option | Type | Description |
| ----------- | -------- | ------------------------------------------------------------------------- |
| `fieldName` | `string` | Override the field name used to track inventory. Defaults to `inventory`. |
For now it's quite rudimentary tracking with no integrations to 3rd party services. It will simply add an `inventory` field to the `variants` collection and decrement the inventory when an order is placed.
## Payments
The `payments` option is used to configure payments and supported payment methods.
| Option | Type | Description |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------- |
| `paymentMethods` | `array` | An array of payment method adapters. Currently, only Stripe is supported. [More](#stripe-adapter) |
### Payment adapters
The plugin supports payment adapters to integrate with different payment gateways. Currently, only the [Stripe adapter](#stripe-adapter) is available. Adapters will provide a client side version as well with slightly different arguments.
Every adapter supports the following arguments in addition to their own:
| Argument | Type | Description |
| ---------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| `label` | `string` | Human readabale label for this payment adapter. |
| `groupOverrides` | `GroupField` with `FieldsOverride` | Use this to override the available fields for the payment adapter type. |
Client side base arguments are the following:
| Argument | Type | Description |
| -------- | -------- | ----------------------------------------------- |
| `label` | `string` | Human readabale label for this payment adapter. |
See the [Stripe adapter](#stripe-adapter) for an example of client side arguments and the [React section](#react) for usage.
#### `groupOverrides`
The `groupOverrides` option allows you to customize the fields that are available for a specific payment adapter. It takes a `GroupField` object with a `fields` function that receives the default fields and returns an array of fields.
These fields are stored in transactions and can be used to collect additional information for the payment method. Stripe, for example, will track the `paymentIntentID`.
Example for overriding the default fields:
```ts
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
groupOverrides: {
fields: ({ defaultFields }) => {
return [
...defaultFields,
{
name: 'customField',
label: 'Custom Field',
type: 'text',
},
]
}
}
}),
],
},
```
### Stripe Adapter
The Stripe adapter is used to integrate with the Stripe payment gateway. It requires a secret key, publishable key, and optionally webhook secret.
<Banner type="info">
Note that Payload will not install the Stripe SDK package for you
automatically, so you will need to install it yourself:
```
pnpm add stripe
```
</Banner>
| Argument | Type | Description |
| ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `secretKey` | `string` | Required for communicating with the Stripe API in the backend. |
| `publishableKey` | `string` | Required for communicating with the Stripe API in the client side. |
| `webhookSecret` | `string` | The webhook secret used to verify incoming webhook requests from Stripe. |
| `webhooks` | `WebhookHandler[]` | An array of webhook handlers to register within Payload's REST API for Stripe to callback. |
| `apiVersion` | `string` | The Stripe API version to use. See [docs](https://stripe.com/docs/api/versioning). This will be deprecated soon by Stripe's SDK, configure the API version in your Stripe Dashboard. |
| `appInfo` | `object` | The application info to pass to Stripe. See [docs](https://stripe.com/docs/api/app_info). |
```ts
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!,
})
```
#### Stripe `webhooks`
The `webhooks` option allows you to register custom webhook handlers for [Stripe events](https://docs.stripe.com/api/events). This is useful if you want to handle specific events that are not covered by the default handlers provided by the plugin.
```ts
stripeAdapter({
webhooks: {
'payment_intent.succeeded': ({ event, req }) => {
// Access to Payload's req object and event data
},
},
}),
```
#### Stripe client side
On the client side, you can use the `publishableKey` to initialize Stripe and handle payments. The client side version of the adapter only requires the `label` and `publishableKey` arguments. Never expose the `secretKey` or `webhookSecret` keys on the client side.
```ts
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
<EcommerceProvider
paymentMethods={[
stripeAdapterClient({
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
}),
]}
>
{children}
</EcommerceProvider>
```
## Products
The `products` option is used to configure the products and variants collections. Defaults to `true` which will create `products` and `variants` collections with default fields. It also takes an object:
| Option | Type | Description |
| ---------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `products` with a function where you can access the `defaultCollection` as an argument. |
| `variants` | `boolean` `object` | Configuration for the variants collection. Defaults to true. [More](#variants) |
| `validation` | `ProductsValidation` | Customise the validation used for checking products or variants before a transaction is created or a payment can be confirmed. [More](#products-validation) |
You can add your own fields or modify the structure of the existing on in the collections. Example for overriding the default fields:
```ts
products: {
productsCollectionOverride: ({ defaultCollection }) => ({
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'notes',
label: 'Notes',
type: 'textarea',
},
],
})
}
```
### Variants
The `variants` option is used to configure the variants collection. It takes an object:
| Option | Type | Description |
| ---------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `variantsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `variants` with a function where you can access the `defaultCollection` as an argument. |
| `variantTypesCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `variantTypes` with a function where you can access the `defaultCollection` as an argument. |
| `variantOptionsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `variantOptions` with a function where you can access the `defaultCollection` as an argument. |
You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields:
```ts
variants: {
variantsCollectionOverride: ({ defaultCollection }) => ({
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'customField',
label: 'Custom Field',
type: 'text',
},
],
})
}
```
The key differences between these collections:
- `variantTypes` are the types of variants that a product can have, e.g. Size, Color.
- `variantOptions` are the options for each variant type, e.g. Small, Medium, Large for Size.
- `variants` are the actual variants of a product, e.g. a T-Shirt in Size Small and Color Red.
### Products validation
We use an addition validation step when creating transactions or confirming payments to ensure that the products and variants being purchased are valid. This is to prevent issues such as purchasing a product that is out of stock or has been deleted.
You can customise this validation by providing your own validation function via the `validation` option which receives the following arguments:
| Option | Type | Description |
| ------------------ | ------------------ | -------------------------------------------------------------------------------------------------------- |
| `currenciesConfig` | `CurrenciesConfig` | The full currencies configuration provided in the plugin options. |
| `product` | `TypedCollection` | The product being purchased. |
| `variant` | `TypedCollection` | The variant being purchased, if a variant was selected for the product otherwise it will be `undefined`. |
| `quantity` | `number` | The quantity being purchased. |
| `currency` | `string` | The currency code being used for the purchase. |
The function should throw an error if the product or variant is not valid. If the function does not throw an error, the product or variant is considered valid.
The default validation function checks for the following:
- A currency is provided.
- The product or variant has a price in the selected currency.
- The product or variant has enough inventory for the requested quantity.
```ts
export const defaultProductsValidation: ProductsValidation = ({
currenciesConfig,
currency,
product,
quantity = 1,
variant,
}) => {
if (!currency) {
throw new Error('Currency must be provided for product validation.')
}
const priceField = `priceIn${currency.toUpperCase()}`
if (variant) {
if (!variant[priceField]) {
throw new Error(
`Variant with ID ${variant.id} does not have a price in ${currency}.`,
)
}
if (
variant.inventory === 0 ||
(variant.inventory && variant.inventory < quantity)
) {
throw new Error(
`Variant with ID ${variant.id} is out of stock or does not have enough inventory.`,
)
}
} else if (product) {
// Validate the product's details only if the variant is not provided as it can have its own inventory and price
if (!product[priceField]) {
throw new Error(`Product does not have a price in.`, {
cause: { code: MissingPrice, codes: [product.id, currency] },
})
}
if (
product.inventory === 0 ||
(product.inventory && product.inventory < quantity)
) {
throw new Error(
`Product is out of stock or does not have enough inventory.`,
{
cause: { code: OutOfStock, codes: [product.id] },
},
)
}
}
}
```
## Orders
The `orders` option is used to configure the orders collection. Defaults to `true` which will create an `orders` collection with default fields. It also takes an object:
| Option | Type | Description |
| -------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `ordersCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `orders` with a function where you can access the `defaultCollection` as an argument. |
You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields:
```ts
orders: {
ordersCollectionOverride: ({ defaultCollection }) => ({
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'notes',
label: 'Notes',
type: 'textarea',
},
],
})
}
```
## Transactions
The `transactions` option is used to configure the transactions collection. Defaults to `true` which will create a `transactions` collection with default fields. It also takes an object:
| Option | Type | Description |
| -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `transactionsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `transactions` with a function where you can access the `defaultCollection` as an argument. |
You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields:
```ts
transactions: {
transactionsCollectionOverride: ({ defaultCollection }) => ({
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'notes',
label: 'Notes',
type: 'textarea',
},
],
})
}
```

View File

@@ -56,6 +56,7 @@ Payload maintains a set of Official Plugins that solve for some of the common us
- [SEO](./seo)
- [Stripe](./stripe)
- [Import/Export](./import-export)
- [Ecommerce](../ecommerce/overview)
You can also [build your own plugin](./build-your-own) to easily extend Payload's functionality in some other way. Once your plugin is ready, consider [sharing it with the community](#community-plugins).