From b44a80324117a84576ff5ee88f544d414dd95297 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:48:24 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 53 + primer-web-components/SKILL.md | 1886 +++++++++++++++++ .../references/component-reference.md | 625 ++++++ .../references/react-patterns.md | 562 +++++ 6 files changed, 3141 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 primer-web-components/SKILL.md create mode 100644 primer-web-components/references/component-reference.md create mode 100644 primer-web-components/references/react-patterns.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..ce9b5fa --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "primer-web-components", + "description": "Build checkout and payment experiences using Primer's web components. Covers React integration, SSR support, event handling, and CSS theming.", + "version": "0.0.0-2025.11.28", + "author": { + "name": "Primer", + "email": "developers@primer.io" + }, + "skills": [ + "./primer-web-components" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88ac7e2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# primer-web-components + +Build checkout and payment experiences using Primer's web components. Covers React integration, SSR support, event handling, and CSS theming. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..d984cc0 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:primer-io/examples:claude-code-skills", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "d85b8890beb7c4772b216c5c8faa7994e9e382f3", + "treeHash": "05c1ccf4fc763c8e385bd35e00e0a6499eb49145174d1f640f3bb6915ce3fbc4", + "generatedAt": "2025-11-28T10:27:40.987593Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "primer-web-components", + "description": "Build checkout and payment experiences using Primer's web components. Covers React integration, SSR support, event handling, and CSS theming.", + "version": null + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "313defa385a3291d5b95c8c09364253ee546cc2b84d77f23dd691e2978fbe52d" + }, + { + "path": "primer-web-components/SKILL.md", + "sha256": "66cfc0c9c34471b8a0e77e79bd5290f117366032a5ddfd426fe08c1747fe8e1f" + }, + { + "path": "primer-web-components/references/react-patterns.md", + "sha256": "ff70bab68af6fe271d6024634ecb9d3f79c59a824011889f24806c84bfa3a75c" + }, + { + "path": "primer-web-components/references/component-reference.md", + "sha256": "90ddceeda95fecf5af18ce19763f4b589e2f93127ec959d8d94ccd36774fa5b8" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "a7c14c05e5dfe1b76354e3a57644f65af41185e1a33269f38c48c2fd0efb97f3" + } + ], + "dirSha256": "05c1ccf4fc763c8e385bd35e00e0a6499eb49145174d1f640f3bb6915ce3fbc4" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/primer-web-components/SKILL.md b/primer-web-components/SKILL.md new file mode 100644 index 0000000..bf31cb5 --- /dev/null +++ b/primer-web-components/SKILL.md @@ -0,0 +1,1886 @@ +--- +name: primer-web-components +description: Build checkout and payment experiences using Primer's web components. Use this skill when implementing payment flows, checkout pages, card forms, or integrating Primer SDK into React, Next.js, or vanilla JavaScript applications. Covers component usage, React integration patterns, stable object references, event handling, SSR support, and CSS theming. +--- + +# Primer Web Components + +## Overview + +This skill provides comprehensive guidance for building checkout and payment experiences using Primer's web component library (`@primer-io/primer-js`). Primer components are framework-agnostic custom elements that work with React, Next.js, Vue, Svelte, or vanilla JavaScript. + +Use this skill when: + +- Implementing checkout pages or payment flows +- Integrating Primer payment methods (cards, PayPal, BLIK, Apple Pay, Google Pay, etc.) +- Building custom card forms with validation +- Working with React and need to handle web component integration properly +- Customizing payment UI with themes and CSS custom properties +- Implementing vault for saved payment methods +- Handling payment lifecycle events and callbacks + +## 🚨 Breaking Changes in v0.7.0 + +**Critical API Changes:** + +Starting in v0.7.0, the callback and event APIs have been updated for clearer separation of success and failure handling: + +- **Callbacks**: `onPaymentComplete` replaced with `onPaymentSuccess` and `onPaymentFailure` +- **State Fields**: `error` → `primerJsError`, `failure` → `paymentFailure` +- **Event Names**: `primer:payment-methods-updated` → use `primer:methods-update` + +**New in v0.7.0:** + +- Payment lifecycle events: `primer:payment-start`, `primer:payment-success`, `primer:payment-failure` +- Vault events: `primer:vault:methods-update` +- Vault callback: `onVaultedMethodsUpdate` +- PII-filtered payment data in success payloads + +All examples in this skill use the v0.7.0+ API. If using older SDK versions, refer to legacy documentation. + +## Quick Start Guide + +### Installation + +```bash +npm install @primer-io/primer-js +``` + +### Basic HTML Setup + +```html + + + + + + Primer Checkout + + + + + + +``` + +### Vanilla JavaScript Initialization + +```typescript +import { loadPrimer } from '@primer-io/primer-js'; +import { fetchClientToken } from './fetchClientToken'; + +(async function () { + await loadPrimer(); + + const checkout = document.querySelector('primer-checkout')!; + const response = await fetchClientToken('order-id'); + + if (response.success) { + checkout.setAttribute('client-token', response.clientToken); + } + + // Handle payment success and failure + checkout.addEventListener('primer:ready', (event) => { + const primer = event.detail; + + primer.onPaymentSuccess = ({ paymentSummary, paymentMethodType }) => { + console.log('✅ Payment successful!', paymentSummary.id); + window.location.href = `/confirmation?orderId=${paymentSummary.orderId}`; + }; + + primer.onPaymentFailure = ({ error }) => { + console.error('❌ Payment failed:', error.message); + // Show error to user + }; + }); +})(); +``` + +### React 19 Setup (Recommended) + +**TypeScript Configuration:** + +```typescript +import type { CheckoutElement } from '@primer-io/primer-js'; + +declare global { + namespace JSX { + interface IntrinsicElements { + 'primer-checkout': CheckoutElement; + } + } +} +``` + +**Component:** + +```typescript +import { useEffect } from 'react'; +import { loadPrimer } from '@primer-io/primer-js'; + +// ✅ Define options outside component for stable reference +const SDK_OPTIONS = { + locale: 'en-GB', + enabledPaymentMethods: [PaymentMethodType.PAYMENT_CARD], +}; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + useEffect(() => { + loadPrimer(); + }, []); + + return ( + + ); +} +``` + +## Component Architecture + +### Core Component Hierarchy + +``` +primer-checkout (root) +├── primer-main (layout container) +│ ├── slot="payments" (payment method selection) +│ ├── slot="checkout-complete" (success state) +│ └── slot="checkout-failure" (error state) +├── primer-payment-method (individual payment type) +├── primer-payment-method-container (declarative filtering) +├── primer-billing-address (billing information, SDK Core only) +├── primer-error-message-container (payment failure display) +└── primer-card-form (card payment inputs) + ├── primer-input-card-number + ├── primer-input-card-expiry + ├── primer-input-cvv + ├── primer-input-card-holder-name + ├── primer-card-form-submit + └── Custom fields using base components: + ├── primer-input-wrapper + ├── primer-input-label + └── primer-input +``` + +## SDK Modes: Core vs Legacy + +### SDK Core (Default since v0.4.0) + +The new payment engine with enhanced features. **This is the default and recommended for new integrations.** + +```javascript +// SDK Core is enabled by default +checkout.options = { + sdkCore: true, // Default, no need to specify +}; +``` + +**Currently Supported Payment Methods:** + +- `PAYMENT_CARD` - Full card payment forms +- `PAYPAL` - PayPal button integration +- `ADYEN_BLIK` - Polish payment method (OTP verification) + +New payment methods are added regularly. Check release notes for updates. + +**Benefits:** + +- Modern payment processing engine +- Enhanced performance and reliability +- New payment methods support +- Better error handling and diagnostics + +### Legacy SDK + +Enable with `sdkCore: false`. Provides access to 50+ payment methods via Web Headless API. + +```javascript +checkout.options = { + sdkCore: false, // Opt into legacy SDK +}; +``` + +**When to use:** + +- Need payment methods not yet in SDK Core +- Existing integration using legacy patterns +- Require specific processor-specific methods + +**Important:** Payment method availability depends on: + +1. Primer Dashboard configuration +2. Payment processor Web Headless support +3. Regional availability + +Not all payment methods support Web Headless. Check the [Primer Payment Methods catalog](https://primer.io/docs/connections/payment-methods/available-payment-methods) for "Web Headless" column. + +## SDK Options Reference + +### Core Options + +Configure SDK behavior through the `options` property: + +```javascript +checkout.options = { + // Core configuration + sdkCore: true, // Default: true (SDK Core enabled) + locale: 'en-US', // Force UI locale + merchantDomain: 'merchant.example.com', // For Apple Pay validation + disabledPayments: false, // Disable all payment methods + enabledPaymentMethods: [ + PaymentMethodType.PAYMENT_CARD, + PaymentMethodType.PAYPAL, + ], +}; +``` + +**Core Options:** + +| Option | Type | Default | Description | +| ----------------------- | --------------------- | ---------------------------------- | ------------------------------------ | +| `sdkCore` | `boolean` | `true` | Enable SDK Core engine | +| `locale` | `string` | Browser's locale | Force UI locale (e.g., "en-GB") | +| `merchantDomain` | `string` | `window.location.hostname` | Domain for Apple Pay validation | +| `disabledPayments` | `boolean` | `false` | Disable all payment methods globally | +| `enabledPaymentMethods` | `PaymentMethodType[]` | `[PaymentMethodType.PAYMENT_CARD]` | Which payment methods to display | + +### Card Options + +Configure card payment form behavior: + +```javascript +checkout.options = { + card: { + cardholderName: { + required: true, // Whether cardholder name is required + visible: true, // Whether cardholder name field is visible + }, + }, +}; +``` + +**Card Options:** + +| Option | Type | Default | Description | +| ------------------------------ | --------- | ------- | -------------------------- | +| `card.cardholderName.required` | `boolean` | `false` | Require cardholder name | +| `card.cardholderName.visible` | `boolean` | `true` | Show cardholder name field | + +### Apple Pay Options + +Configure Apple Pay button appearance and data collection: + +```javascript +checkout.options = { + applePay: { + buttonType: 'buy', // 'plain' | 'buy' | 'set-up' | 'donate' | 'check-out' | 'book' | 'subscribe' + buttonStyle: 'black', // 'white' | 'white-outline' | 'black' + billingOptions: { + requiredBillingContactFields: ['postalAddress', 'emailAddress'], + }, + shippingOptions: { + requiredShippingContactFields: ['postalAddress', 'name'], + requireShippingMethod: false, + }, + }, +}; +``` + +### Google Pay Options + +Configure Google Pay button appearance and data collection: + +```javascript +checkout.options = { + googlePay: { + buttonType: 'long', // 'long' | 'short' | 'book' | 'buy' | 'checkout' | 'donate' | 'order' | 'pay' | 'plain' | 'subscribe' + buttonColor: 'black', // 'default' | 'black' | 'white' + buttonSizeMode: 'fill', // 'fill' | 'static' + captureBillingAddress: true, + emailRequired: false, + requireShippingMethod: false, + }, +}; +``` + +### Klarna Options + +Configure Klarna payment behavior: + +```javascript +checkout.options = { + klarna: { + paymentFlow: 'DEFAULT', // 'DEFAULT' | 'PREFER_VAULT' + allowedPaymentCategories: ['pay_now', 'pay_later', 'pay_over_time'], + buttonOptions: { + text: 'Pay with Klarna', + }, + }, +}; +``` + +### Vault Options + +Configure payment method vaulting (saving for future use): + +```javascript +checkout.options = { + vault: { + enabled: true, // Enable vaulting + showEmptyState: true, // Show message when no vaulted methods exist + }, +}; +``` + +### Stripe Options + +Configure Stripe-specific payment options: + +```javascript +checkout.options = { + stripe: { + mandateData: { + fullMandateText: 'By providing your payment information...', + merchantName: 'Your Business Name', + }, + publishableKey: 'pk_test_...', + }, +}; +``` + +### Submit Button Options + +Configure submit button behavior: + +```javascript +checkout.options = { + submitButton: { + amountVisible: true, // Show amount on button (e.g., "Pay $12.34") + useBuiltInButton: true, // Default: true, set false for external buttons + }, +}; +``` + +**Using External Submit Buttons:** + +```javascript +// Hide built-in button +checkout.options = { + submitButton: { + useBuiltInButton: false, + }, +}; + +// Dispatch event to submit from external button +document.getElementById('my-button').addEventListener('click', () => { + document.dispatchEvent( + new CustomEvent('primer:card-submit', { + bubbles: true, + composed: true, + detail: { source: 'external-button' }, + }), + ); +}); +``` + +## PayPal Integration + +PayPal integration requires SDK Core (`sdkCore: true`, which is the default). + +### Basic Configuration + +```javascript +import { PaymentMethodType } from '@primer-io/primer-js'; + +checkout.options = { + sdkCore: true, // Default, required for PayPal + enabledPaymentMethods: [ + PaymentMethodType.PAYMENT_CARD, + PaymentMethodType.PAYPAL, + ], + paypal: { + style: { + layout: 'vertical', + color: 'gold', + shape: 'rect', + height: 45, + label: 'checkout', + }, + }, +}; +``` + +### Button Styling Options + +Customize PayPal button appearance: + +| Option | Type | Default | Description | +| ----------------- | ---------------------------------------------------------------------- | ------------ | ------------------------------------- | +| `layout` | `'vertical'` \| `'horizontal'` | `'vertical'` | Button layout orientation | +| `color` | `'gold'` \| `'blue'` \| `'silver'` \| `'white'` \| `'black'` | `'gold'` | Button color theme | +| `shape` | `'rect'` \| `'pill'` | `'rect'` | Button border shape | +| `height` | `number` (25-55) | `40` | Button height in pixels | +| `label` | `'paypal'` \| `'checkout'` \| `'buynow'` \| `'pay'` \| `'installment'` | `'paypal'` | Button label text | +| `tagline` | `boolean` | `false` | Show tagline (horizontal layout only) | +| `borderRadius` | `number` (0-55) | `4` | Button corner radius in pixels | +| `disableMaxWidth` | `boolean` | `false` | Disable maximum width constraint | + +**Styling Examples:** + +```javascript +// Horizontal blue pill buttons +paypal: { + style: { + layout: 'horizontal', + color: 'blue', + shape: 'pill', + height: 45, + label: 'checkout', + tagline: false, + } +} + +// Vertical silver buttons with custom border radius +paypal: { + style: { + layout: 'vertical', + color: 'silver', + shape: 'rect', + height: 50, + borderRadius: 8, + disableMaxWidth: true, + } +} +``` + +### Funding Source Control + +Control which PayPal funding sources are available: + +```javascript +paypal: { + disableFunding: ['credit', 'paylater', 'card'], // Hide these options + enableFunding: ['venmo'], // Explicitly enable Venmo +} +``` + +**Available Funding Sources:** + +- `card` - Guest card payments (credit/debit without PayPal account) +- `credit` - PayPal Credit (US, UK) +- `paylater` - PayPal Pay Later +- `venmo` - Venmo (US) + +**Funding Control Examples:** + +```javascript +// Only PayPal balance and bank account +paypal: { + disableFunding: ['card', 'credit', 'paylater', 'venmo'], +} + +// PayPal with Venmo only +paypal: { + disableFunding: ['card', 'credit', 'paylater'], + enableFunding: ['venmo'], +} +``` + +**Important:** `disableFunding` takes precedence over `enableFunding`. If a source appears in both arrays, it will be disabled. + +### PayPal Vaulting + +Enable vaulting to allow customers to save their PayPal account: + +```javascript +paypal: { + vault: true, // Enable vaulting in SDK +} +``` + +**Requirements:** + +Vaulting requires **both** SDK configuration and server-side setup: + +1. **SDK Configuration**: Set `vault: true` in PayPal options +2. **Client Session**: Configure `vaultOnSuccess: true` in your client session creation request + +**Legacy SDK**: For `sdkCore: false`, use: + +```javascript +paypal: { + paymentFlow: 'PREFER_VAULT', +} +``` + +### Complete PayPal Example + +```javascript +checkout.options = { + sdkCore: true, + enabledPaymentMethods: [ + PaymentMethodType.PAYMENT_CARD, + PaymentMethodType.PAYPAL, + ], + paypal: { + // Button styling + style: { + layout: 'vertical', + color: 'gold', + shape: 'pill', + height: 45, + label: 'checkout', + borderRadius: 6, + }, + + // Funding control + disableFunding: ['credit', 'card'], + enableFunding: ['venmo'], + + // Vaulting + vault: true, + }, +}; +``` + +## Events & Callbacks + +Primer Checkout uses an event-driven architecture with custom DOM events and callbacks. Events bubble up through the DOM, and callbacks provide direct handling of payment lifecycle. + +### Core Events + +#### `primer:ready` + +Dispatched when the Primer SDK is fully initialized and ready for use. + +**Event Detail:** Contains the PrimerJS instance with methods and callbacks. + +**Usage:** + +```javascript +const checkout = document.querySelector('primer-checkout'); + +checkout.addEventListener('primer:ready', (event) => { + const primer = event.detail; + console.log('✅ Primer SDK ready'); + + // Configure payment success handler + primer.onPaymentSuccess = ({ paymentSummary, paymentMethodType }) => { + console.log('✅ Payment successful', paymentSummary.id); + console.log('💳 Method:', paymentMethodType); + + // Access available payment data (PII-filtered) + if (paymentSummary.paymentMethodData?.last4Digits) { + console.log('Last 4:', paymentSummary.paymentMethodData.last4Digits); + } + + // Redirect to confirmation page + window.location.href = `/order/confirmation?id=${paymentSummary.orderId}`; + }; + + // Configure payment failure handler + primer.onPaymentFailure = ({ error, paymentMethodType }) => { + console.error('❌ Payment failed', error.message); + console.error('Error code:', error.code); + + // Log diagnostics ID for support + if (error.diagnosticsId) { + console.error('Diagnostics ID:', error.diagnosticsId); + } + + // Show error message and allow retry + showErrorMessage(error.message); + }; + + // Configure vaulted methods update handler + primer.onVaultedMethodsUpdate = ({ vaultedPayments }) => { + console.log('Vault updated:', vaultedPayments.size(), 'methods'); + updateVaultUI(vaultedPayments.toArray()); + }; +}); +``` + +#### `primer:state-change` + +Dispatched whenever the checkout state changes (processing, success, error, etc.). + +**Event Detail:** Contains `isProcessing`, `isSuccessful`, `isLoading`, `primerJsError`, `paymentFailure`. + +**Usage:** + +```javascript +checkout.addEventListener('primer:state-change', (event) => { + const { isProcessing, isSuccessful, primerJsError, paymentFailure } = + event.detail; + + if (isProcessing) { + console.log('⏳ Processing payment...'); + showLoadingSpinner(); + } else if (isSuccessful) { + console.log('✅ Payment successful!'); + hideLoadingSpinner(); + } else if (primerJsError || paymentFailure) { + const errorMessage = + primerJsError?.message || paymentFailure?.message || 'An error occurred'; + console.error('❌ Payment failed:', errorMessage); + + // Log error code for debugging + if (paymentFailure?.code) { + console.error('Error code:', paymentFailure.code); + } + + hideLoadingSpinner(); + showErrorMessage(errorMessage); + } +}); +``` + +**State Field Changes in v0.7.0:** + +- `error` → `primerJsError` (SDK-level errors) +- `failure` → `paymentFailure` (payment-level failures) + +#### `primer:methods-update` + +Dispatched when available payment methods are loaded and ready. + +**Event Detail:** Contains `InitializedPayments` instance with `toArray()` and `size()` methods. + +**Usage:** + +```javascript +checkout.addEventListener('primer:methods-update', (event) => { + const paymentMethods = event.detail.toArray(); + + console.log('Available payment methods:', paymentMethods); + console.log('Total methods:', paymentMethods.length); + + // Access individual method details + paymentMethods.forEach((method) => { + console.log('Method type:', method.type); + }); +}); +``` + +**Tip:** For most layout and filtering use cases, the `primer-payment-method-container` component provides a simpler declarative approach without requiring event listeners. + +### Payment Lifecycle Events (New in v0.7.0) + +Payment lifecycle events provide granular tracking of payment processing stages with detailed data payloads. + +#### `primer:payment-start` + +Dispatched when payment processing begins, immediately after the user initiates a payment. + +**Event Detail:** `undefined` (use as trigger signal only) + +**Usage:** + +```javascript +document.addEventListener('primer:payment-start', () => { + console.log('💳 Payment processing started'); + + // Show loading indicators + showPaymentLoadingSpinner(); + + // Disable form inputs to prevent duplicate submissions + disableFormInputs(); + + // Track payment initiation + analytics.track('Payment Started'); +}); +``` + +#### `primer:payment-success` + +Dispatched when a payment completes successfully. + +**Event Detail:** + +```typescript +{ + paymentSummary: PaymentSummary; // PII-filtered payment data + paymentMethodType: string; // e.g., 'PAYMENT_CARD', 'PAYPAL' + timestamp: number; // Unix timestamp of success +} +``` + +**PaymentSummary Structure:** + +Available fields (PII-filtered): + +- `id`: Payment ID +- `orderId`: Merchant order ID +- `paymentMethodType`: Type of payment method used +- `paymentMethodData`: Object containing non-sensitive card data + - `last4Digits`: Last 4 digits of card number (if applicable) + - `network`: Card network (Visa, Mastercard, etc.) + - `paymentMethodType`: Payment method type + +Filtered fields (not available): + +- `cardholderName`: Filtered for PII protection + +**Usage:** + +```javascript +document.addEventListener('primer:payment-success', (event) => { + const { paymentSummary, paymentMethodType, timestamp } = event.detail; + + console.log('✅ Payment successful!'); + console.log('Payment ID:', paymentSummary.id); + console.log('Order ID:', paymentSummary.orderId); + console.log('Method:', paymentMethodType); + console.log('Timestamp:', new Date(timestamp)); + + // Access available payment method data + if (paymentSummary.paymentMethodData?.last4Digits) { + console.log('Last 4 digits:', paymentSummary.paymentMethodData.last4Digits); + console.log('Network:', paymentSummary.paymentMethodData.network); + } + + // Track successful payment in analytics + analytics.track('Payment Successful', { + paymentId: paymentSummary.id, + orderId: paymentSummary.orderId, + method: paymentMethodType, + last4: paymentSummary.paymentMethodData?.last4Digits, + }); + + // Redirect to confirmation page + window.location.href = `/order/confirmation?id=${paymentSummary.orderId}`; +}); +``` + +**Important:** The `PaymentSummary` object filters sensitive information like cardholder names. Only use the provided non-sensitive fields for display and analytics. + +#### `primer:payment-failure` + +Dispatched when a payment fails or encounters an error. + +**Event Detail:** + +```typescript +{ + error: { + code: string; // Error code (e.g., 'CARD_DECLINED') + message: string; // User-friendly error message + diagnosticsId?: string; // Optional diagnostics ID for support + data?: any; // Optional additional error data + }; + paymentSummary?: PaymentSummary; // Optional, may be undefined + paymentMethodType: string; + timestamp: number; +} +``` + +**Usage:** + +```javascript +document.addEventListener('primer:payment-failure', (event) => { + const { error, paymentSummary, paymentMethodType, timestamp } = event.detail; + + console.error('❌ Payment failed'); + console.error('Error code:', error.code); + console.error('Error message:', error.message); + + if (error.diagnosticsId) { + console.error('Diagnostics ID:', error.diagnosticsId); + } + + // Display error message to user + showErrorMessage(error.message); + + // Track payment failure in analytics + analytics.track('Payment Failed', { + errorCode: error.code, + errorMessage: error.message, + diagnosticsId: error.diagnosticsId, + method: paymentMethodType, + timestamp: new Date(timestamp), + }); + + // Send to error tracking service + if (error.diagnosticsId) { + errorTracker.capturePaymentFailure({ + diagnosticsId: error.diagnosticsId, + code: error.code, + paymentMethodType, + }); + } +}); +``` + +### Vault Events (New in v0.7.0) + +#### `primer:vault:methods-update` + +Dispatched when vaulted payment methods are loaded, updated, or when the vault state changes. + +**Event Detail:** + +```typescript +{ + vaultedPayments: InitializedVaultedPayments; // Vault API instance + timestamp: number; // Unix timestamp +} +``` + +**InitializedVaultedPayments API:** + +- `toArray()`: Returns array of `VaultedPaymentMethodSummary` objects +- `get(id: string)`: Gets a specific vaulted payment method by ID +- `size()`: Returns the number of saved payment methods + +**VaultedPaymentMethodSummary Structure:** + +- `id`: Unique identifier for the vaulted payment method +- `analyticsId`: Analytics tracking identifier +- `paymentMethodType`: Type of payment method (e.g., 'PAYMENT_CARD', 'ADYEN_STRIPE_ACH') +- `paymentInstrumentType`: Instrument type +- `paymentInstrumentData`: Object with PII-filtered payment instrument details + - `last4Digits`: Last 4 digits of card (cards only) + - `network`: Card network like VISA, MASTERCARD (cards only) + - `accountNumberLastFourDigits`: Last 4 of account number (ACH only) + - `bankName`: Bank name (ACH only) + - `accountType`: CHECKING or SAVINGS (ACH only) + - `email`: Email address (wallet methods like PayPal) +- `userDescription`: Optional user-provided description + +**Important:** Sensitive fields like cardholder names, expiration dates, and full account numbers are filtered out for security. + +**Usage:** + +```javascript +document.addEventListener('primer:vault:methods-update', (event) => { + const { vaultedPayments, timestamp } = event.detail; + + console.log('💳 Vault methods updated'); + console.log('Total saved methods:', vaultedPayments.size()); + + // Get all saved payment methods + const methods = vaultedPayments.toArray(); + + methods.forEach((method) => { + console.log('Method ID:', method.id); + console.log('Type:', method.paymentMethodType); + + if (method.paymentInstrumentData) { + console.log('Last 4:', method.paymentInstrumentData.last4Digits); + console.log('Network:', method.paymentInstrumentData.network); + } + }); + + // Update UI with saved methods + updateVaultDisplay(methods); + + // Track vault updates in analytics + analytics.track('Vault Methods Updated', { + count: methods.length, + timestamp, + }); +}); +``` + +### Card Events + +Card events are specific to card payment form interactions and validation. + +#### `primer:card-success` + +Dispatched when a card form is successfully validated and submitted. + +**Event Detail:** Contains `result` object with payment submission data. + +**Usage:** + +```javascript +checkout.addEventListener('primer:card-success', (event) => { + const result = event.detail.result; + console.log('✅ Card form submitted successfully', result); + + // Disable form to prevent duplicate submissions + disableCardForm(); + + // Show intermediate success message + showMessage('Processing your payment...'); +}); +``` + +#### `primer:card-error` + +Dispatched when card validation fails or submission encounters an error. + +**Event Detail:** Contains `errors` array with validation error objects. + +**Usage:** + +```javascript +checkout.addEventListener('primer:card-error', (event) => { + const errors = event.detail.errors; + console.error('❌ Card validation errors:', errors); + + // Log each error + errors.forEach((error) => { + console.error(`${error.field}: ${error.error}`); + }); + + // Display custom error UI + displayValidationErrors(errors); +}); +``` + +#### `primer:card-network-change` + +Dispatched when the card network (Visa, Mastercard, etc.) is detected or changes based on the card number input. + +**Event Detail:** Contains `detectedCardNetwork`, `selectableCardNetworks`, and `isLoading`. + +**Usage:** + +```javascript +checkout.addEventListener('primer:card-network-change', (event) => { + const { detectedCardNetwork, selectableCardNetworks, isLoading } = + event.detail; + + if (isLoading) { + console.log('🔍 Detecting card network...'); + return; + } + + if (detectedCardNetwork) { + const network = detectedCardNetwork.network; + console.log('💳 Card network detected:', network); + + // Show card brand logo + updateCardBrandLogo(network); + + // Track card network detection + analytics.track('Card Network Detected', { network }); + } +}); +``` + +### Triggerable Events + +Triggerable events are events that YOU dispatch to control SDK behavior. + +#### `primer:card-submit` + +Trigger card form submission programmatically from anywhere in your application. + +**Event Detail:** Optional `source` property to identify the trigger source. + +**Usage:** + +The checkout component listens for this event at the document level, so you can dispatch it from anywhere without referencing the card form element directly. + +```javascript +// Trigger card form submission from anywhere +document.dispatchEvent( + new CustomEvent('primer:card-submit', { + bubbles: true, + composed: true, + detail: { source: 'custom-button' }, + }), +); +``` + +**Complete Example: External Submit Button** + +```html + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + +``` + +**Important:** + +- The `bubbles: true` and `composed: true` properties are required +- Always include a meaningful `source` parameter for debugging +- The checkout component handles the event at document level and forwards it internally + +## Vault Integration + +Vault allows customers to save payment methods for future use. + +### Configuration + +```javascript +checkout.options = { + vault: { + enabled: true, // Enable vaulting + showEmptyState: true, // Show empty state message when no saved methods + }, +}; +``` + +### Vault Events + +Use the `primer:vault:methods-update` event to respond to vault changes: + +```javascript +document.addEventListener('primer:vault:methods-update', (event) => { + const { vaultedPayments, timestamp } = event.detail; + + console.log('Total saved methods:', vaultedPayments.size()); + + // Get all methods + const methods = vaultedPayments.toArray(); + methods.forEach((method) => { + console.log(`${method.network} ending in ${method.last4Digits}`); + }); + + // Get specific method + const method = vaultedPayments.get('payment-method-id'); + if (method) { + console.log('Found method:', method); + } +}); +``` + +### Vault Callback + +Use the callback for direct vault handling in the `primer:ready` event: + +```javascript +checkout.addEventListener('primer:ready', (event) => { + const primer = event.detail; + + primer.onVaultedMethodsUpdate = ({ vaultedPayments }) => { + console.log('Vault updated:', vaultedPayments.size(), 'methods'); + updateVaultUI(vaultedPayments.toArray()); + }; +}); +``` + +### Complete Vault Example + +```html + + +
+ + +
+
+
+ + +``` + +## React Integration Patterns + +### Critical: Stable Object References + +**THE MOST COMMON MISTAKE** with Primer in React is creating new object references on every render, causing component re-initialization and loss of user input. + +This applies to **BOTH React 18 AND React 19**. + +### React 18 vs React 19 Comparison + +React 19 introduced improved support for web components, but the need for stable references remains critical. + +| Aspect | React 18 | React 19 | +| ----------------------------- | ------------------------------------- | --------------------- | +| **How objects passed** | ref + useEffect | JSX props | +| **Attribute conversion** | Converts objects to `[object Object]` | Assigns as properties | +| **Code pattern** | Imperative | Declarative | +| **Lines of code** | ~15 lines | ~5 lines | +| **Stable references needed?** | ✅ Yes (always) | ✅ Yes (always) | +| **Can inline objects?** | ❌ No (doesn't work) | ❌ No (causes issues) | + +### ALL Three Stable Reference Patterns + +#### Pattern 1: Constant Outside Component (For Static Options) + +```typescript +// ✅ Created once at module load, same reference forever +const SDK_OPTIONS = { + locale: 'en-GB', + card: { + cardholderName: { + required: true, + visible: true, + }, + }, +}; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + // React 19 example + return ; +} +``` + +**When to use:** Options are static and don't depend on props, state, or user input + +**Benefits:** + +- ✅ Zero re-render overhead +- ✅ Simplest pattern +- ✅ No React hooks needed + +#### Pattern 2: useMemo for Dynamic Options + +```typescript +import { useMemo } from 'react'; + +function CheckoutPage({ clientToken, userLocale, merchantName }: Props) { + // ✅ Creates new object ONLY when dependencies change + const sdkOptions = useMemo( + () => ({ + locale: userLocale, + applePay: { + merchantName: merchantName, + merchantCountryCode: 'GB', + }, + }), + [userLocale, merchantName] // Only recreate when these change + ); + + // React 19 example + return ; +} +``` + +**When to use:** Options depend on props, state, or context that can change + +**Benefits:** + +- ✅ Stable reference until dependencies change +- ✅ Only re-initializes when necessary +- ✅ Prevents unnecessary re-renders + +#### Pattern 3: Common Mistakes to Avoid + +```typescript +// ❌ WRONG: Inline object in JSX +function CheckoutPage() { + // New object on every render + return ; +} + +// ❌ WRONG: Object in component body +function CheckoutPage() { + // New object on every render + const options = { locale: 'en-GB' }; + return ; +} + +// ✅ CORRECT: Use constant or useMemo +const SDK_OPTIONS = { locale: 'en-GB' }; + +function CheckoutPage() { + // Same object reference every render + return ; +} + +// ✅ CORRECT: Use useMemo for empty deps +function CheckoutPage() { + const options = useMemo(() => ({ locale: 'en-GB' }), []); + return ; +} +``` + +### TypeScript Setup + +Show both patterns: + +**Pattern 1: CheckoutElement** + +```typescript +import type { CheckoutElement } from '@primer-io/primer-js'; + +declare global { + namespace JSX { + interface IntrinsicElements { + 'primer-checkout': CheckoutElement; + } + } +} +``` + +**Pattern 2: SDK Options Type** + +```typescript +import type { PrimerCheckoutOptions } from '@primer-io/primer-js'; + +const options: PrimerCheckoutOptions = { + locale: 'en-GB', + enabledPaymentMethods: [PaymentMethodType.PAYMENT_CARD], +}; +``` + +### React 18 Pattern (For Legacy Apps) + +For React 18, you must use refs and useEffect: + +```typescript +import { useRef, useEffect } from 'react'; + +// ✅ Define options outside component or use useMemo +const SDK_OPTIONS = { locale: 'en-GB' }; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + const checkoutRef = useRef(null); + + useEffect(() => { + const checkout = checkoutRef.current; + if (!checkout) return; + + // Imperative property assignment + checkout.options = SDK_OPTIONS; + + // Set up event listeners + const handleReady = () => console.log('✅ SDK ready'); + checkout.addEventListener('primer:ready', handleReady); + + return () => { + checkout.removeEventListener('primer:ready', handleReady); + }; + }, []); // Empty deps - runs once + + return ; +} +``` + +### React 19 Pattern (Recommended) + +React 19 allows direct JSX property assignment: + +```typescript +// ✅ Define options outside component or use useMemo +const SDK_OPTIONS = { locale: 'en-GB' }; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + return ; +} +``` + +**Critical:** Keep the constant! React 19 doesn't eliminate the need for stable references. + +## Server-Side Rendering (SSR) + +Primer Checkout requires browser APIs (Web Components, DOM) and must load client-side only. + +### Why SSR Requires Special Handling + +The SDK depends on: + +- Web Components API (`customElements.define()`) +- DOM APIs for component rendering +- Browser context for iframes and payment processing +- `window` object + +These don't exist in Node.js (server) environment. + +### Next.js + +#### App Router (Next.js 13+) + +```typescript +'use client'; + +import { useEffect } from 'react'; +import { loadPrimer } from '@primer-io/primer-js'; + +export default function CheckoutPage() { + useEffect(() => { + if (typeof window !== 'undefined') { + loadPrimer().catch(console.error); + } + }, []); + + return ; +} +``` + +The `'use client'` directive marks this component as client-side only. + +#### Pages Router (Legacy) + +```typescript +import { useEffect } from 'react'; +import { loadPrimer } from '@primer-io/primer-js'; + +function CheckoutPage() { + useEffect(() => { + if (typeof window !== 'undefined') { + const initializePrimer = async () => { + try { + await loadPrimer(); + console.log('✅ Primer loaded'); + } catch (error) { + console.error('❌ Failed to load Primer:', error); + } + }; + + initializePrimer(); + } + }, []); + + return ; +} + +export default CheckoutPage; +``` + +### Nuxt.js 3 + +```vue + + + +``` + +**Note:** Use `import.meta.client` (modern Nuxt 3) instead of `process.client` (legacy Nuxt 2). + +### SvelteKit + +```svelte + + + +``` + +### Best Practices + +1. **Always use framework lifecycle methods** (useEffect, onMounted, onMount) +2. **Include environment checks** (`typeof window`, `import.meta.client`, `browser`) +3. **Use dynamic imports** to prevent server bundling +4. **Wrap in try-catch** for error handling +5. **Use stable references** for options objects (apply to all frameworks) + +## Error Handling + +### Payment Failure vs Validation Errors + +**Validation Errors:** + +- Handled by input components themselves +- Prevent form submission until fixed +- Displayed inline by card inputs +- No action needed from you + +**Payment Failures:** + +- Occur after form submission +- Displayed via `` or custom handling +- Require user action (retry, change payment method) + +### Using Error Message Container + +```html + + +
+ + + + +
+
+
+``` + +**Placement Guidelines:** + +1. Prominently visible after payment attempt +2. Where users naturally look for feedback +3. Within same visual context as payment method + +### Custom Error Handling + +**Using Callbacks:** + +```javascript +checkout.addEventListener('primer:ready', (event) => { + const primer = event.detail; + + primer.onPaymentFailure = ({ error, paymentMethodType }) => { + // Display custom error UI + showErrorNotification({ + title: 'Payment Failed', + message: error.message, + allowRetry: true, + }); + + // Log for debugging + console.error('Payment failed:', { + code: error.code, + message: error.message, + diagnosticsId: error.diagnosticsId, // For support + method: paymentMethodType, + }); + + // Send to error tracking + errorTracker.capture({ + errorCode: error.code, + diagnosticsId: error.diagnosticsId, + }); + }; +}); +``` + +**Using State Change Event:** + +```javascript +checkout.addEventListener('primer:state-change', (event) => { + const { primerJsError, paymentFailure } = event.detail; + + if (primerJsError || paymentFailure) { + const message = primerJsError?.message || paymentFailure?.message; + showErrorMessage(message); + + // Log diagnostics ID for support + if (paymentFailure?.diagnosticsId) { + console.error('Diagnostics ID:', paymentFailure.diagnosticsId); + } + } +}); +``` + +**Using Payment Failure Event:** + +```javascript +document.addEventListener('primer:payment-failure', (event) => { + const { error, paymentMethodType } = event.detail; + + // Show user-friendly error + showErrorMessage(error.message); + + // Track in analytics + analytics.track('Payment Failed', { + errorCode: error.code, + method: paymentMethodType, + }); + + // Log for debugging + if (error.diagnosticsId) { + console.error('Diagnostics ID for support:', error.diagnosticsId); + } +}); +``` + +## Component Properties vs SDK Options + +### Why the Distinction Exists + +Component properties use Lit's attribute system which monitors DOM attribute changes. Direct property assignment bypasses this system, causing values to be ignored. The `options` property is the ONLY exception - it's designed to accept direct property assignment. + +### Component Properties (use `setAttribute()`) + +These are HTML attributes set via `setAttribute()`: + +- `client-token` - JWT from backend (REQUIRED) +- `custom-styles` - JSON string of CSS variables +- `loader-disabled` - Boolean to disable loader + +```javascript +checkout.setAttribute('client-token', 'your-token'); +checkout.setAttribute('loader-disabled', 'true'); +checkout.setAttribute( + 'custom-styles', + JSON.stringify({ primerColorBrand: '#4a6cf7' }), +); +``` + +### SDK Options (use property assignment) + +Everything else goes in the `options` object: + +- Locale, payment methods, vault configuration, etc. + +```javascript +checkout.options = { + locale: 'en-GB', + enabledPaymentMethods: [PaymentMethodType.PAYMENT_CARD], + vault: { enabled: true }, +}; +``` + +### Debugging Tip + +```javascript +// Check if using correctly +checkout.getAttribute('client-token'); // Should return token +checkout.options; // Should return options object + +// Common mistake +checkout.getAttribute('locale'); // Returns null (locale is in options!) +``` + +**Remember:** Never mix these up. Component properties use `setAttribute()`, SDK options use direct property assignment. + +## Preventing Flash of Undefined Components (FOUC) + +Web components register via JavaScript. Before registration, custom elements may flash as undefined. + +### CSS Solution (Simple) + +```css +primer-checkout:has(:not(:defined)) { + visibility: hidden; +} +``` + +Use `visibility: hidden` (not `display: none`) to preserve layout space. + +### JavaScript Solution (More Control) + +```javascript +Promise.allSettled([ + customElements.whenDefined('primer-checkout'), + customElements.whenDefined('primer-payment-method'), +]).then(() => { + document.querySelector('.checkout-container').classList.add('ready'); +}); +``` + +```css +.checkout-container { + visibility: hidden; +} + +.checkout-container.ready { + visibility: visible; +} +``` + +## CSS Theming + +### Custom Properties + +Apply via CSS: + +```css +:root { + --primer-color-brand: #2f98ff; + --primer-radius-base: 8px; + --primer-typography-brand: 'Inter, sans-serif'; + --primer-space-base: 4px; +} + +/* Or scope to specific checkout */ +primer-checkout { + --primer-color-brand: #4a6cf7; +} +``` + +Or via `custom-styles` attribute: + +```html + +``` + +### Dark Theme + +```css +primer-checkout.primer-dark-theme { + --primer-color-text-primary: var(--primer-color-gray-100); + --primer-color-background-outlined-default: var(--primer-color-gray-800); +} +``` + +```javascript +// Apply theme +const checkout = document.querySelector('primer-checkout'); +checkout.classList.add('primer-dark-theme'); +``` + +## Common Use Cases + +### 1. Default Checkout (Simplest) + +```html + +``` + +This provides a complete checkout experience with all available payment methods. + +### 2. Custom Payment Method Layout + +```html + + +
+

Choose Payment Method

+ + + + + + + +
+ +
+

Thank you for your order!

+
+
+
+``` + +### 3. Declarative Payment Filtering + +```html +
+ + + + + + + +
+``` + +### 4. Custom Card Form + +```html + +
+ + +
+ + +
+ + + + + + Billing Zip + + + + +
+
+``` + +## Best Practices + +1. **Always use stable object references** in React (module-level constants or `useMemo`) +2. **Set component properties via `setAttribute()`**, SDK options via property assignment +3. **Clean up event listeners** in React `useEffect` cleanup functions +4. **Use declarative containers** (`primer-payment-method-container`) instead of manual filtering +5. **Include error handling** with `primer-error-message-container` or custom callbacks +6. **Load Primer in `useEffect`** (or equivalent) for SSR frameworks +7. **Use TypeScript declarations** for proper JSX support +8. **Keep SDK options simple** - only configure what you need +9. **Use v0.7.0+ callbacks** (`onPaymentSuccess`, `onPaymentFailure`) for clearer error handling +10. **Track diagnosticsId** in payment failures for support inquiries + +## Common Troubleshooting + +### Component re-initializing on every render? + +→ Check object reference stability. Use module-level constants or `useMemo`. +→ In React 19, ensure options object has stable reference. +→ Applies to BOTH React 18 AND React 19. + +### TypeScript errors with JSX? + +→ Add TypeScript declarations: `import type { CheckoutElement } from '@primer-io/primer-js'` +→ Declare in global JSX namespace or use `CustomElements` type + +### SSR errors ("customElements is not defined", "window is not defined")? + +→ Load Primer in client-side lifecycle: `useEffect`, `onMounted`, `onMount` +→ Use `'use client'` directive in Next.js App Router +→ Add environment checks: `typeof window !== 'undefined'` +→ Use dynamic imports: `await import('@primer-io/primer-js')` + +### Event not firing? + +→ Ensure component is mounted before adding listener +→ Use `useEffect` in React, wait for `primer:ready` +→ Check event name (v0.7.0 renamed some events) + +### Payment methods not showing? + +→ Check client token is valid +→ Check `enabledPaymentMethods` configuration +→ Wait for `primer:ready` event before accessing SDK +→ Verify methods are configured in Primer Dashboard +→ Check SDK Core vs Legacy mode compatibility + +### Options not applying? + +→ Check you're using `checkout.options = {...}`, not `setAttribute` +→ Verify object has stable reference in React +→ Check SDK Core vs Legacy mode compatibility +→ Never set `client-token` in options (it's a component property) + +### Styling not applying? + +→ CSS custom properties pierce Shadow DOM +→ Use `--primer-*` variables +→ Check specificity and scoping +→ Apply to `primer-checkout` element or `:root` + +### Infinite re-renders in React? + +→ Inline object in JSX: `options={{ locale: 'en-GB' }}` - use constant or useMemo +→ Object in component body without useMemo +→ Dependencies missing in useMemo array +→ This happens in BOTH React 18 AND React 19 + +### "Cannot set property options of HTMLElement"? + +→ Component not yet registered, wait for `primer:ready` +→ Or ensure `loadPrimer()` was called +→ Use `customElements.whenDefined('primer-checkout')` to wait + +### Payment failures not displaying? + +→ Include `` in your layout +→ Or implement custom error handling with `onPaymentFailure` callback +→ Or listen to `primer:payment-failure` event +→ Check `primerJsError` and `paymentFailure` in state change events + +### Vaulted methods not appearing? + +→ Check `vault.enabled: true` in options +→ Verify client session has `vaultOnSuccess: true` +→ Listen to `primer:vault:methods-update` event +→ Use `onVaultedMethodsUpdate` callback for updates + +### PayPal button not showing? + +→ Check `sdkCore: true` (required for PayPal) +→ Include `PaymentMethodType.PAYPAL` in `enabledPaymentMethods` +→ Verify PayPal is configured in Primer Dashboard +→ Check browser console for PayPal SDK errors + +## Resources + +For always up-to-date documentation, this skill references the Primer Checkout documentation covering: + +- Component APIs and properties +- SDK options and configuration +- Event payloads and callbacks +- Payment lifecycle handling +- Vault integration patterns +- React integration patterns (React 18 & 19) +- SSR framework patterns (Next.js, Nuxt, SvelteKit) +- CSS theming and customization +- TypeScript type definitions + +For the latest component APIs, patterns, and examples, use Context7 MCP server: + +```typescript +// Resolve library +const library = await resolveLibraryId('primer checkout components'); +// Returns: /primer-io/examples + +// Fetch documentation +const docs = await getLibraryDocs('/primer-io/examples', { + topic: 'payment lifecycle events', + tokens: 10000, +}); +``` + +This ensures access to the most current component APIs, v0.7.0+ features, and integration patterns. diff --git a/primer-web-components/references/component-reference.md b/primer-web-components/references/component-reference.md new file mode 100644 index 0000000..8076761 --- /dev/null +++ b/primer-web-components/references/component-reference.md @@ -0,0 +1,625 @@ +# Primer Web Components - Component Reference + +## Core Components + +### `` + +The root component that initializes the Primer SDK and manages the checkout flow. + +**Required Attributes:** + +- `client-token` - JWT token from backend client session + +**Optional Attributes:** + +- `custom-styles` - JSON string of CSS custom properties +- `loader-disabled` - Boolean to disable loading indicator + +**Properties (set via JavaScript):** + +- `options` - SDK configuration object (locale, payment methods, etc.) + +**Example:** + +```html + + + + + +``` + +### `` + +Container for checkout content with predefined slots for different states. + +**Slots:** + +- `payments` - Main payment method selection area +- `checkout-complete` - Success state content +- `checkout-failure` - Error state content + +**Example:** + +```html + +
+ +
+
+

Payment Successful!

+
+
+``` + +### `` + +Displays a specific payment method. + +**Attributes:** + +- `type` - Payment method type (PAYMENT_CARD, PAYPAL, APPLE_PAY, GOOGLE_PAY, etc.) +- `disabled` - Boolean to disable the payment method + +**Example:** + +```html + + + +``` + +### `` + +Declarative container for automatically rendering filtered payment methods. + +**Attributes:** + +- `include` - Comma-separated list of payment method types to include +- `exclude` - Comma-separated list of payment method types to exclude + +**Example:** + +```html + + + + + +``` + +## Card Form Components + +### `` + +Container for card payment inputs with built-in validation and state management. + +**Slots:** + +- `card-form-content` - Custom content area for inputs + +**Example:** + +```html + +
+ + + + +
+
+``` + +### `` + +Secure card number input field with automatic card type detection. + +**DOM Structure:** + +```html + + Card Number +
+ + +
+
+``` + +### `` + +Expiration date input with automatic formatting (MM/YY). + +### `` + +CVV/security code input field. + +### `` + +Cardholder name text input (not in secure iframe). + +### `` + +Localized submit button for card forms. + +### `` + +Collects billing address information (SDK Core only). + +**Attributes:** + +- Mode configuration (drop-in or custom layout) + +**Example:** + +```html + +
+ + + +
+
+``` + +## Base UI Components + +### `` + +Container that provides consistent styling for form inputs. + +**Attributes:** + +- `has-error` - Boolean to show error state + +**Slots:** + +- `label` - For primer-input-label +- `input` - For primer-input or custom content +- `error` - For primer-input-error + +**Example:** + +```html + + Email + + Invalid email + +``` + +### `` + +Standard input field with consistent styling. + +**Attributes:** + +- Standard HTML input attributes: `type`, `value`, `placeholder`, `disabled`, `required`, etc. +- `name` - For form data collection + +**Events:** + +- `input` - Value changed +- `change` - Value committed (blur/Enter) +- `focus` - Input focused +- `blur` - Input blurred +- `invalid` - Validation failed + +### `` + +Form label component. + +**Attributes:** + +- `for` - ID of associated input +- `disabled` - Boolean for disabled state + +### `` + +Error message component. + +**Attributes:** + +- `for` - ID of associated input +- `active` - Boolean to show/hide error + +### `` + +Styled button component. + +**Attributes:** + +- `variant` - "primary" or "secondary" +- `buttonType` - "button" or "submit" +- `disabled` - Boolean + +**Example:** + +```html +Pay Now +Cancel +``` + +## Utility Components + +### `` + +Automatically displays payment processing errors. + +**Example:** + +```html +
+ + +
+``` + +### `` + +Displays saved payment methods when vault is enabled. + +**Requires:** + +- Vault enabled in SDK options: `{"vault": {"enabled": true}}` + +### `` + +Manages visibility of alternative payment methods when vault is active. + +**Slots:** + +- `other-payments` - Content shown when "Show other payments" is clicked + +## Events + +### Core Checkout Events + +**`primer:ready`** + +- Fired when SDK initialization completes +- Detail: PrimerJS instance with callbacks: + - `onPaymentSuccess` - New in v0.7.0 + - `onPaymentFailure` - New in v0.7.0 + - `onVaultedMethodsUpdate` - New in v0.7.0 + - `onPaymentStart`, `onPaymentPrepare` + - `refreshSession()`, `getPaymentMethods()` + +**Example:** + +```javascript +checkout.addEventListener('primer:ready', (event) => { + const primer = event.detail; + + // Set up callbacks + primer.onPaymentSuccess = ({ paymentSummary, paymentMethodType }) => { + console.log('Payment successful!'); + }; + + primer.onPaymentFailure = ({ error, paymentMethodType }) => { + console.error('Payment failed:', error.message); + }; +}); +``` + +**`primer:state-change`** + +- Fired when checkout state changes +- Detail: `{isProcessing, isSuccessful, isLoading, primerJsError, paymentFailure}` +- Note: `error` → `primerJsError`, `failure` → `paymentFailure` (v0.7.0) + +**Example:** + +```javascript +checkout.addEventListener('primer:state-change', (event) => { + const { isProcessing, isSuccessful, primerJsError, paymentFailure } = + event.detail; + if (primerJsError) { + console.error('SDK error:', primerJsError); + } + if (paymentFailure) { + console.error('Payment failed:', paymentFailure); + } +}); +``` + +**`primer:methods-update`** + +- Fired when available payment methods change +- Detail: InitializedPayments instance with `.toArray()`, `.get()`, `.size()` methods +- Note: Replaces `primer-payment-methods-updated` + +**Example:** + +```javascript +checkout.addEventListener('primer:methods-update', (event) => { + const methods = event.detail.toArray(); + console.log(`${methods.length} payment methods available`); + + // Get specific method + const cardMethod = event.detail.get('PAYMENT_CARD'); +}); +``` + +### Payment Lifecycle Events (New in v0.7.0) + +**`primer:payment-start`** + +- Fired when payment processing begins +- Detail: undefined + +**`primer:payment-success`** + +- Fired when payment completes successfully +- Detail: `{paymentSummary, paymentMethodType, timestamp}` +- PaymentSummary is PII-filtered (safe for client-side) + +**Example:** + +```javascript +checkout.addEventListener('primer:payment-success', (event) => { + const { paymentSummary, paymentMethodType, timestamp } = event.detail; + console.log(`✅ Payment successful via ${paymentMethodType}`); + console.log( + `Card: ${paymentSummary.network} ending in ${paymentSummary.last4Digits}`, + ); + // Navigate to success page +}); +``` + +**`primer:payment-failure`** + +- Fired when payment fails +- Detail: `{error: {code, message, diagnosticsId}, paymentSummary?, paymentMethodType, timestamp}` + +**Example:** + +```javascript +checkout.addEventListener('primer:payment-failure', (event) => { + const { error, paymentMethodType } = event.detail; + console.error(`❌ Payment failed: ${error.message}`); + console.error(`Diagnostics ID: ${error.diagnosticsId}`); + // Show error to user +}); +``` + +### Vault Events (New in v0.7.0) + +**`primer:vault:methods-update`** + +- Fired when vaulted payment methods loaded/updated +- Detail: `{vaultedPayments, timestamp}` +- vaultedPayments API: `.toArray()`, `.get(id)`, `.size()` + +**Example:** + +```javascript +checkout.addEventListener('primer:vault:methods-update', (event) => { + const { vaultedPayments } = event.detail; + console.log(`${vaultedPayments.size()} saved payment methods`); + + vaultedPayments.toArray().forEach((method) => { + console.log( + `${method.paymentInstrumentType}: ${method.paymentInstrumentData.last4Digits}`, + ); + }); +}); +``` + +### Card Events + +**`primer:card-network-change`** + +- Fired when card network detected +- Detail: `{detectedCardNetwork, selectableCardNetworks, isLoading}` + +**Example:** + +```javascript +cardForm.addEventListener('primer:card-network-change', (event) => { + const { detectedCardNetwork, selectableCardNetworks } = event.detail; + console.log(`Detected: ${detectedCardNetwork}`); +}); +``` + +**`primer:card-success`** + +- Fired when card form submission succeeds +- Detail: `{result}` + +**`primer:card-error`** + +- Fired when card validation errors occur +- Detail: `{errors: InputValidationError[]}` + +**`primer:card-submit`** (Triggerable) + +- Dispatch this event to trigger card form submission programmatically +- Detail: `{source?: string}` + +**Example:** + +```javascript +// Trigger card form submission from external button +const cardForm = document.querySelector('primer-card-form'); +cardForm.dispatchEvent( + new CustomEvent('primer:card-submit', { + detail: { source: 'external-button' }, + }), +); +``` + +## SDK Options Structure + +```typescript +interface SDKOptions { + // Core Options + sdkCore?: boolean; // Default: true since v0.4.0 + locale?: string; // e.g., 'en-GB', 'fr-FR' + merchantDomain?: string; // For Apple Pay domain validation + disabledPayments?: boolean; // Disable all payment methods + enabledPaymentMethods?: PaymentMethodType[]; // Filter which methods display + + // Card Options + card?: { + cardholderName?: { + required?: boolean; + visible?: boolean; + }; + }; + + // Apple Pay Options + applePay?: { + buttonType?: + | 'buy' + | 'donate' + | 'plain' + | 'checkout' + | 'set-up' + | 'book' + | 'subscribe'; + buttonStyle?: 'black' | 'white' | 'white-outline'; + billingOptions?: { + requiredBillingContactFields?: ( + | 'emailAddress' + | 'name' + | 'phoneNumber' + | 'postalAddress' + | 'phoneticName' + )[]; + }; + shippingOptions?: { + requiredShippingContactFields?: ( + | 'emailAddress' + | 'name' + | 'phoneNumber' + | 'postalAddress' + | 'phoneticName' + )[]; + requireShippingMethod?: boolean; + }; + }; + + // Google Pay Options + googlePay?: { + buttonType?: + | 'long' + | 'short' + | 'book' + | 'buy' + | 'checkout' + | 'donate' + | 'order' + | 'pay' + | 'plain' + | 'subscribe'; + buttonColor?: 'default' | 'black' | 'white'; + buttonSizeMode?: 'fill' | 'static'; + captureBillingAddress?: boolean; + emailRequired?: boolean; + requireShippingMethod?: boolean; + }; + + // PayPal Options + paypal?: { + style?: { + layout?: 'vertical' | 'horizontal'; + color?: 'gold' | 'blue' | 'silver' | 'white' | 'black'; + shape?: 'rect' | 'pill'; + height?: number; // 25-55 + label?: 'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment'; + tagline?: boolean; + borderRadius?: number; // 0-55 + disableMaxWidth?: boolean; + }; + disableFunding?: string[]; // ['credit', 'card', 'paylater', etc.] + enableFunding?: string[]; // ['venmo', etc.] + vault?: boolean; + buyerCountry?: string; // Sandbox only + debug?: boolean; + }; + + // Klarna Options + klarna?: { + paymentFlow?: 'DEFAULT' | 'PREFER_VAULT'; + allowedPaymentCategories?: ('pay_now' | 'pay_later' | 'pay_over_time')[]; + buttonOptions?: { + text?: string; + }; + }; + + // Vault Options + vault?: { + enabled?: boolean; + showEmptyState?: boolean; + }; + + // Stripe Options + stripe?: { + mandateData?: { + fullMandateText?: string; + merchantName?: string; + }; + publishableKey?: string; + }; + + // Submit Button Options + submitButton?: { + amountVisible?: boolean; + useBuiltInButton?: boolean; // Set false for external buttons + }; +} +``` + +## CSS Custom Properties + +### Brand Colors + +- `--primer-color-brand` - Primary brand color +- `--primer-color-loader` - Loading indicator color +- `--primer-color-focus` - Focus state color + +### Typography + +- `--primer-typography-brand` - Font family + +### Spacing & Sizing + +- `--primer-space-base` - Base spacing unit (default: 4px) +- `--primer-size-base` - Base size unit (default: 4px) +- `--primer-radius-base` - Border radius (default: 4px) + +### Theme-Specific Variables + +```css +.primer-light-theme { + --primer-color-text-primary: var(--primer-color-gray-900); + --primer-color-background-outlined-default: var(--primer-color-gray-000); +} + +.primer-dark-theme { + --primer-color-text-primary: var(--primer-color-gray-100); + --primer-color-background-outlined-default: var(--primer-color-gray-800); +} +``` + +### Usage Example + +```css +:root { + --primer-color-brand: #2f98ff; + --primer-radius-base: 8px; + --primer-typography-brand: 'Inter, sans-serif'; +} + +/* Or apply to specific checkout */ +primer-checkout { + --primer-color-brand: #4a6cf7; + --primer-radius-base: 4px; +} +``` diff --git a/primer-web-components/references/react-patterns.md b/primer-web-components/references/react-patterns.md new file mode 100644 index 0000000..c725979 --- /dev/null +++ b/primer-web-components/references/react-patterns.md @@ -0,0 +1,562 @@ +# React Integration Patterns for Primer Web Components + +## React 19 vs React 18 Integration + +### React 19 (Recommended) + +React 19 natively supports passing object props to web components via JSX. + +**TypeScript Setup:** + +```typescript +import { CustomElements } from '@primer-io/primer-js/dist/jsx/index'; + +declare module 'react' { + namespace JSX { + interface IntrinsicElements extends CustomElements {} + } +} +``` + +**Component Example:** + +```typescript +const SDK_OPTIONS = { locale: 'en-GB' }; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + return ( + + ); +} +``` + +### React 18 (Legacy) + +React 18 requires imperative property assignment via refs. + +**Component Example:** + +```typescript +import { useRef, useEffect } from 'react'; + +const SDK_OPTIONS = { locale: 'en-GB' }; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + const checkoutRef = useRef(null); + + useEffect(() => { + const checkout = checkoutRef.current; + if (!checkout) return; + + // Imperative property assignment + checkout.options = SDK_OPTIONS; + + // Event listeners + const handleReady = () => console.log('SDK ready'); + checkout.addEventListener('primer:ready', handleReady); + + return () => { + checkout.removeEventListener('primer:ready', handleReady); + }; + }, []); + + return ( + + ); +} +``` + +## Critical Pattern: Stable Object References + +**THE PROBLEM:** Creating new object references on every render causes web components to re-initialize, losing user input and state. + +### ❌ WRONG - Causes Re-initialization + +```typescript +// WRONG: New object every render +function CheckoutPage() { + return ; +} + +// WRONG: New object in component body +function CheckoutPage() { + const options = { locale: 'en-GB' }; // New object every render! + return ; +} +``` + +### ✅ CORRECT - Stable References + +**Pattern 1: Module-level constant (static options)** + +```typescript +const SDK_OPTIONS = { + locale: 'en-GB', + paymentMethodOptions: { + PAYMENT_CARD: { + requireCVV: true, + requireBillingAddress: true, + }, + }, +}; + +function CheckoutPage({ clientToken }: { clientToken: string }) { + return ( + + ); +} +``` + +**Pattern 2: useMemo (dynamic options)** + +```typescript +import { useMemo } from 'react'; + +function CheckoutPage({ + clientToken, + userLocale, + merchantName +}: CheckoutPageProps) { + const sdkOptions = useMemo( + () => ({ + locale: userLocale, + paymentMethodOptions: { + APPLE_PAY: { + merchantName: merchantName, + merchantCountryCode: 'GB', + }, + }, + }), + [userLocale, merchantName] // Only recreate when deps change + ); + + return ( + + ); +} +``` + +**Pattern 3: useMemo with empty deps (constant within component)** + +```typescript +function CheckoutPage() { + const options = useMemo(() => ({ locale: 'en-GB' }), []); + return ; +} +``` + +## Event Handling in React + +### React 19 + +```typescript +function CheckoutPage({ clientToken }: { clientToken: string }) { + const checkoutRef = useRef(null); + + useEffect(() => { + const checkout = checkoutRef.current; + if (!checkout) return; + + const handleStateChange = (event: CustomEvent) => { + const { isProcessing, isSuccessful, primerJsError, paymentFailure } = event.detail; + console.log('State:', { isProcessing, isSuccessful, primerJsError, paymentFailure }); + }; + + checkout.addEventListener('primer:state-change', handleStateChange); + + return () => { + checkout.removeEventListener('primer:state-change', handleStateChange); + }; + }, []); + + return ( + + ); +} +``` + +### Dynamic Payment Method Handling + +```typescript +function CheckoutPage() { + const [paymentMethods, setPaymentMethods] = useState([]); + const checkoutRef = useRef(null); + + useEffect(() => { + const checkout = checkoutRef.current; + if (!checkout) return; + + const handleMethodsUpdate = (event: CustomEvent) => { + const methods = event.detail.toArray(); + setPaymentMethods(methods); + }; + + checkout.addEventListener('primer:methods-update', handleMethodsUpdate); + + return () => { + checkout.removeEventListener('primer:methods-update', handleMethodsUpdate); + }; + }, []); + + return ( + + +
+ {paymentMethods.map(({ type }) => ( + + ))} +
+
+
+ ); +} +``` + +### Payment Lifecycle Event Handling (New in v0.7.0) + +```typescript +function CheckoutPage({ clientToken }: { clientToken: string }) { + const checkoutRef = useRef(null); + + useEffect(() => { + const checkout = checkoutRef.current; + if (!checkout) return; + + // Set up callbacks via primer:ready + const handleReady = (event: CustomEvent) => { + const primer = event.detail; + + primer.onPaymentSuccess = ({ paymentSummary, paymentMethodType }) => { + console.log('✅ Payment successful'); + console.log(`${paymentSummary.network} ending in ${paymentSummary.last4Digits}`); + // Navigate to success page + }; + + primer.onPaymentFailure = ({ error, paymentMethodType }) => { + console.error('❌ Payment failed:', error.message); + console.error('Diagnostics ID:', error.diagnosticsId); + // Show error to user + }; + + primer.onVaultedMethodsUpdate = ({ vaultedPayments }) => { + console.log(`${vaultedPayments.size()} saved payment methods`); + }; + }; + + checkout.addEventListener('primer:ready', handleReady); + + return () => { + checkout.removeEventListener('primer:ready', handleReady); + }; + }, []); + + return ( + + ); +} +``` + +Alternatively, you can use events directly: + +```typescript +function CheckoutPage() { + useEffect(() => { + const handlePaymentSuccess = (event: CustomEvent) => { + const { paymentSummary, paymentMethodType, timestamp } = event.detail; + console.log('Payment successful!'); + // Handle success + }; + + const handlePaymentFailure = (event: CustomEvent) => { + const { error, paymentMethodType } = event.detail; + console.error('Payment failed:', error.message); + // Handle failure + }; + + document.addEventListener('primer:payment-success', handlePaymentSuccess); + document.addEventListener('primer:payment-failure', handlePaymentFailure); + + return () => { + document.removeEventListener( + 'primer:payment-success', + handlePaymentSuccess, + ); + document.removeEventListener( + 'primer:payment-failure', + handlePaymentFailure, + ); + }; + }, []); + + // ... +} +``` + +## Server-Side Rendering (SSR) Support + +### Next.js App Router (13+) + +```typescript +'use client'; + +import { useEffect } from 'react'; +import { loadPrimer } from '@primer-io/primer-js'; + +export default function CheckoutPage() { + useEffect(() => { + try { + loadPrimer(); + console.log('✅ Primer loaded'); + } catch (error) { + console.error('❌ Primer loading failed:', error); + } + }, []); + + return ( +
+ + {/* Checkout content */} + +
+ ); +} +``` + +### SvelteKit + +```javascript + + +
+ + + +
+``` + +## Custom Hooks + +### usePrimerDropIn Hook (Legacy Universal Checkout API) + +**⚠️ IMPORTANT:** This hook uses the legacy `Primer.showUniversalCheckout()` API, which is NOT part of the Primer Web Components API documented in this skill. This hook is provided for reference only if you need to work with the older Universal Checkout integration. + +For web components, use the patterns shown in the "Event Handling in React" section above instead. + +```typescript +'use client'; +import { Primer, PrimerCheckout } from '@primer-io/checkout-web'; +import { useCallback, useEffect, useState, useRef } from 'react'; + +interface UsePrimerDropInOptions { + clientToken?: string | null; + containerId?: string; + onCheckoutComplete?: (data: any) => void; +} + +export function usePrimerDropIn({ + clientToken, + containerId = 'container', + onCheckoutComplete, +}: UsePrimerDropInOptions = {}) { + const primerInstanceRef = useRef(null); + const isInitializingRef = useRef(false); + const previousTokenRef = useRef(null); + + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!clientToken || typeof window === 'undefined') return; + + if (clientToken === previousTokenRef.current && primerInstanceRef.current) { + return; + } + + if (isInitializingRef.current) { + return; + } + + isInitializingRef.current = true; + setIsLoading(true); + setError(null); + + const initialize = async () => { + try { + if (primerInstanceRef.current) { + await primerInstanceRef.current.teardown(); + primerInstanceRef.current = null; + } + + const checkout = await Primer.showUniversalCheckout(clientToken, { + container: `#${containerId}`, + onCheckoutComplete: (data: any) => { + setIsSuccess(true); + setIsLoading(false); + if (onCheckoutComplete) { + onCheckoutComplete(data); + } + }, + onCheckoutFail: (err: any, data: any, handler: any) => { + setError(err as Error); + setIsLoading(false); + return handler?.showErrorMessage(); + }, + }); + + if (!checkout) { + throw new Error('Failed to create drop-in checkout'); + } + + primerInstanceRef.current = checkout; + previousTokenRef.current = clientToken; + } catch (err) { + setError(err as Error); + primerInstanceRef.current = null; + } finally { + setIsLoading(false); + isInitializingRef.current = false; + } + }; + + initialize(); + + return () => { + if (primerInstanceRef.current) { + primerInstanceRef.current.teardown(); + } + }; + }, [clientToken, containerId, onCheckoutComplete]); + + const resetPrimerInstance = useCallback(async () => { + if (isInitializingRef.current) return; + + setIsLoading(true); + try { + if (primerInstanceRef.current) { + await primerInstanceRef.current.teardown(); + } + } catch (err) { + console.error('Error tearing down Primer checkout:', err); + } finally { + primerInstanceRef.current = null; + previousTokenRef.current = null; + isInitializingRef.current = false; + setIsSuccess(false); + setError(null); + setIsLoading(false); + } + }, []); + + return { + dropInCheckout: primerInstanceRef.current, + isLoading, + isSuccess, + error, + resetPrimerInstance, + }; +} +``` + +**Usage:** + +```typescript +import { usePrimerDropIn } from '@/hooks/usePrimerDropIn'; +import { useRouter } from 'next/navigation'; + +function CheckoutComponent({ clientToken }: { clientToken: string | null }) { + const router = useRouter(); + + const { isLoading, isSuccess, error, resetPrimerInstance } = usePrimerDropIn({ + clientToken, + containerId: 'primer-checkout-container', + onCheckoutComplete: (data) => { + console.log('Payment completed!', data); + setTimeout(() => router.push('/success'), 2000); + }, + }); + + return ( +
+ {isLoading &&
Processing payment...
} + {error && ( +
+

Error: {String(error)}

+ +
+ )} + {isSuccess &&
Payment successful!
} + {!error && !isSuccess && ( +
+ )} +
+ ); +} +``` + +## Preventing Flash of Undefined Components + +### CSS Approach + +```css +primer-checkout:has(:not(:defined)) { + visibility: hidden; +} +``` + +### JavaScript Approach + +```javascript +Promise.allSettled([ + customElements.whenDefined('primer-checkout'), + customElements.whenDefined('primer-payment-method'), +]).then(() => { + document.querySelector('.checkout-container').classList.add('ready'); +}); +``` + +```css +.checkout-container { + opacity: 0; + transition: opacity 0.2s; +} + +.checkout-container.ready { + opacity: 1; +} +```