5 Commits

Author SHA1 Message Date
6de405d07f 0.1.18 2025-11-21 15:39:40 +01:00
7c0b42e35d fix: resolve plugin initialization failure in Next.js API routes
Use Symbol.for() instead of Symbol() for plugin singleton storage to ensure
plugin state persists across different module loading contexts (admin panel,
API routes, server components).

This fixes the "Billing plugin not initialized" error that occurred when
calling payload.create() from Next.js API routes, server components, or
server actions.

Changes:
- Plugin singleton now uses Symbol.for('@xtr-dev/payload-billing')
- Provider singletons (stripe, mollie, test) use global symbols
- Enhanced error message with troubleshooting guidance

Fixes #1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:35:12 +01:00
25b340d818 0.1.17 2025-11-18 23:12:32 +01:00
46bec6bd2e feat: add defaultPopulate configuration to payments collection
- Include defaultPopulate fields to simplify API responses
- Ensure key payment details (amount, status, provider, etc.) are preloaded
2025-11-18 23:12:30 +01:00
4fde492e0f feat: add checkoutUrl field to payment collection
- Add checkoutUrl field to Payment type and collection
- Mollie provider now sets checkoutUrl from _links.checkout.href
- Test provider sets checkoutUrl to interactive payment UI
- Stripe provider doesn't use checkoutUrl (uses client_secret instead)
- Update README with checkoutUrl examples and clarifications
- Make it easier to redirect users to payment pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 23:01:43 +01:00
9 changed files with 42 additions and 11 deletions

View File

@@ -111,9 +111,9 @@ const payment = await payload.create({
**What you get back:** **What you get back:**
- **Stripe**: `providerId` = PaymentIntent ID, `providerData.raw.client_secret` for Stripe.js - **Stripe**: `providerId` = PaymentIntent ID, use `providerData.raw.client_secret` with Stripe.js on frontend
- **Mollie**: `providerId` = Transaction ID, `providerData.raw._links.checkout.href` for redirect URL - **Mollie**: `providerId` = Transaction ID, redirect user to `checkoutUrl` to complete payment
- **Test**: `providerId` = Test payment ID, `providerData.raw.paymentUrl` for interactive test UI - **Test**: `providerId` = Test payment ID, navigate to `checkoutUrl` for interactive test UI
## Payment Providers ## Payment Providers
@@ -407,6 +407,7 @@ Tracks payment transactions with provider integration.
amount: number // Amount in cents amount: number // Amount in cents
currency: string // ISO 4217 currency code currency: string // ISO 4217 currency code
description?: string description?: string
checkoutUrl?: string // Checkout URL (if applicable)
invoice?: Invoice | string // Linked invoice invoice?: Invoice | string // Linked invoice
metadata?: Record<string, any> // Custom metadata metadata?: Record<string, any> // Custom metadata
providerData?: ProviderData // Raw provider response (read-only) providerData?: ProviderData // Raw provider response (read-only)
@@ -639,12 +640,14 @@ const payment = await payload.create({
} }
}) })
// Get client secret for Stripe.js // Get client secret for Stripe.js (Stripe doesn't use checkoutUrl)
const clientSecret = payment.providerData.raw.client_secret const clientSecret = payment.providerData.raw.client_secret
// Frontend: Confirm payment with Stripe.js // Frontend: Confirm payment with Stripe.js
// const stripe = Stripe('pk_...') // const stripe = Stripe('pk_...')
// await stripe.confirmCardPayment(clientSecret, { ... }) // await stripe.confirmCardPayment(clientSecret, { ... })
// For Mollie/Test: redirect to payment.checkoutUrl instead
``` ```
### Creating an Invoice with Line Items ### Creating an Invoice with Line Items

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-billing", "name": "@xtr-dev/payload-billing",
"version": "0.1.15", "version": "0.1.18",
"description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing", "description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View File

@@ -7,7 +7,8 @@ export const initProviderPayment = async (payload: Payload, payment: Partial<Pay
if (!billing) { if (!billing) {
throw new Error( throw new Error(
'Billing plugin not initialized. Make sure the billingPlugin is properly configured in your Payload config and that Payload has finished initializing.' 'Billing plugin not initialized. Make sure the billingPlugin is properly configured in your Payload config and that Payload has finished initializing. ' +
'If you are calling this from a Next.js API route or Server Component, ensure you are using getPayload() with the same config instance used in your Payload configuration.'
) )
} }

View File

@@ -78,6 +78,14 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
description: 'Payment description', description: 'Payment description',
}, },
}, },
{
name: 'checkoutUrl',
type: 'text',
admin: {
description: 'Checkout URL where user can complete payment (if applicable)',
readOnly: true,
},
},
{ {
name: 'invoice', name: 'invoice',
type: 'relationship', type: 'relationship',
@@ -136,6 +144,18 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
useAsTitle: 'id', useAsTitle: 'id',
}, },
fields, fields,
defaultPopulate: {
id: true,
provider: true,
status: true,
amount: true,
currency: true,
description: true,
checkoutUrl: true,
providerId: true,
metadata: true,
providerData: true,
},
hooks: { hooks: {
afterChange: [ afterChange: [
async ({ doc, operation, req, previousDoc }) => { async ({ doc, operation, req, previousDoc }) => {

View File

@@ -4,7 +4,7 @@ import type { Config, Payload } from 'payload'
import { createSingleton } from './singleton' import { createSingleton } from './singleton'
import type { PaymentProvider } from '../providers/index' import type { PaymentProvider } from '../providers/index'
const singleton = createSingleton(Symbol('billingPlugin')) const singleton = createSingleton(Symbol.for('@xtr-dev/payload-billing'))
type BillingPlugin = { type BillingPlugin = {
config: BillingPluginConfig config: BillingPluginConfig

View File

@@ -22,6 +22,10 @@ export interface Payment {
* Payment description * Payment description
*/ */
description?: string | null; description?: string | null;
/**
* Checkout URL where user can complete payment (if applicable)
*/
checkoutUrl?: string | null;
invoice?: (Id | null) | Invoice; invoice?: (Id | null) | Invoice;
/** /**
* Additional metadata for the payment * Additional metadata for the payment

View File

@@ -14,7 +14,7 @@ import {
import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency' import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency'
import { createContextLogger } from '../utils/logger' import { createContextLogger } from '../utils/logger'
const symbol = Symbol('mollie') const symbol = Symbol.for('@xtr-dev/payload-billing/mollie')
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0] export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
/** /**
@@ -155,6 +155,7 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
}); });
payment.providerId = molliePayment.id payment.providerId = molliePayment.id
payment.providerData = molliePayment.toPlainObject() payment.providerData = molliePayment.toPlainObject()
payment.checkoutUrl = molliePayment._links?.checkout?.href || null
return payment return payment
}, },
} satisfies PaymentProvider } satisfies PaymentProvider

View File

@@ -14,7 +14,7 @@ import {
import { isValidAmount, isValidCurrencyCode } from './currency' import { isValidAmount, isValidCurrencyCode } from './currency'
import { createContextLogger } from '../utils/logger' import { createContextLogger } from '../utils/logger'
const symbol = Symbol('stripe') const symbol = Symbol.for('@xtr-dev/payload-billing/stripe')
export interface StripeProviderConfig { export interface StripeProviderConfig {
secretKey: string secretKey: string

View File

@@ -6,7 +6,7 @@ import { handleWebhookError, logWebhookEvent } from './utils'
import { isValidAmount, isValidCurrencyCode } from './currency' import { isValidAmount, isValidCurrencyCode } from './currency'
import { createContextLogger } from '../utils/logger' import { createContextLogger } from '../utils/logger'
const TestModeWarningSymbol = Symbol('TestModeWarning') const TestModeWarningSymbol = Symbol.for('@xtr-dev/payload-billing/test-mode-warning')
const hasGivenTestModeWarning = () => TestModeWarningSymbol in globalThis const hasGivenTestModeWarning = () => TestModeWarningSymbol in globalThis
const setTestModeWarning = () => ((<any>globalThis)[TestModeWarningSymbol] = true) const setTestModeWarning = () => ((<any>globalThis)[TestModeWarningSymbol] = true)
@@ -492,6 +492,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
// Set provider ID and data // Set provider ID and data
payment.providerId = testPaymentId payment.providerId = testPaymentId
const paymentUrl = `${baseUrl}/api/payload-billing/test/payment/${testPaymentId}`
const providerData: ProviderData = { const providerData: ProviderData = {
raw: { raw: {
id: testPaymentId, id: testPaymentId,
@@ -500,7 +501,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
description: payment.description, description: payment.description,
status: 'pending', status: 'pending',
testMode: true, testMode: true,
paymentUrl: `${baseUrl}/api/payload-billing/test/payment/${testPaymentId}`, paymentUrl,
scenarios: scenarios.map(s => ({ id: s.id, name: s.name, description: s.description })), scenarios: scenarios.map(s => ({ id: s.id, name: s.name, description: s.description })),
methods: Object.entries(PAYMENT_METHODS).map(([key, value]) => ({ methods: Object.entries(PAYMENT_METHODS).map(([key, value]) => ({
id: key, id: key,
@@ -512,6 +513,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
provider: 'test' provider: 'test'
} }
payment.providerData = providerData payment.providerData = providerData
payment.checkoutUrl = paymentUrl
return payment return payment
}, },