{"_id":"@deskcreate/cybersource-3ds-web","_rev":"4-e5ddab319a9d611c0c7442f0a3161b47","name":"@deskcreate/cybersource-3ds-web","dist-tags":{"latest":"1.0.4"},"versions":{"1.0.0":{"name":"@deskcreate/cybersource-3ds-web","version":"1.0.0","keywords":["cybersource","3ds","3d-secure","payment","browser","ui","iframe","flex","microform","tokenization"],"author":{"name":"DeskCreate"},"license":"MIT","_id":"@deskcreate/cybersource-3ds-web@1.0.0","maintainers":[{"name":"richy8","email":"menaelvisjones@gmail.com"}],"homepage":"https://github.com/deskcreate/cybersource-3ds-web#readme","bugs":{"url":"https://github.com/deskcreate/cybersource-3ds-web/issues"},"dist":{"shasum":"6c5183a5959759b14be0a753cbf10826db9fbdc4","tarball":"https://registry.npmjs.org/@deskcreate/cybersource-3ds-web/-/cybersource-3ds-web-1.0.0.tgz","fileCount":18,"integrity":"sha512-RgWJhwvc+T6H5zv1Tm/M4Vgo6X3BsBPR1BBCn6Tu7g6jnLnCcSzXyqnGBRp51t8hzlqTruY3ekNjx3BQRUKa7w==","signatures":[{"sig":"MEQCIAyAK0aqPf+ONbrLzesL6c5hYaO0Bxv03iVKDw/1ZObeAiBmIiXk+q7SdziSKYfeNWBkrZqH3EjXM+UBZ8plotPl7w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":254166},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.esm.js","gitHead":"fa97cb534494b72c1a6d1b5193c9a65d5c225b6d","scripts":{"dev":"rollup -c -w","build":"rollup -c","prepublishOnly":"npm run build"},"_npmUser":{"name":"richy8","email":"menaelvisjones@gmail.com"},"repository":{"url":"git+https://github.com/deskcreate/cybersource-3ds-web.git","type":"git"},"_npmVersion":"9.8.1","description":"CyberSource 3DS Web SDK for browser-based integrations with Flex Microform support","directories":{},"_nodeVersion":"18.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.3","tslib":"^2.6.2","rollup":"^4.9.0","typescript":"^5.3.3","@types/react":"^19.2.8","@types/react-dom":"^19.2.3","@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-typescript":"^11.1.5","@rollup/plugin-node-resolve":"^15.2.3"},"_npmOperationalInternal":{"tmp":"tmp/cybersource-3ds-web_1.0.0_1768500301270_0.4874729024060187","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@deskcreate/cybersource-3ds-web","version":"1.0.1","keywords":["cybersource","3ds","3d-secure","payment","browser","ui","iframe","flex","microform","tokenization"],"author":{"name":"DeskCreate"},"license":"MIT","_id":"@deskcreate/cybersource-3ds-web@1.0.1","maintainers":[{"name":"richy8","email":"menaelvisjones@gmail.com"}],"homepage":"https://github.com/deskcreate/cybersource-3ds-web#readme","bugs":{"url":"https://github.com/deskcreate/cybersource-3ds-web/issues"},"dist":{"shasum":"299c85588bc060efa2966be140bd0049f9d39c9c","tarball":"https://registry.npmjs.org/@deskcreate/cybersource-3ds-web/-/cybersource-3ds-web-1.0.1.tgz","fileCount":18,"integrity":"sha512-SRQimfRJ/1N/57vhmb6gNlsRwQ1ZGvejE967mi/0/hjOuViSqg8OfiAUazOBun8dS/RztVr2eL19pzNLwwJZVg==","signatures":[{"sig":"MEUCIQCY6gKSMLFhdzIXuQSmK4+jedXubEaS6Q0d6vHg4ZcUwgIgDe2+tPaW85kVgmg+UE4CM06/ZSNmCEzMXJJrmSC23UU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":255336},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.esm.js","gitHead":"64af4ae9c40cb59c86aeb37b227a6875c48edd6b","scripts":{"dev":"rollup -c -w","build":"rollup -c","prepublishOnly":"npm run build"},"_npmUser":{"name":"richy8","email":"menaelvisjones@gmail.com"},"repository":{"url":"git+https://github.com/deskcreate/cybersource-3ds-web.git","type":"git"},"_npmVersion":"9.8.1","description":"CyberSource 3DS Web SDK for browser-based integrations with Flex Microform support","directories":{},"_nodeVersion":"18.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.3","tslib":"^2.6.2","rollup":"^4.9.0","typescript":"^5.3.3","@types/react":"^19.2.8","@types/react-dom":"^19.2.3","@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-typescript":"^11.1.5","@rollup/plugin-node-resolve":"^15.2.3"},"_npmOperationalInternal":{"tmp":"tmp/cybersource-3ds-web_1.0.1_1768502055381_0.6559881843182942","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@deskcreate/cybersource-3ds-web","version":"1.0.3","keywords":["cybersource","3ds","3d-secure","payment","browser","ui","iframe","flex","microform","tokenization"],"author":{"name":"DeskCreate"},"license":"MIT","_id":"@deskcreate/cybersource-3ds-web@1.0.3","maintainers":[{"name":"richy8","email":"menaelvisjones@gmail.com"}],"homepage":"https://github.com/deskcreate/cybersource-3ds-web#readme","bugs":{"url":"https://github.com/deskcreate/cybersource-3ds-web/issues"},"dist":{"shasum":"26eff419100039f4898c49f362deb55ab6c612b1","tarball":"https://registry.npmjs.org/@deskcreate/cybersource-3ds-web/-/cybersource-3ds-web-1.0.3.tgz","fileCount":18,"integrity":"sha512-FjmO3qb1LvxO/gwQjgUDd/qgO9bX6qlqQA2esVctyvguHwW5j4Od3kEA2saedI9eap/lveYw/yK4AfMRcZbwHA==","signatures":[{"sig":"MEYCIQDddlsyO6OY/8NjzPAOfDNWlFaeW4k3QU/MdQJT9JY0xAIhAN+9BUd6xGAcHEcxXrbe9vNExEU3zJ812b+oBseo5ej2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":381780},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.esm.js","gitHead":"ac97bfa8f9741b6ded563417e5679c1464d01967","scripts":{"dev":"rollup -c -w","build":"rollup -c","prepublishOnly":"npm run build"},"_npmUser":{"name":"richy8","email":"menaelvisjones@gmail.com"},"repository":{"url":"git+https://github.com/deskcreate/cybersource-3ds-web.git","type":"git"},"_npmVersion":"9.8.1","description":"CyberSource 3DS Web SDK for browser-based integrations with Flex Microform support","directories":{},"_nodeVersion":"18.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.3","tslib":"^2.6.2","rollup":"^4.9.0","typescript":"^5.3.3","@types/react":"^19.2.8","@types/react-dom":"^19.2.3","@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-typescript":"^11.1.5","@rollup/plugin-node-resolve":"^15.2.3"},"_npmOperationalInternal":{"tmp":"tmp/cybersource-3ds-web_1.0.3_1768586929102_0.2850716757216991","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@deskcreate/cybersource-3ds-web","version":"1.0.4","description":"CyberSource 3DS Web SDK for browser-based integrations with Flex Microform support","main":"dist/index.js","module":"dist/index.esm.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"scripts":{"build":"rollup -c","dev":"rollup -c -w","prepublishOnly":"npm run build"},"keywords":["cybersource","3ds","3d-secure","payment","browser","ui","iframe","flex","microform","tokenization"],"author":{"name":"DeskCreate"},"license":"MIT","devDependencies":{"@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-node-resolve":"^15.2.3","@rollup/plugin-typescript":"^11.1.5","@types/react":"^19.2.8","@types/react-dom":"^19.2.3","react":"^19.2.3","rollup":"^4.9.0","tslib":"^2.6.2","typescript":"^5.3.3"},"repository":{"type":"git","url":"git+https://github.com/deskcreate/cybersource-3ds-web.git"},"_id":"@deskcreate/cybersource-3ds-web@1.0.4","gitHead":"572f249aab3cc5804b5b22cc9006d9dbcad5d983","bugs":{"url":"https://github.com/deskcreate/cybersource-3ds-web/issues"},"homepage":"https://github.com/deskcreate/cybersource-3ds-web#readme","_nodeVersion":"18.18.2","_npmVersion":"9.8.1","dist":{"integrity":"sha512-qAJw+elFSnNxYY6V+/u/EuzOLYY5twlDJB4wHd5PD1ejP7c33Pe9EEXU4LZJFMdgLacYXUlF61LU6pJHNsslZw==","shasum":"bc3a65d4d7c8dfed141494c6d8fc6fe5d5d15cbf","tarball":"https://registry.npmjs.org/@deskcreate/cybersource-3ds-web/-/cybersource-3ds-web-1.0.4.tgz","fileCount":19,"unpackedSize":407808,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBbhwzHlptTYv9Dxn1aPqQS7ZphRZ9STWkXOrxyOq/9eAiBR0C6puEaodvGWAqXlF4NdIRJztOgaGCOaZxXgNBfllg=="}]},"_npmUser":{"name":"richy8","email":"menaelvisjones@gmail.com"},"directories":{},"maintainers":[{"name":"richy8","email":"menaelvisjones@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cybersource-3ds-web_1.0.4_1768928526469_0.7569860226146099"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-15T18:05:01.210Z","modified":"2026-01-20T17:02:06.812Z","1.0.0":"2026-01-15T18:05:01.412Z","1.0.1":"2026-01-15T18:34:15.547Z","1.0.3":"2026-01-16T18:08:49.268Z","1.0.4":"2026-01-20T17:02:06.681Z"},"bugs":{"url":"https://github.com/deskcreate/cybersource-3ds-web/issues"},"author":{"name":"DeskCreate"},"license":"MIT","homepage":"https://github.com/deskcreate/cybersource-3ds-web#readme","keywords":["cybersource","3ds","3d-secure","payment","browser","ui","iframe","flex","microform","tokenization"],"repository":{"type":"git","url":"git+https://github.com/deskcreate/cybersource-3ds-web.git"},"description":"CyberSource 3DS Web SDK for browser-based integrations with Flex Microform support","maintainers":[{"name":"richy8","email":"menaelvisjones@gmail.com"}],"readme":"# CyberSource 3DS Web SDK\n\nA lightweight, framework-agnostic JavaScript SDK for handling CyberSource 3D Secure authentication and Flex Microform card collection in the browser.\n\n## Features\n\n- ✅ **Flex Microform** - PCI-compliant card collection (iFrame-based)\n- ✅ **Device Data Collection** - Automated fingerprinting for 3DS 2.x\n- ✅ **Challenge UI** - Pre-built modal for 3DS step-up authentication\n- ✅ **Smart Loading** - Automatic Flex SDK script loading with retry logic\n- ✅ **TypeScript First** - Full type definitions for a better developer experience\n- ✅ **Framework Agnostic** - Seamlessly integrates with React, Vue, Angular, or Vanilla JS\n\n---\n\n## Installation\n\n```bash\nnpm install @deskcreate/cybersource-3ds-web\n```\n\n---\n\n## 🏗 Payment Flow Overview\n\nUnderstanding the 3DS payment flow is crucial for proper integration:\n\n```mermaid\ngraph TD\n    A[Initialize WebClient] --> B[Get Capture Context from Backend]\n    B --> C[Setup Flex Microform]\n    C --> D[User Enters Card Details]\n    D --> E[Tokenize Card Data]\n    E --> F[Collect Browser Info]\n    F --> G[Send to Backend: Start 3DS]\n    G --> H{Challenge Required?}\n    H -->|No - Frictionless| I[Backend Processes Payment]\n    H -->|Yes - Friction| J[Show Challenge Modal]\n    J --> K[User Completes Challenge]\n    K --> L[Webhook Validates & Processes]\n    L --> M[PostMessage to Frontend]\n    M --> N[Close Modal]\n    I --> O[Payment Complete]\n    N --> O\n```\n\n### Two Flow Types\n\n#### 1. **Frictionless Flow** (No Challenge)\n- Card issuer approves without additional verification\n- Backend processes payment immediately after enrollment\n- Frontend calls `/complete` endpoint to finalize\n\n#### 2. **Friction Flow** (Challenge Required)\n- Card issuer requires additional verification (OTP, biometric, etc.)\n- Frontend shows challenge modal (iframe)\n- User completes verification with their bank\n- **Backend webhook automatically validates and processes payment**\n- Webhook sends `postMessage` to frontend with result\n- Frontend closes modal and shows success/error\n\n> [!IMPORTANT]\n> **For friction flows, the payment is processed automatically by your backend webhook.** The frontend only needs to listen for the completion message via `postMessage`.\n\n---\n\n## Quick Start\n\n### 1. HTML Setup\n\n```html\n<!-- Container for the Flex Microform iframes -->\n<div id=\"microform-container\"></div>\n\n<!-- Expiry Inputs (Required - Flex v2 only handles card number and CVV) -->\n<input type=\"text\" id=\"expiryMonth\" placeholder=\"MM\" maxlength=\"2\" />\n<input type=\"text\" id=\"expiryYear\" placeholder=\"YYYY\" maxlength=\"4\" />\n```\n\n> **Note:** The SDK automatically loads the Flex Microform script when you call `setupFlexMicroform`. No manual script tag needed.\n\n### 2. Initialize the Client\n\n```javascript\nimport { WebClient } from '@deskcreate/cybersource-3ds-web'\n\nconst client = new WebClient()\n```\n\n### 3. Setup Secure Card Input\n\n> [!IMPORTANT]\n> **Flex Microform only handles Card Number and CVV.** You MUST collect the Expiry Month and Expiry Year using your own standard HTML inputs.\n\n#### Minimal Setup\n```javascript\n// Get capture context from your backend\nconst { captureContext } = await fetch('/api/capture-context').then(r => r.json())\n\n// Setup microform\nconst flex = await client.setupFlexMicroform('microform-container', captureContext)\n```\n\n#### Customized Setup\n```javascript\nconst flex = await client.setupFlexMicroform('microform-container', captureContext, {\n  layout: 'stacked', // or 'inline'\n  placeholders: {\n    cardNumber: '1234 5678 9012 3456',\n    securityCode: '•••'\n  },\n  labels: {\n    cardNumber: 'Card Number',\n    securityCode: 'CVV'\n  },\n  customStyles: {\n    fontSize: '16px',\n    fontFamily: 'system-ui, sans-serif',\n    textColor: '#1a1a1a',\n    borderColor: '#d1d5db',\n    focusBorderColor: '#4f46e5',\n    borderRadius: '8px',\n    inputHeight: '48px'\n  }\n})\n```\n\n### 4. Process Payment\n\n```javascript\n// 1. Tokenize card data\nconst { token, cardType, maskedPan } = await flex.tokenize(expiryMonth, expiryYear)\n\n// 2. Collect browser information for 3DS\nconst deviceInfo = await client.collectBrowserInfo()\n\n// 3. Send to your backend to start 3DS enrollment\nconst { enrollment, transactionId } = await fetch('/api/start', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    transientToken: token,\n    cardType,\n    amount: '100.00',\n    currency: 'USD',\n    deviceInformation: deviceInfo,\n    customerInfo: { /* ... */ }\n  })\n}).then(r => r.json())\n\n// 4. Handle challenge if required\nif (enrollment.challengeRequired) {\n  // Show challenge modal (non-blocking)\n  client.showChallengeModal(\n    enrollment.stepUpUrl,\n    enrollment.accessToken,\n    { transactionId }\n  )\n  \n  // Wait for completion via postMessage\n  const result = await waitForChallengeComplete(transactionId)\n  \n  // Close modal\n  client.closeChallengeModal()\n  \n  if (!result.success) {\n    throw new Error(result.error || 'Payment failed')\n  }\n  \n  // Payment already completed by backend webhook!\n  console.log('Payment successful:', result.payment)\n  \n} else {\n  // Frictionless - call complete endpoint\n  const payment = await fetch('/api/complete', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ transactionId })\n  }).then(r => r.json())\n  \n  console.log('Payment successful:', payment)\n}\n```\n\n---\n\n## 📖 API Reference\n\n### `WebClient`\n\nMain client class for interacting with CyberSource services.\n\n#### Constructor\n```typescript\nconst client = new WebClient()\n```\n\n#### Methods\n\n##### `setupFlexMicroform(containerId, captureContext, options?)`\n\nSets up secure card input fields in the specified container.\n\n**Parameters:**\n- `containerId` (string, required) - DOM element ID where fields will be rendered\n- `captureContext` (string, required) - JWT from your backend's capture context API\n- `options` (object, optional):\n  - `layout?: 'stacked' | 'inline'` - Field layout (default: 'stacked')\n  - `placeholders?: { cardNumber?: string, securityCode?: string }`\n  - `labels?: { cardNumber?: string, securityCode?: string }`\n  - `customStyles?: FlexFieldStyles` - Custom styling options\n  - `onFieldChange?: (fieldType: string, data: FlexFieldChangeData) => void`\n\n**Returns:** `Promise<FlexMicroformInstance>`\n\n**Example:**\n```javascript\nconst flex = await client.setupFlexMicroform('card-container', captureContext, {\n  layout: 'inline',\n  customStyles: {\n    fontSize: '16px',\n    borderRadius: '8px'\n  }\n})\n```\n\n---\n\n##### `collectBrowserInfo(ipAddress?)`\n\nCollects browser fingerprint data for 3DS authentication.\n\n**Parameters:**\n- `ipAddress` (string, optional) - Client's public IP address\n\n**Returns:** `Promise<DeviceInformation>`\n\n**Recommended:** Use this method instead of `collectDeviceData` for simpler integration.\n\n**Example:**\n```javascript\nconst deviceInfo = await client.collectBrowserInfo()\n```\n\n---\n\n##### `collectDeviceData(deviceDataUrl, accessToken, options?)`\n\nLegacy method for device data collection using hidden iframe.\n\n**Parameters:**\n- `deviceDataUrl` (string, required) - Device data collection URL from CyberSource\n- `accessToken` (string, required) - Access token from CyberSource\n- `options` (object, optional):\n  - `timeoutMs?: number` - Timeout in milliseconds (default: 10000)\n\n**Returns:** `Promise<{ success: boolean, timeout?: boolean }>`\n\n---\n\n##### `showChallengeModal(stepUpUrl, accessToken, options?)`\n\nDisplays the 3DS challenge modal for user authentication.\n\n**Parameters:**\n- `stepUpUrl` (string, required) - Challenge URL from enrollment response\n- `accessToken` (string, required) - Access token from enrollment response\n- `options` (object, optional):\n  - `transactionId?: string` - Transaction ID for matching completion events\n  - `timeoutMs?: number` - Modal timeout (default: 600000 / 10 minutes)\n\n**Returns:** `Promise<AuthenticationResult>`\n\n> **Note:** This method is non-blocking. It shows the modal but doesn't wait for completion. Use `postMessage` listener to detect completion.\n\n**Example:**\n```javascript\n// Show modal (non-blocking)\nclient.showChallengeModal(stepUpUrl, accessToken, { transactionId })\n\n// Listen for completion separately\nwindow.addEventListener('message', (event) => {\n  if (event.data.type === '3DS_COMPLETE' && event.data.transactionId === transactionId) {\n    client.closeChallengeModal()\n    // Handle result\n  }\n})\n```\n\n---\n\n##### `closeChallengeModal()`\n\nManually closes the challenge modal.\n\n**Example:**\n```javascript\nclient.closeChallengeModal()\n```\n\n---\n\n##### `destroy()`\n\nCleans up all resources and removes event listeners.\n\n**Example:**\n```javascript\nclient.destroy()\n```\n\n---\n\n### `FlexMicroformInstance`\n\nReturned by `setupFlexMicroform()`.\n\n#### Methods\n\n##### `tokenize(expiryMonth, expiryYear)`\n\nTokenizes the card data entered in the secure fields.\n\n**Parameters:**\n- `expiryMonth` (string, required) - 2-digit month (e.g., '06')\n- `expiryYear` (string, required) - 4-digit year (e.g., '2029')\n\n**Returns:** `Promise<FlexTokenizeResult>`\n```typescript\n{\n  token: string        // Transient token JWT\n  maskedPan: string    // Masked card number (e.g., '411111••••••1111')\n  cardType: string     // Card type (e.g., 'visa', 'mastercard')\n  expiryMonth: string  // Normalized month\n  expiryYear: string   // Normalized year\n}\n```\n\n**Example:**\n```javascript\nconst { token, cardType, maskedPan } = await flex.tokenize('06', '2029')\n```\n\n---\n\n##### `isValid()`\n\nChecks if all fields are valid.\n\n**Returns:** `boolean`\n\n---\n\n##### `destroy()`\n\nDestroys the microform instance and cleans up.\n\n---\n\n## 🚀 Complete React Example\n\nHere's a production-ready React hook implementation:\n\n```typescript\nimport { useState, useRef, useEffect, useCallback } from 'react'\nimport { WebClient, type FlexMicroformInstance } from '@deskcreate/cybersource-3ds-web'\n\ninterface PaymentCompleteMessage {\n  type: '3DS_COMPLETE'\n  transactionId: string\n  success: boolean\n  payment?: {\n    id: string\n    status: string\n    amount: number\n    currency: string\n  }\n  error?: string\n}\n\nexport const useCheckout = () => {\n  const [status, setStatus] = useState<'IDLE' | 'INITIALIZING' | 'READY' | 'PROCESSING' | 'CHALLENGING' | 'SUCCESS' | 'ERROR'>('IDLE')\n  const [message, setMessage] = useState('')\n  const [error, setError] = useState<Error | null>(null)\n  const [result, setResult] = useState<any>(null)\n\n  const clientRef = useRef<WebClient | null>(null)\n  const microformRef = useRef<FlexMicroformInstance | null>(null)\n  const challengeResolveRef = useRef<((result: PaymentCompleteMessage) => void) | null>(null)\n  const currentTransactionIdRef = useRef<string | null>(null)\n\n  // Listen for challenge completion messages\n  useEffect(() => {\n    const handleMessage = (event: MessageEvent) => {\n      const data = event.data as PaymentCompleteMessage\n\n      if (!data || data.type !== '3DS_COMPLETE') return\n\n      // Verify transaction ID matches\n      if (currentTransactionIdRef.current && data.transactionId !== currentTransactionIdRef.current) {\n        return\n      }\n\n      // Resolve the challenge promise\n      if (challengeResolveRef.current) {\n        challengeResolveRef.current(data)\n        challengeResolveRef.current = null\n      }\n    }\n\n    window.addEventListener('message', handleMessage)\n    return () => window.removeEventListener('message', handleMessage)\n  }, [])\n\n  // Initialize microform\n  useEffect(() => {\n    let isActive = true\n    const client = new WebClient()\n    clientRef.current = client\n\n    const init = async () => {\n      try {\n        setStatus('INITIALIZING')\n        setMessage('Loading...')\n\n        // Get capture context from your backend\n        const { captureContext } = await fetch('/api/capture-context').then(r => r.json())\n\n        if (!isActive) return\n\n        // Setup microform\n        const microform = await client.setupFlexMicroform('microform-container', captureContext, {\n          layout: 'stacked',\n          customStyles: {\n            fontSize: '16px',\n            borderRadius: '8px'\n          }\n        })\n\n        if (!isActive) {\n          microform.destroy()\n          return\n        }\n\n        microformRef.current = microform\n        setStatus('READY')\n        setMessage('Ready to pay')\n      } catch (err: any) {\n        if (!isActive) return\n        setStatus('ERROR')\n        setMessage(err.message || 'Failed to initialize')\n        setError(err)\n      }\n    }\n\n    init()\n\n    return () => {\n      isActive = false\n      microformRef.current?.destroy()\n      clientRef.current?.destroy()\n    }\n  }, [])\n\n  // Wait for challenge completion\n  const waitForChallengeComplete = useCallback((transactionId: string): Promise<PaymentCompleteMessage> => {\n    return new Promise((resolve, reject) => {\n      currentTransactionIdRef.current = transactionId\n      challengeResolveRef.current = resolve\n\n      // Timeout after 10 minutes\n      const timeout = setTimeout(() => {\n        if (challengeResolveRef.current) {\n          challengeResolveRef.current = null\n          reject(new Error('Challenge timed out'))\n        }\n      }, 600000)\n\n      // Clear timeout on resolve\n      const originalResolve = challengeResolveRef.current\n      challengeResolveRef.current = (result) => {\n        clearTimeout(timeout)\n        originalResolve(result)\n      }\n    })\n  }, [])\n\n  // Payment function\n  const pay = useCallback(async (amount: string, currency: string, expiryMonth: string, expiryYear: string) => {\n    const microform = microformRef.current\n    const client = clientRef.current\n\n    if (!microform || !client) {\n      setStatus('ERROR')\n      setMessage('Payment form not ready')\n      return\n    }\n\n    try {\n      // 1. Tokenize\n      setStatus('PROCESSING')\n      setMessage('Securing card...')\n\n      const { token, cardType } = await microform.tokenize(expiryMonth, expiryYear)\n\n      // 2. Collect device info\n      const deviceInfo = await client.collectBrowserInfo()\n\n      // 3. Start 3DS\n      setMessage('Verifying card...')\n\n      const response = await fetch('/api/start', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          transientToken: token,\n          cardType,\n          amount,\n          currency,\n          deviceInformation: deviceInfo\n        })\n      }).then(r => r.json())\n\n      const { enrollment, transactionId } = response\n\n      // 4. Handle challenge if required\n      if (enrollment.challengeRequired) {\n        setStatus('CHALLENGING')\n        setMessage('Verify your identity...')\n\n        // Show modal\n        client.showChallengeModal(enrollment.stepUpUrl, enrollment.accessToken, { transactionId })\n\n        // Wait for completion\n        const result = await waitForChallengeComplete(transactionId)\n\n        // Close modal\n        client.closeChallengeModal()\n\n        if (!result.success) {\n          throw new Error(result.error || 'Payment failed')\n        }\n\n        // Payment completed by backend\n        setStatus('SUCCESS')\n        setMessage('Payment successful!')\n        setResult(result.payment)\n      } else {\n        // Frictionless flow\n        setMessage('Processing payment...')\n\n        const payment = await fetch('/api/complete', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ transactionId })\n        }).then(r => r.json())\n\n        setStatus('SUCCESS')\n        setMessage('Payment successful!')\n        setResult(payment)\n      }\n    } catch (err: any) {\n      clientRef.current?.closeChallengeModal()\n      setStatus('ERROR')\n      setMessage(err.message || 'Payment failed')\n      setError(err)\n    }\n  }, [waitForChallengeComplete])\n\n  return {\n    status,\n    message,\n    error,\n    result,\n    pay,\n    isReady: status === 'READY',\n    isLoading: ['INITIALIZING', 'PROCESSING', 'CHALLENGING'].includes(status)\n  }\n}\n```\n\n**Usage:**\n```tsx\nfunction CheckoutForm() {\n  const { status, message, pay, isReady } = useCheckout()\n  const [expiry, setExpiry] = useState({ month: '06', year: '2029' })\n\n  return (\n    <div>\n      <div id=\"microform-container\"></div>\n      \n      <input \n        value={expiry.month} \n        onChange={e => setExpiry({ ...expiry, month: e.target.value })}\n        placeholder=\"MM\"\n      />\n      <input \n        value={expiry.year}\n        onChange={e => setExpiry({ ...expiry, year: e.target.value })}\n        placeholder=\"YYYY\"\n      />\n      \n      <button \n        onClick={() => pay('100.00', 'USD', expiry.month, expiry.year)}\n        disabled={!isReady}\n      >\n        {status === 'PROCESSING' ? message : 'Pay $100.00'}\n      </button>\n    </div>\n  )\n}\n```\n\n---\n\n## 🔧 Backend Integration\n\nYour backend needs three endpoints:\n\n### 1. Generate Capture Context\n```javascript\nPOST /api/capture-context\nBody: { targetOrigins: ['https://yoursite.com'] }\nResponse: { captureContext: 'eyJraWQ...' }\n```\n\n### 2. Start 3DS Enrollment\n```javascript\nPOST /api/start\nBody: {\n  transientToken: string\n  cardType: string\n  amount: string\n  currency: string\n  deviceInformation: object\n  customerInfo: object\n}\nResponse: {\n  transactionId: string\n  enrollment: {\n    challengeRequired: boolean\n    stepUpUrl?: string\n    accessToken?: string\n  }\n}\n```\n\n### 3. Complete Payment (Frictionless)\n```javascript\nPOST /api/complete\nBody: { transactionId: string }\nResponse: { status: 'success', payment: {...} }\n```\n\n### 4. Webhook (Friction Flow)\n```javascript\nPOST /api/webhook/3ds?transactionId=xxx&cardType=xxx\nBody: { TransactionId: string, ... }\nResponse: HTML with postMessage script\n```\n\nThe webhook should:\n1. Validate the authentication\n2. Process the payment\n3. Return HTML that sends a `postMessage` to the parent window:\n\n```html\n<script>\n  const message = {\n    type: '3DS_COMPLETE',\n    transactionId: 'xxx',\n    success: true,\n    payment: { id: 'pay_123', status: 'AUTHORIZED', amount: 100.00, currency: 'USD' }\n  }\n  \n  if (window.parent !== window) window.parent.postMessage(message, '*')\n  if (window.opener) window.opener.postMessage(message, '*')\n  if (window.top && window.top !== window) window.top.postMessage(message, '*')\n</script>\n```\n\n---\n\n## 🎨 Styling the Challenge Modal\n\nYou can customize the modal appearance:\n\n```javascript\nclient.setModalStyles({\n  overlay: {\n    backgroundColor: 'rgba(0, 0, 0, 0.75)'\n  },\n  modal: {\n    borderRadius: '16px',\n    boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'\n  },\n  header: {\n    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',\n    padding: '24px'\n  }\n})\n```\n\n---\n\n## 🐛 Troubleshooting\n\n### Microform fields not showing\n- Ensure the container element exists in the DOM before calling `setupFlexMicroform`\n- Check browser console for script loading errors\n- Verify the capture context is valid and not expired\n\n### Challenge modal not closing\n- Ensure your webhook returns proper HTML with `postMessage` script\n- Check that the `transactionId` in the message matches\n- Verify the message event listener is set up before showing the modal\n\n### \"Flex SDK not available\" error\n- The SDK automatically loads the Flex script, but network issues can cause failures\n- Check your Content Security Policy allows loading from `flex.cybersource.com`\n- Try manually including the script tag in your HTML\n\n---\n\n## Contributing\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## License\nMIT License.\n","readmeFilename":"README.md"}