Initial commit
This commit is contained in:
625
primer-web-components/references/component-reference.md
Normal file
625
primer-web-components/references/component-reference.md
Normal file
@@ -0,0 +1,625 @@
|
||||
# Primer Web Components - Component Reference
|
||||
|
||||
## Core Components
|
||||
|
||||
### `<primer-checkout>`
|
||||
|
||||
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
|
||||
<primer-checkout client-token="eyJ0eXAiOiJKV1QiLCJhbGc...">
|
||||
<primer-main slot="main">
|
||||
<!-- Content -->
|
||||
</primer-main>
|
||||
</primer-checkout>
|
||||
```
|
||||
|
||||
### `<primer-main>`
|
||||
|
||||
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
|
||||
<primer-main slot="main">
|
||||
<div slot="payments">
|
||||
<primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
|
||||
</div>
|
||||
<div slot="checkout-complete">
|
||||
<h2>Payment Successful!</h2>
|
||||
</div>
|
||||
</primer-main>
|
||||
```
|
||||
|
||||
### `<primer-payment-method>`
|
||||
|
||||
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
|
||||
<primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
|
||||
<primer-payment-method type="PAYPAL"></primer-payment-method>
|
||||
<primer-payment-method type="GOOGLE_PAY" disabled></primer-payment-method>
|
||||
```
|
||||
|
||||
### `<primer-payment-method-container>`
|
||||
|
||||
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
|
||||
<!-- Show only digital wallets -->
|
||||
<primer-payment-method-container
|
||||
include="APPLE_PAY,GOOGLE_PAY"
|
||||
></primer-payment-method-container>
|
||||
|
||||
<!-- Show everything except cards -->
|
||||
<primer-payment-method-container
|
||||
exclude="PAYMENT_CARD"
|
||||
></primer-payment-method-container>
|
||||
```
|
||||
|
||||
## Card Form Components
|
||||
|
||||
### `<primer-card-form>`
|
||||
|
||||
Container for card payment inputs with built-in validation and state management.
|
||||
|
||||
**Slots:**
|
||||
|
||||
- `card-form-content` - Custom content area for inputs
|
||||
|
||||
**Example:**
|
||||
|
||||
```html
|
||||
<primer-card-form>
|
||||
<div slot="card-form-content">
|
||||
<primer-input-card-number></primer-input-card-number>
|
||||
<primer-input-card-expiry></primer-input-card-expiry>
|
||||
<primer-input-cvv></primer-input-cvv>
|
||||
<primer-card-form-submit></primer-card-form-submit>
|
||||
</div>
|
||||
</primer-card-form>
|
||||
```
|
||||
|
||||
### `<primer-input-card-number>`
|
||||
|
||||
Secure card number input field with automatic card type detection.
|
||||
|
||||
**DOM Structure:**
|
||||
|
||||
```html
|
||||
<primer-input-wrapper>
|
||||
<primer-input-label slot="label">Card Number</primer-input-label>
|
||||
<div slot="input">
|
||||
<!-- Secure iframe for card number -->
|
||||
<primer-card-network-selector></primer-card-network-selector>
|
||||
</div>
|
||||
</primer-input-wrapper>
|
||||
```
|
||||
|
||||
### `<primer-input-card-expiry>`
|
||||
|
||||
Expiration date input with automatic formatting (MM/YY).
|
||||
|
||||
### `<primer-input-cvv>`
|
||||
|
||||
CVV/security code input field.
|
||||
|
||||
### `<primer-input-card-holder-name>`
|
||||
|
||||
Cardholder name text input (not in secure iframe).
|
||||
|
||||
### `<primer-card-form-submit>`
|
||||
|
||||
Localized submit button for card forms.
|
||||
|
||||
### `<primer-billing-address>`
|
||||
|
||||
Collects billing address information (SDK Core only).
|
||||
|
||||
**Attributes:**
|
||||
|
||||
- Mode configuration (drop-in or custom layout)
|
||||
|
||||
**Example:**
|
||||
|
||||
```html
|
||||
<primer-card-form>
|
||||
<div slot="card-form-content">
|
||||
<primer-input-card-number></primer-input-card-number>
|
||||
<primer-billing-address></primer-billing-address>
|
||||
<primer-card-form-submit></primer-card-form-submit>
|
||||
</div>
|
||||
</primer-card-form>
|
||||
```
|
||||
|
||||
## Base UI Components
|
||||
|
||||
### `<primer-input-wrapper>`
|
||||
|
||||
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
|
||||
<primer-input-wrapper>
|
||||
<primer-input-label slot="label">Email</primer-input-label>
|
||||
<primer-input slot="input" type="email"></primer-input>
|
||||
<primer-input-error slot="error">Invalid email</primer-input-error>
|
||||
</primer-input-wrapper>
|
||||
```
|
||||
|
||||
### `<primer-input>`
|
||||
|
||||
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
|
||||
|
||||
### `<primer-input-label>`
|
||||
|
||||
Form label component.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
- `for` - ID of associated input
|
||||
- `disabled` - Boolean for disabled state
|
||||
|
||||
### `<primer-input-error>`
|
||||
|
||||
Error message component.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
- `for` - ID of associated input
|
||||
- `active` - Boolean to show/hide error
|
||||
|
||||
### `<primer-button>`
|
||||
|
||||
Styled button component.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
- `variant` - "primary" or "secondary"
|
||||
- `buttonType` - "button" or "submit"
|
||||
- `disabled` - Boolean
|
||||
|
||||
**Example:**
|
||||
|
||||
```html
|
||||
<primer-button variant="primary">Pay Now</primer-button>
|
||||
<primer-button variant="secondary">Cancel</primer-button>
|
||||
```
|
||||
|
||||
## Utility Components
|
||||
|
||||
### `<primer-error-message-container>`
|
||||
|
||||
Automatically displays payment processing errors.
|
||||
|
||||
**Example:**
|
||||
|
||||
```html
|
||||
<div slot="payments">
|
||||
<primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
|
||||
<primer-error-message-container></primer-error-message-container>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `<primer-vault-manager>`
|
||||
|
||||
Displays saved payment methods when vault is enabled.
|
||||
|
||||
**Requires:**
|
||||
|
||||
- Vault enabled in SDK options: `{"vault": {"enabled": true}}`
|
||||
|
||||
### `<primer-show-other-payments>`
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
562
primer-web-components/references/react-patterns.md
Normal file
562
primer-web-components/references/react-patterns.md
Normal file
@@ -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 (
|
||||
<primer-checkout
|
||||
client-token={clientToken}
|
||||
options={SDK_OPTIONS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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<HTMLElement>(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 (
|
||||
<primer-checkout
|
||||
ref={checkoutRef}
|
||||
client-token={clientToken}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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 <primer-checkout options={{ locale: 'en-GB' }} />;
|
||||
}
|
||||
|
||||
// WRONG: New object in component body
|
||||
function CheckoutPage() {
|
||||
const options = { locale: 'en-GB' }; // New object every render!
|
||||
return <primer-checkout options={options} />;
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 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 (
|
||||
<primer-checkout
|
||||
client-token={clientToken}
|
||||
options={SDK_OPTIONS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 (
|
||||
<primer-checkout
|
||||
client-token={clientToken}
|
||||
options={sdkOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: useMemo with empty deps (constant within component)**
|
||||
|
||||
```typescript
|
||||
function CheckoutPage() {
|
||||
const options = useMemo(() => ({ locale: 'en-GB' }), []);
|
||||
return <primer-checkout options={options} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Event Handling in React
|
||||
|
||||
### React 19
|
||||
|
||||
```typescript
|
||||
function CheckoutPage({ clientToken }: { clientToken: string }) {
|
||||
const checkoutRef = useRef<HTMLElement>(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 (
|
||||
<primer-checkout
|
||||
ref={checkoutRef}
|
||||
client-token={clientToken}
|
||||
options={SDK_OPTIONS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Payment Method Handling
|
||||
|
||||
```typescript
|
||||
function CheckoutPage() {
|
||||
const [paymentMethods, setPaymentMethods] = useState([]);
|
||||
const checkoutRef = useRef<HTMLElement>(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 (
|
||||
<primer-checkout ref={checkoutRef} client-token={clientToken}>
|
||||
<primer-main slot="main">
|
||||
<div slot="payments">
|
||||
{paymentMethods.map(({ type }) => (
|
||||
<primer-payment-method key={type} type={type} />
|
||||
))}
|
||||
</div>
|
||||
</primer-main>
|
||||
</primer-checkout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Payment Lifecycle Event Handling (New in v0.7.0)
|
||||
|
||||
```typescript
|
||||
function CheckoutPage({ clientToken }: { clientToken: string }) {
|
||||
const checkoutRef = useRef<HTMLElement>(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 (
|
||||
<primer-checkout
|
||||
ref={checkoutRef}
|
||||
client-token={clientToken}
|
||||
options={SDK_OPTIONS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<primer-checkout client-token="your-client-token">
|
||||
{/* Checkout content */}
|
||||
</primer-checkout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### SvelteKit
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
onMount(async () => {
|
||||
if (browser) {
|
||||
try {
|
||||
const { loadPrimer } = await import('@primer-io/primer-js');
|
||||
loadPrimer();
|
||||
console.log('✅ Primer loaded');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load Primer:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<primer-checkout client-token="your-client-token">
|
||||
<!-- Checkout content -->
|
||||
</primer-checkout>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 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<PrimerCheckout | null>(null);
|
||||
const isInitializingRef = useRef<boolean>(false);
|
||||
const previousTokenRef = useRef<string | null | undefined>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(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 (
|
||||
<div>
|
||||
{isLoading && <div>Processing payment...</div>}
|
||||
{error && (
|
||||
<div>
|
||||
<p>Error: {String(error)}</p>
|
||||
<button onClick={resetPrimerInstance}>Try again</button>
|
||||
</div>
|
||||
)}
|
||||
{isSuccess && <div>Payment successful!</div>}
|
||||
{!error && !isSuccess && (
|
||||
<div id="primer-checkout-container"></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user