From 7522787f923187133ff4153b5ec9bb954b40dc8a Mon Sep 17 00:00:00 2001 From: Sam Blacklock Date: Thu, 17 Jul 2025 16:41:04 +0100 Subject: [PATCH 1/5] feat: add a very simple manual payment workflow --- examples/spa-manual-payments/.gitignore | 24 + examples/spa-manual-payments/README.md | 64 + examples/spa-manual-payments/eslint.config.js | 28 + examples/spa-manual-payments/index.html | 13 + examples/spa-manual-payments/package.json | 31 + examples/spa-manual-payments/pnpm-lock.yaml | 2073 +++++++++++++++++ examples/spa-manual-payments/public/vite.svg | 1 + examples/spa-manual-payments/src/App.tsx | 377 +++ .../spa-manual-payments/src/assets/react.svg | 1 + .../src/auth/CartProvider.tsx | 13 + .../src/auth/StorefrontProvider.tsx | 36 + .../src/components/ManualPayment.tsx | 121 + .../src/components/OrderCreator.tsx | 63 + .../src/components/OrderStatus.tsx | 98 + examples/spa-manual-payments/src/constants.ts | 2 + examples/spa-manual-payments/src/index.css | 1 + examples/spa-manual-payments/src/main.tsx | 16 + .../spa-manual-payments/src/vite-env.d.ts | 1 + .../spa-manual-payments/tsconfig.app.json | 27 + examples/spa-manual-payments/tsconfig.json | 7 + .../spa-manual-payments/tsconfig.node.json | 25 + examples/spa-manual-payments/vite.config.ts | 8 + 22 files changed, 3030 insertions(+) create mode 100644 examples/spa-manual-payments/.gitignore create mode 100644 examples/spa-manual-payments/README.md create mode 100644 examples/spa-manual-payments/eslint.config.js create mode 100644 examples/spa-manual-payments/index.html create mode 100644 examples/spa-manual-payments/package.json create mode 100644 examples/spa-manual-payments/pnpm-lock.yaml create mode 100644 examples/spa-manual-payments/public/vite.svg create mode 100644 examples/spa-manual-payments/src/App.tsx create mode 100644 examples/spa-manual-payments/src/assets/react.svg create mode 100644 examples/spa-manual-payments/src/auth/CartProvider.tsx create mode 100644 examples/spa-manual-payments/src/auth/StorefrontProvider.tsx create mode 100644 examples/spa-manual-payments/src/components/ManualPayment.tsx create mode 100644 examples/spa-manual-payments/src/components/OrderCreator.tsx create mode 100644 examples/spa-manual-payments/src/components/OrderStatus.tsx create mode 100644 examples/spa-manual-payments/src/constants.ts create mode 100644 examples/spa-manual-payments/src/index.css create mode 100644 examples/spa-manual-payments/src/main.tsx create mode 100644 examples/spa-manual-payments/src/vite-env.d.ts create mode 100644 examples/spa-manual-payments/tsconfig.app.json create mode 100644 examples/spa-manual-payments/tsconfig.json create mode 100644 examples/spa-manual-payments/tsconfig.node.json create mode 100644 examples/spa-manual-payments/vite.config.ts diff --git a/examples/spa-manual-payments/.gitignore b/examples/spa-manual-payments/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/spa-manual-payments/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/spa-manual-payments/README.md b/examples/spa-manual-payments/README.md new file mode 100644 index 00000000..e0aa5e2d --- /dev/null +++ b/examples/spa-manual-payments/README.md @@ -0,0 +1,64 @@ +# SPA Manual Payments Example + +This example demonstrates how to handle manual payment processing in Elastic Path Commerce using the manual payment gateway. It focuses specifically on converting incomplete orders to complete orders through manual payment processing. + +## Overview + +This example builds upon the foundation of the spa-guest-checkout example but focuses narrowly on the payment handling aspect. Instead of showing a full storefront experience, it demonstrates: + +1. **Creating an Incomplete Order** - A simple button that creates a test product, adds it to cart, checks out, and creates an incomplete order +2. **Manual Payment Processing** - Shows how to use Elastic Path's manual payment gateway to process payments for incomplete orders +3. **Order Completion** - Demonstrates converting an incomplete order to a complete order after payment processing + +## Key Features + +- Simplified UI focused on payment processing +- Manual payment gateway integration +- Order status management (incomplete β†’ complete) +- Error handling for payment processing +- Real-time order status updates + +## Manual Payment Gateway + +The manual payment gateway in Elastic Path is designed for scenarios where payments are processed outside of the standard automated flow. This could include: + +- Bank transfers +- Cash payments +- Check payments +- Manual credit card processing +- Custom payment workflows + +## Getting Started + +1. Install dependencies: + + ```bash + pnpm install + ``` + +2. Configure your environment variables (copy from .env.example if available) + +3. Start the development server: + ```bash + pnpm dev + ``` + +## Flow + +1. **Initialize** - Click "Create Incomplete Order" to generate a test order +2. **Process Payment** - Use the manual payment interface to mark the payment as received +3. **Complete Order** - The order status changes from incomplete to complete + +## API Endpoints Used + +- Cart management APIs for creating test orders +- Checkout API for order creation +- Manual payment gateway APIs for payment processing +- Order management APIs for status updates + +## Learning Objectives + +- Understanding manual payment gateway configuration +- Order state management in Elastic Path +- Payment processing workflows +- Error handling in payment scenarios diff --git a/examples/spa-manual-payments/eslint.config.js b/examples/spa-manual-payments/eslint.config.js new file mode 100644 index 00000000..092408a9 --- /dev/null +++ b/examples/spa-manual-payments/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/examples/spa-manual-payments/index.html b/examples/spa-manual-payments/index.html new file mode 100644 index 00000000..5ca536c8 --- /dev/null +++ b/examples/spa-manual-payments/index.html @@ -0,0 +1,13 @@ + + + + + + + SPA Guest Checkout Example - Elastic Path + + +
+ + + diff --git a/examples/spa-manual-payments/package.json b/examples/spa-manual-payments/package.json new file mode 100644 index 00000000..a7902efc --- /dev/null +++ b/examples/spa-manual-payments/package.json @@ -0,0 +1,31 @@ +{ + "name": "spa-manual-payments", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@epcc-sdk/sdks-shopper": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@tailwindcss/vite": "^4.1.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "tailwindcss": "^4.1.7", + "typescript": "^5.5.3", + "vite": "^5.4.1" + } +} diff --git a/examples/spa-manual-payments/pnpm-lock.yaml b/examples/spa-manual-payments/pnpm-lock.yaml new file mode 100644 index 00000000..5b282d7b --- /dev/null +++ b/examples/spa-manual-payments/pnpm-lock.yaml @@ -0,0 +1,2073 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@epcc-sdk/sdks-shopper': + specifier: workspace:* + version: link:../../packages/sdks/shopper + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@eslint/js': + specifier: ^9.9.0 + version: 9.31.0 + '@tailwindcss/vite': + specifier: ^4.1.7 + version: 4.1.11(vite@5.4.19(lightningcss@1.30.1)) + '@types/react': + specifier: ^18.3.3 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.23) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.6.0(vite@5.4.19(lightningcss@1.30.1)) + eslint: + specifier: ^9.9.0 + version: 9.31.0(jiti@2.4.2) + eslint-plugin-react-hooks: + specifier: ^5.1.0-rc.0 + version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-react-refresh: + specifier: ^0.4.9 + version: 0.4.20(eslint@9.31.0(jiti@2.4.2)) + globals: + specifier: ^15.9.0 + version: 15.15.0 + tailwindcss: + specifier: ^4.1.7 + version: 4.1.11 + typescript: + specifier: ^5.5.3 + version: 5.8.3 + vite: + specifier: ^5.4.1 + version: 5.4.19(lightningcss@1.30.1) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.31.0': + resolution: {integrity: sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + + '@vitejs/plugin-react@4.6.0': + resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.186: + resolution: {integrity: sha512-lur7L4BFklgepaJxj4DqPk7vKbTEl0pajNlg2QjE5shefmlmBLm2HvQ7PMf1R/GvlevT/581cop33/quQcfX3A==} + + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.31.0: + resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2))': + dependencies: + eslint: 9.31.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.31.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.3': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@rolldown/pluginutils@1.0.0-beta.19': {} + + '@rollup/rollup-android-arm-eabi@4.45.1': + optional: true + + '@rollup/rollup-android-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-x64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.45.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.45.1': + optional: true + + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.2 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/vite@4.1.11(vite@5.4.19(lightningcss@1.30.1))': + dependencies: + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + tailwindcss: 4.1.11 + vite: 5.4.19(lightningcss@1.30.1) + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.1 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.23)': + dependencies: + '@types/react': 18.3.23 + + '@types/react@18.3.23': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@vitejs/plugin-react@4.6.0(vite@5.4.19(lightningcss@1.30.1))': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.19 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.19(lightningcss@1.30.1) + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.186 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001727: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chownr@3.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.0.4: {} + + electron-to-chromium@1.5.186: {} + + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + + eslint-plugin-react-refresh@0.4.20(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.31.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.15.1 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.31.0 + '@eslint/plugin-kit': 0.3.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@15.15.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.4.2: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.19: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + resolve-from@4.0.0: {} + + rollup@4.45.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 + fsevents: 2.3.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwindcss@4.1.11: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@5.8.3: {} + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@5.4.19(lightningcss@1.30.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.45.1 + optionalDependencies: + fsevents: 2.3.3 + lightningcss: 1.30.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yocto-queue@0.1.0: {} diff --git a/examples/spa-manual-payments/public/vite.svg b/examples/spa-manual-payments/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/spa-manual-payments/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/spa-manual-payments/src/App.tsx b/examples/spa-manual-payments/src/App.tsx new file mode 100644 index 00000000..f3f6d27f --- /dev/null +++ b/examples/spa-manual-payments/src/App.tsx @@ -0,0 +1,377 @@ +import { useEffect, useState } from "react" +import { + getByContextAllProducts, + getCartId, + manageCarts, + checkoutApi, + getStock, + initializeCart, + type OrderResponse, +} from "@epcc-sdk/sdks-shopper" +import { OrderCreator } from "./components/OrderCreator" +import { ManualPayment } from "./components/ManualPayment" +import { OrderStatus } from "./components/OrderStatus" + +function App() { + const [currentStep, setCurrentStep] = useState< + "create" | "payment" | "complete" + >("create") + const [order, setOrder] = useState(null) + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [cartId, setCartId] = useState(null) + + // Initialize cart and check authentication on startup + useEffect(() => { + const initializeApp = async () => { + try { + await initializeCart() + + // Test authentication by fetching products + const response = await getByContextAllProducts() + if (response.data?.data && response.data.data.length > 0) { + setIsAuthenticated(true) + } + + // Get current cart ID + const currentCartId = getCartId() + setCartId(currentCartId) + } catch (error) { + console.error("Failed to initialize app:", error) + setIsAuthenticated(false) + } + } + + initializeApp() + }, []) + + // Helper function to check if product has available stock + const checkProductStock = async (productId: string): Promise => { + try { + const stockResponse = await getStock({ + path: { product_uuid: productId }, + }) + + if (!stockResponse.data?.data?.attributes) { + return true // If no stock data, assume available + } + + const available = Number( + stockResponse.data.data.attributes.available || 0, + ) + return available > 0 + } catch (error) { + console.warn(`Could not check stock for product ${productId}:`, error) + return true // If stock check fails, assume available + } + } + + const createIncompleteOrder = async () => { + try { + setLoading(true) + setError(null) + + // Get products and find one that can be added to cart + const productsResponse = await getByContextAllProducts() + const allProducts = productsResponse.data?.data || [] + + if (allProducts.length === 0) { + throw new Error("No products available to create order") + } + + // Filter for products that can be added to cart: + // - Simple products (no variations) + // - Child products (variants with parent relationship) + // Exclude base products (parents that cannot be added to cart) + const addableProducts = allProducts.filter((product: any) => { + // If it has relationships.parent, it's a child product (variant) - can add to cart + if (product.relationships?.parent) { + return true + } + // If it has base_product attribute set to true, it's a base product - cannot add to cart + if (product.attributes?.base_product) { + return false + } + // Otherwise it's likely a simple product - can add to cart + return true + }) + + if (addableProducts.length === 0) { + throw new Error( + "No products available that can be added to cart. All products are base products that require variant selection.", + ) + } + + // Find the first product with available stock + let firstProduct = null + for (const product of addableProducts) { + if (!product.id) continue // Skip products without ID + const hasStock = await checkProductStock(product.id) + if (hasStock) { + firstProduct = product + break + } + } + + if (!firstProduct) { + throw new Error( + "No products available with sufficient stock. Please check your inventory or try again later.", + ) + } + const cartId = getCartId() + + if (!cartId) { + throw new Error("No cart found. Please refresh the page.") + } + + // Add product to cart + await manageCarts({ + path: { + cartID: cartId, + }, + body: { + data: { + type: "cart_item", + id: firstProduct.id, + quantity: 1, + }, + }, + }) + + // Create checkout with minimal customer data (using the correct API structure) + const customerData = { + customer: { + email: "test@example.com", + name: "Test Customer", + }, + billing_address: { + first_name: "Test", + last_name: "Customer", + company_name: "", + line_1: "123 Test Street", + line_2: "", + city: "Test City", + region: "Test State", + postcode: "12345", + county: "", + country: "US", + }, + shipping_address: { + first_name: "Test", + last_name: "Customer", + company_name: "", + line_1: "123 Test Street", + line_2: "", + city: "Test City", + region: "Test State", + postcode: "12345", + county: "", + country: "US", + phone_number: "", + instructions: "", + }, + } + + const checkoutResponse = await checkoutApi({ + path: { cartID: cartId }, + body: { data: customerData as any }, + }) + + const orderData = checkoutResponse.data?.data + if (!orderData) { + throw new Error("Failed to create order") + } + + const newOrder: OrderResponse = orderData + + setOrder(newOrder) + setCurrentStep("payment") + + // Update cart ID (cart should now be empty after checkout) + const updatedCartId = getCartId() + setCartId(updatedCartId) + + // Fire cart update event + window.dispatchEvent(new Event("cart:updated")) + } catch (err: any) { + setError(err?.message || "Failed to create order") + console.error("Order creation error:", err) + } finally { + setLoading(false) + } + } + + const handlePaymentComplete = (updatedOrder: OrderResponse) => { + setOrder(updatedOrder) + setCurrentStep("complete") + } + + const resetFlow = () => { + setOrder(null) + setCurrentStep("create") + setError(null) + } + + return ( +
+
+
+

+ Manual Payment Gateway Demo (SPA) +

+

+ Status:{" "} + {isAuthenticated ? ( + + Storefront successfully authenticated + + ) : ( + + Storefront not authenticated + + )} +

+ + {/* Cart ID Display */} +
+ Cart ID:{" "} + + {cartId || "No cart initialized"} + +
+ + {error && ( +
+

{error}

+
+ )} +
+ +
+ {/* Step indicator */} +
+
+
+ 1 +
+ Create Order +
+ +
+ +
+
+ 2 +
+ Process Payment +
+ +
+ +
+
+ 3 +
+ Order Complete +
+
+ + {/* Content based on current step */} + {currentStep === "create" && ( + + )} + + {currentStep === "payment" && order && ( +
+ + +
+ )} + + {currentStep === "complete" && order && ( +
+ +
+
+ πŸŽ‰ Payment Complete! +
+

+ The order has been marked as paid and is now complete. +

+ +
+
+ )} +
+
+
+ ) +} + +export default App diff --git a/examples/spa-manual-payments/src/assets/react.svg b/examples/spa-manual-payments/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/spa-manual-payments/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/spa-manual-payments/src/auth/CartProvider.tsx b/examples/spa-manual-payments/src/auth/CartProvider.tsx new file mode 100644 index 00000000..dbb16c96 --- /dev/null +++ b/examples/spa-manual-payments/src/auth/CartProvider.tsx @@ -0,0 +1,13 @@ +"use client" + +import React, { useEffect } from "react" +import { initializeCart } from "@epcc-sdk/sdks-shopper" + +export function CartProvider({ children }: { children: React.ReactNode }) { + // Initialize cart if not already initialized + useEffect(() => { + initializeCart() + }, []) + + return <>{children} +} diff --git a/examples/spa-manual-payments/src/auth/StorefrontProvider.tsx b/examples/spa-manual-payments/src/auth/StorefrontProvider.tsx new file mode 100644 index 00000000..51da88f0 --- /dev/null +++ b/examples/spa-manual-payments/src/auth/StorefrontProvider.tsx @@ -0,0 +1,36 @@ +"use client" + +import React, { useEffect } from "react" +import { + client, + createAuthLocalStorageInterceptor, +} from "@epcc-sdk/sdks-shopper" + +client.setConfig({ + baseUrl: import.meta.env.VITE_APP_EPCC_ENDPOINT_URL!, +}) + +export function StorefrontProvider({ + children, +}: { + children: React.ReactNode +}) { + useEffect(() => { + if (!import.meta.env.VITE_APP_EPCC_CLIENT_ID) { + throw new Error("Missing storefront client id") + } + + const interceptor = createAuthLocalStorageInterceptor({ + clientId: import.meta.env.VITE_APP_EPCC_CLIENT_ID, + }) + + // Add request interceptor to include the token in requests + client.interceptors.request.use(interceptor) + + return () => { + client.interceptors.request.eject(interceptor) + } + }, []) + + return <>{children} +} diff --git a/examples/spa-manual-payments/src/components/ManualPayment.tsx b/examples/spa-manual-payments/src/components/ManualPayment.tsx new file mode 100644 index 00000000..e994483d --- /dev/null +++ b/examples/spa-manual-payments/src/components/ManualPayment.tsx @@ -0,0 +1,121 @@ +import React, { useState } from "react" +import { paymentSetup, type OrderResponse } from "@epcc-sdk/sdks-shopper" + +type Props = { + order: OrderResponse + onPaymentComplete: (updatedOrder: OrderResponse) => void + onError: (error: string) => void +} + +export function ManualPayment({ order, onPaymentComplete, onError }: Props) { + const [processing, setProcessing] = useState(false) + const [paymentReference, setPaymentReference] = useState("") + + const processManualPayment = async () => { + try { + setProcessing(true) + onError("") + + // Build paymentmethod_meta only if user provided a reference + const paymentmethod_meta: { custom_reference?: string } = {} + + if (paymentReference.trim()) { + paymentmethod_meta.custom_reference = paymentReference.trim() + } + + // Process payment using Elastic Path manual payment gateway + // In a real storefront, this would typically use "purchase" to immediately complete the payment + if (!order.id) { + throw new Error("Order ID is missing") + } + + const paymentResponse = await paymentSetup({ + path: { + orderID: order.id, + }, + body: { + data: { + gateway: "manual", + method: "purchase", // Always use purchase for immediate completion in real storefronts + // Only include paymentmethod_meta if user provided a reference + ...(Object.keys(paymentmethod_meta).length > 0 && { + paymentmethod_meta, + }), + }, + }, + }) + + if (paymentResponse.error) { + throw new Error("Payment processing failed") + } + + // Update order with only the fields that actually change after payment + // Note: We can't fetch the updated order from API (requires elevated permissions in SPA), + // so we update client-side based on documented Elastic Path payment behavior + const updatedOrder: OrderResponse = { + ...order, + // After successful manual payment, order status changes to "complete" + status: "complete", + // Payment status changes to "paid" after successful payment processing + payment: "paid", + } + + onPaymentComplete(updatedOrder) + } catch (err: any) { + onError(err?.message || "Payment processing failed") + } finally { + setProcessing(false) + } + } + + return ( +
+

+ Step 2: Take Payment +

+ +
+
+ + setPaymentReference(e.target.value)} + placeholder="Transaction ID, Check Number, etc." + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={processing} + /> +

+ Optional: Enter a reference number or identifier for this payment +

+
+ +
+

+ Order Summary +

+
+ Order ID: + {order.id} +
+
+ Amount to Process: + + {order.meta?.display_price?.with_tax?.formatted || "Unknown"} + +
+
+ + +
+
+ ) +} diff --git a/examples/spa-manual-payments/src/components/OrderCreator.tsx b/examples/spa-manual-payments/src/components/OrderCreator.tsx new file mode 100644 index 00000000..b8d81055 --- /dev/null +++ b/examples/spa-manual-payments/src/components/OrderCreator.tsx @@ -0,0 +1,63 @@ +import React from "react" + +type Props = { + onCreateOrder: () => void + loading: boolean + isAuthenticated: boolean +} + +export function OrderCreator({ + onCreateOrder, + loading, + isAuthenticated, +}: Props) { + return ( +
+

+ Step 1: Create Test Order +

+ +
+

+ Create a sample order to demonstrate the manual payment process. Add + to cart and checkout are handled for you this example. Refer to other + examples in this repo for more information on how to add products to + cart and initiate checkout. +

+ +
+

+ What happens when you create an order: +

+
    +
  1. + Adds the first suitable and available product in your store to + your cart +
  2. +
  3. + Checks out the cart with placeholder customer data, thereby + generating an incomplete order ready for payment processing +
  4. +
+
+ + {!isAuthenticated && ( +
+

+ ⚠️ Storefront not authenticated. Please check your environment + configuration. +

+
+ )} + + +
+
+ ) +} diff --git a/examples/spa-manual-payments/src/components/OrderStatus.tsx b/examples/spa-manual-payments/src/components/OrderStatus.tsx new file mode 100644 index 00000000..a5408cc6 --- /dev/null +++ b/examples/spa-manual-payments/src/components/OrderStatus.tsx @@ -0,0 +1,98 @@ +import React from "react" +import type { OrderResponse } from "@epcc-sdk/sdks-shopper" + +type Props = { + order: OrderResponse +} + +export function OrderStatus({ order }: Props) { + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case "incomplete": + return "text-orange-600 bg-orange-100" + case "complete": + return "text-green-600 bg-green-100" + case "processing": + return "text-blue-600 bg-blue-100" + case "cancelled": + return "text-red-600 bg-red-100" + default: + return "text-gray-600 bg-gray-100" + } + } + + const getPaymentColor = (payment: string) => { + switch (payment.toLowerCase()) { + case "paid": + return "text-green-600 bg-green-100" + case "unpaid": + return "text-red-600 bg-red-100" + case "authorized": + return "text-blue-600 bg-blue-100" + case "refunded": + return "text-purple-600 bg-purple-100" + default: + return "text-gray-600 bg-gray-100" + } + } + + return ( +
+

+ Order Information +

+ +
+
+
+ +
{order.id}
+
+ +
+ +
+ {order.payment + ? order.payment.charAt(0).toUpperCase() + order.payment.slice(1) + : "Not specified"} +
+
+
+ +
+
+ +
+ {order.status + ? order.status.charAt(0).toUpperCase() + order.status.slice(1) + : "Unknown"} +
+
+ +
+ +
+ {order.meta?.display_price?.with_tax?.formatted || "Unknown"} +
+
+
+
+
+ ) +} diff --git a/examples/spa-manual-payments/src/constants.ts b/examples/spa-manual-payments/src/constants.ts new file mode 100644 index 00000000..251bbe48 --- /dev/null +++ b/examples/spa-manual-payments/src/constants.ts @@ -0,0 +1,2 @@ +export const EPCC_ENDPOINT_URL = import.meta.env.VITE_APP_EPCC_ENDPOINT_URL +export const COOKIE_PREFIX_KEY = "_store" diff --git a/examples/spa-manual-payments/src/index.css b/examples/spa-manual-payments/src/index.css new file mode 100644 index 00000000..f1d8c73c --- /dev/null +++ b/examples/spa-manual-payments/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/examples/spa-manual-payments/src/main.tsx b/examples/spa-manual-payments/src/main.tsx new file mode 100644 index 00000000..9b142e50 --- /dev/null +++ b/examples/spa-manual-payments/src/main.tsx @@ -0,0 +1,16 @@ +import React from "react" +import ReactDOM from "react-dom/client" +import App from "./App.tsx" +import "./index.css" +import { StorefrontProvider } from "./auth/StorefrontProvider.tsx" +import { CartProvider } from "./auth/CartProvider.tsx" + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + , +) diff --git a/examples/spa-manual-payments/src/vite-env.d.ts b/examples/spa-manual-payments/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/spa-manual-payments/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/spa-manual-payments/tsconfig.app.json b/examples/spa-manual-payments/tsconfig.app.json new file mode 100644 index 00000000..c9ccbd4c --- /dev/null +++ b/examples/spa-manual-payments/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/spa-manual-payments/tsconfig.json b/examples/spa-manual-payments/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/examples/spa-manual-payments/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/examples/spa-manual-payments/tsconfig.node.json b/examples/spa-manual-payments/tsconfig.node.json new file mode 100644 index 00000000..9728af2d --- /dev/null +++ b/examples/spa-manual-payments/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/spa-manual-payments/vite.config.ts b/examples/spa-manual-payments/vite.config.ts new file mode 100644 index 00000000..a7b2d995 --- /dev/null +++ b/examples/spa-manual-payments/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react" +import tailwindcss from "@tailwindcss/vite" + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], +}) From b970933ef1b1d13f761dc47ff4fda17b961bfe8c Mon Sep 17 00:00:00 2001 From: Sam Blacklock Date: Thu, 17 Jul 2025 16:57:31 +0100 Subject: [PATCH 2/5] feat: rewrite readme --- examples/spa-manual-payments/README.md | 132 ++++++++++++------ .../src/components/ManualPayment.tsx | 20 +-- .../src/components/OrderCreator.tsx | 2 +- .../src/components/OrderStatus.tsx | 16 +-- 4 files changed, 99 insertions(+), 71 deletions(-) diff --git a/examples/spa-manual-payments/README.md b/examples/spa-manual-payments/README.md index e0aa5e2d..757394ba 100644 --- a/examples/spa-manual-payments/README.md +++ b/examples/spa-manual-payments/README.md @@ -1,64 +1,108 @@ -# SPA Manual Payments Example +# Manual Payment Processing SPA Example -This example demonstrates how to handle manual payment processing in Elastic Path Commerce using the manual payment gateway. It focuses specifically on converting incomplete orders to complete orders through manual payment processing. +This example showcases how to implement **manual payment gateway processing** with **Elastic Path Commerce Cloud** in a Single-Page Application (SPA) written in React. -## Overview +> **Heads-up:** This project focuses **exclusively** on manual payment processing workflows. It creates minimal test orders solely to demonstrate payment handlingβ€”cart management, product catalogs, customer accounts, etc. are kept deliberately simple and are not the focus. -This example builds upon the foundation of the spa-guest-checkout example but focuses narrowly on the payment handling aspect. Instead of showing a full storefront experience, it demonstrates: +Key capabilities demonstrated: -1. **Creating an Incomplete Order** - A simple button that creates a test product, adds it to cart, checks out, and creates an incomplete order -2. **Manual Payment Processing** - Shows how to use Elastic Path's manual payment gateway to process payments for incomplete orders -3. **Order Completion** - Demonstrates converting an incomplete order to a complete order after payment processing +1. **Test Order Creation** – automatically creates incomplete orders with test products for payment processing demonstration. +2. **Manual Payment Gateway** – processes payments using Elastic Path's manual payment gateway for scenarios like: + β€’ Bank transfers + β€’ Cash payments + β€’ Check payments + β€’ Custom payment workflows +3. **Order State Management** – converts incomplete orders to complete orders after manual payment recording. +4. **Payment Status Tracking** – displays real-time order and payment status updates with proper UI indicators. -## Key Features +> The example purposefully uses simplified order creation and minimal product selection to keep the focus on manual payment mechanics. -- Simplified UI focused on payment processing -- Manual payment gateway integration -- Order status management (incomplete β†’ complete) -- Error handling for payment processing -- Real-time order status updates +--- -## Manual Payment Gateway +## Project Structure -The manual payment gateway in Elastic Path is designed for scenarios where payments are processed outside of the standard automated flow. This could include: +``` +spa-manual-payments/ +β”œβ”€β”€ index.html # Vite entry point +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ App.tsx # Main application with stepper UI +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ OrderCreator.tsx # Creates test orders for payment demo +β”‚ β”‚ β”œβ”€β”€ ManualPayment.tsx # Manual payment processing form +β”‚ β”‚ └── OrderStatus.tsx # Order & payment status display +β”‚ └── auth/ +β”‚ β”œβ”€β”€ CartProvider.tsx # Basic cart initialization +β”‚ └── StorefrontProvider.tsx # Elastic Path client setup +└── README.md # ← you are here +``` -- Bank transfers -- Cash payments -- Check payments -- Manual credit card processing -- Custom payment workflows +## How It Works -## Getting Started +### 1. Test Order Creation -1. Install dependencies: +`OrderCreator` automatically creates a test scenario: - ```bash - pnpm install - ``` +```tsx +// Creates cart β†’ adds test product β†’ checkout β†’ incomplete order +const createIncompleteOrder = async () => { + const products = await getByContextAllProducts() + const selectedProduct = products.data.find(/* stock logic */) + // ... cart creation, checkout, order creation +} +``` -2. Configure your environment variables (copy from .env.example if available) +This generates an incomplete order ready for manual payment processing. -3. Start the development server: - ```bash - pnpm dev - ``` +### 2. Manual Payment Processing -## Flow +`ManualPayment` captures payment details and processes them: -1. **Initialize** - Click "Create Incomplete Order" to generate a test order -2. **Process Payment** - Use the manual payment interface to mark the payment as received -3. **Complete Order** - The order status changes from incomplete to complete +```tsx +const paymentData = { + gateway: "manual", + method: "purchase", + // if the user supplies a reference + paymentmethod_meta: { + custom_reference: paymentReference, + name: "Manual Payment", + }, +} -## API Endpoints Used +await paymentSetup({ + path: { orderID: order.id }, + body: { data: paymentData }, +}) +``` -- Cart management APIs for creating test orders -- Checkout API for order creation -- Manual payment gateway APIs for payment processing -- Order management APIs for status updates +### 3. Status Display -## Learning Objectives +`OrderStatus` shows order and payment status. -- Understanding manual payment gateway configuration -- Order state management in Elastic Path -- Payment processing workflows -- Error handling in payment scenarios +--- + +## Running the Example Locally + +1. **Install deps** (from the repo root): + +```bash +pnpm i # or npm install / yarn +``` + +2. **Set environment variables** – create a `.env` file in `examples/spa-manual-payments` (or export in your shell): + +``` +VITE_APP_EPCC_ENDPOINT_URL=https://YOUR_EP_DOMAIN.elasticpath.com +VITE_APP_EPCC_CLIENT_ID=YOUR_CLIENT_ID +``` + +3. **Start Vite dev server**: + +```bash +pnpm --filter spa-manual-payments dev +``` + +## Learn More + +- [Manual Payment Gateway Documentation](https://elasticpath.dev/docs/api/carts/cart-management) +- [Order Management with Elastic Path](https://elasticpath.dev/docs/api/carts/cart-management) +- [Payment Processing APIs](https://elasticpath.dev/docs/api/carts/cart-management) diff --git a/examples/spa-manual-payments/src/components/ManualPayment.tsx b/examples/spa-manual-payments/src/components/ManualPayment.tsx index e994483d..dc2c8389 100644 --- a/examples/spa-manual-payments/src/components/ManualPayment.tsx +++ b/examples/spa-manual-payments/src/components/ManualPayment.tsx @@ -16,6 +16,10 @@ export function ManualPayment({ order, onPaymentComplete, onError }: Props) { setProcessing(true) onError("") + if (!order.id) { + throw new Error("Order ID is missing") + } + // Build paymentmethod_meta only if user provided a reference const paymentmethod_meta: { custom_reference?: string } = {} @@ -23,12 +27,6 @@ export function ManualPayment({ order, onPaymentComplete, onError }: Props) { paymentmethod_meta.custom_reference = paymentReference.trim() } - // Process payment using Elastic Path manual payment gateway - // In a real storefront, this would typically use "purchase" to immediately complete the payment - if (!order.id) { - throw new Error("Order ID is missing") - } - const paymentResponse = await paymentSetup({ path: { orderID: order.id, @@ -36,8 +34,7 @@ export function ManualPayment({ order, onPaymentComplete, onError }: Props) { body: { data: { gateway: "manual", - method: "purchase", // Always use purchase for immediate completion in real storefronts - // Only include paymentmethod_meta if user provided a reference + method: "purchase", ...(Object.keys(paymentmethod_meta).length > 0 && { paymentmethod_meta, }), @@ -49,14 +46,11 @@ export function ManualPayment({ order, onPaymentComplete, onError }: Props) { throw new Error("Payment processing failed") } - // Update order with only the fields that actually change after payment - // Note: We can't fetch the updated order from API (requires elevated permissions in SPA), - // so we update client-side based on documented Elastic Path payment behavior + // Note: we are creating a client-side order state object because we can't fetch the + // updated order from API (requires elevated permissions in SPA) const updatedOrder: OrderResponse = { ...order, - // After successful manual payment, order status changes to "complete" status: "complete", - // Payment status changes to "paid" after successful payment processing payment: "paid", } diff --git a/examples/spa-manual-payments/src/components/OrderCreator.tsx b/examples/spa-manual-payments/src/components/OrderCreator.tsx index b8d81055..531aa22d 100644 --- a/examples/spa-manual-payments/src/components/OrderCreator.tsx +++ b/examples/spa-manual-payments/src/components/OrderCreator.tsx @@ -45,7 +45,7 @@ export function OrderCreator({

⚠️ Storefront not authenticated. Please check your environment - configuration. + variables configuration.

)} diff --git a/examples/spa-manual-payments/src/components/OrderStatus.tsx b/examples/spa-manual-payments/src/components/OrderStatus.tsx index a5408cc6..5241349e 100644 --- a/examples/spa-manual-payments/src/components/OrderStatus.tsx +++ b/examples/spa-manual-payments/src/components/OrderStatus.tsx @@ -11,24 +11,14 @@ export function OrderStatus({ order }: Props) { case "incomplete": return "text-orange-600 bg-orange-100" case "complete": + case "paid": return "text-green-600 bg-green-100" case "processing": + case "authorized": return "text-blue-600 bg-blue-100" case "cancelled": - return "text-red-600 bg-red-100" - default: - return "text-gray-600 bg-gray-100" - } - } - - const getPaymentColor = (payment: string) => { - switch (payment.toLowerCase()) { - case "paid": - return "text-green-600 bg-green-100" case "unpaid": return "text-red-600 bg-red-100" - case "authorized": - return "text-blue-600 bg-blue-100" case "refunded": return "text-purple-600 bg-purple-100" default: @@ -56,7 +46,7 @@ export function OrderStatus({ order }: Props) { Payment Status
From fb797633ef635bd845b2a4fd3be92ddbef099bd4 Mon Sep 17 00:00:00 2001 From: Sam Blacklock Date: Thu, 17 Jul 2025 17:04:01 +0100 Subject: [PATCH 3/5] feat: declutter App.tsx --- examples/spa-manual-payments/src/App.tsx | 339 ++---------------- .../src/components/AppHeader.tsx | 40 +++ .../src/components/OrderCompleteView.tsx | 22 ++ .../src/components/OrderCreator.tsx | 2 +- .../src/components/StepIndicator.tsx | 74 ++++ .../src/hooks/useAppInitialization.ts | 43 +++ .../src/hooks/useOrderCreation.ts | 155 ++++++++ 7 files changed, 358 insertions(+), 317 deletions(-) create mode 100644 examples/spa-manual-payments/src/components/AppHeader.tsx create mode 100644 examples/spa-manual-payments/src/components/OrderCompleteView.tsx create mode 100644 examples/spa-manual-payments/src/components/StepIndicator.tsx create mode 100644 examples/spa-manual-payments/src/hooks/useAppInitialization.ts create mode 100644 examples/spa-manual-payments/src/hooks/useOrderCreation.ts diff --git a/examples/spa-manual-payments/src/App.tsx b/examples/spa-manual-payments/src/App.tsx index f3f6d27f..c20d77c2 100644 --- a/examples/spa-manual-payments/src/App.tsx +++ b/examples/spa-manual-payments/src/App.tsx @@ -1,204 +1,29 @@ -import { useEffect, useState } from "react" -import { - getByContextAllProducts, - getCartId, - manageCarts, - checkoutApi, - getStock, - initializeCart, - type OrderResponse, -} from "@epcc-sdk/sdks-shopper" +import { useState } from "react" +import { type OrderResponse } from "@epcc-sdk/sdks-shopper" +import { AppHeader } from "./components/AppHeader" +import { StepIndicator } from "./components/StepIndicator" import { OrderCreator } from "./components/OrderCreator" import { ManualPayment } from "./components/ManualPayment" import { OrderStatus } from "./components/OrderStatus" +import { OrderCompleteView } from "./components/OrderCompleteView" +import { useAppInitialization } from "./hooks/useAppInitialization" +import { useOrderCreation } from "./hooks/useOrderCreation" function App() { const [currentStep, setCurrentStep] = useState< "create" | "payment" | "complete" >("create") const [order, setOrder] = useState(null) - const [isAuthenticated, setIsAuthenticated] = useState(false) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [cartId, setCartId] = useState(null) - // Initialize cart and check authentication on startup - useEffect(() => { - const initializeApp = async () => { - try { - await initializeCart() - - // Test authentication by fetching products - const response = await getByContextAllProducts() - if (response.data?.data && response.data.data.length > 0) { - setIsAuthenticated(true) - } - - // Get current cart ID - const currentCartId = getCartId() - setCartId(currentCartId) - } catch (error) { - console.error("Failed to initialize app:", error) - setIsAuthenticated(false) - } - } - - initializeApp() - }, []) - - // Helper function to check if product has available stock - const checkProductStock = async (productId: string): Promise => { - try { - const stockResponse = await getStock({ - path: { product_uuid: productId }, - }) - - if (!stockResponse.data?.data?.attributes) { - return true // If no stock data, assume available - } - - const available = Number( - stockResponse.data.data.attributes.available || 0, - ) - return available > 0 - } catch (error) { - console.warn(`Could not check stock for product ${productId}:`, error) - return true // If stock check fails, assume available - } - } - - const createIncompleteOrder = async () => { - try { - setLoading(true) - setError(null) - - // Get products and find one that can be added to cart - const productsResponse = await getByContextAllProducts() - const allProducts = productsResponse.data?.data || [] - - if (allProducts.length === 0) { - throw new Error("No products available to create order") - } - - // Filter for products that can be added to cart: - // - Simple products (no variations) - // - Child products (variants with parent relationship) - // Exclude base products (parents that cannot be added to cart) - const addableProducts = allProducts.filter((product: any) => { - // If it has relationships.parent, it's a child product (variant) - can add to cart - if (product.relationships?.parent) { - return true - } - // If it has base_product attribute set to true, it's a base product - cannot add to cart - if (product.attributes?.base_product) { - return false - } - // Otherwise it's likely a simple product - can add to cart - return true - }) - - if (addableProducts.length === 0) { - throw new Error( - "No products available that can be added to cart. All products are base products that require variant selection.", - ) - } - - // Find the first product with available stock - let firstProduct = null - for (const product of addableProducts) { - if (!product.id) continue // Skip products without ID - const hasStock = await checkProductStock(product.id) - if (hasStock) { - firstProduct = product - break - } - } - - if (!firstProduct) { - throw new Error( - "No products available with sufficient stock. Please check your inventory or try again later.", - ) - } - const cartId = getCartId() - - if (!cartId) { - throw new Error("No cart found. Please refresh the page.") - } - - // Add product to cart - await manageCarts({ - path: { - cartID: cartId, - }, - body: { - data: { - type: "cart_item", - id: firstProduct.id, - quantity: 1, - }, - }, - }) - - // Create checkout with minimal customer data (using the correct API structure) - const customerData = { - customer: { - email: "test@example.com", - name: "Test Customer", - }, - billing_address: { - first_name: "Test", - last_name: "Customer", - company_name: "", - line_1: "123 Test Street", - line_2: "", - city: "Test City", - region: "Test State", - postcode: "12345", - county: "", - country: "US", - }, - shipping_address: { - first_name: "Test", - last_name: "Customer", - company_name: "", - line_1: "123 Test Street", - line_2: "", - city: "Test City", - region: "Test State", - postcode: "12345", - county: "", - country: "US", - phone_number: "", - instructions: "", - }, - } - - const checkoutResponse = await checkoutApi({ - path: { cartID: cartId }, - body: { data: customerData as any }, - }) - - const orderData = checkoutResponse.data?.data - if (!orderData) { - throw new Error("Failed to create order") - } - - const newOrder: OrderResponse = orderData + const { isAuthenticated, cartId } = useAppInitialization() + const { createIncompleteOrder, loading, error, clearError } = + useOrderCreation() + const handleCreateOrder = async () => { + const newOrder = await createIncompleteOrder() + if (newOrder) { setOrder(newOrder) setCurrentStep("payment") - - // Update cart ID (cart should now be empty after checkout) - const updatedCartId = getCartId() - setCartId(updatedCartId) - - // Fire cart update event - window.dispatchEvent(new Event("cart:updated")) - } catch (err: any) { - setError(err?.message || "Failed to create order") - console.error("Order creation error:", err) - } finally { - setLoading(false) } } @@ -210,129 +35,24 @@ function App() { const resetFlow = () => { setOrder(null) setCurrentStep("create") - setError(null) + clearError() } return (
-
-

- Manual Payment Gateway Demo (SPA) -

-

- Status:{" "} - {isAuthenticated ? ( - - Storefront successfully authenticated - - ) : ( - - Storefront not authenticated - - )} -

- - {/* Cart ID Display */} -
- Cart ID:{" "} - - {cartId || "No cart initialized"} - -
- - {error && ( -
-

{error}

-
- )} -
+
- {/* Step indicator */} -
-
-
- 1 -
- Create Order -
- -
- -
-
- 2 -
- Process Payment -
- -
- -
-
- 3 -
- Order Complete -
-
+ - {/* Content based on current step */} {currentStep === "create" && ( @@ -344,7 +64,7 @@ function App() {
)} @@ -352,20 +72,7 @@ function App() { {currentStep === "complete" && order && (
-
-
- πŸŽ‰ Payment Complete! -
-

- The order has been marked as paid and is now complete. -

- -
+
)}
diff --git a/examples/spa-manual-payments/src/components/AppHeader.tsx b/examples/spa-manual-payments/src/components/AppHeader.tsx new file mode 100644 index 00000000..af0c3d60 --- /dev/null +++ b/examples/spa-manual-payments/src/components/AppHeader.tsx @@ -0,0 +1,40 @@ +interface AppHeaderProps { + isAuthenticated: boolean + cartId: string | null + error: string | null +} + +export function AppHeader({ isAuthenticated, cartId, error }: AppHeaderProps) { + return ( +
+

+ Manual Payment Gateway Demo (SPA) +

+

+ Status:{" "} + {isAuthenticated ? ( + + Storefront successfully authenticated + + ) : ( + + Storefront not authenticated + + )} +

+ +
+ Cart ID:{" "} + + {cartId || "No cart initialized"} + +
+ + {error && ( +
+

{error}

+
+ )} +
+ ) +} diff --git a/examples/spa-manual-payments/src/components/OrderCompleteView.tsx b/examples/spa-manual-payments/src/components/OrderCompleteView.tsx new file mode 100644 index 00000000..84ef68fe --- /dev/null +++ b/examples/spa-manual-payments/src/components/OrderCompleteView.tsx @@ -0,0 +1,22 @@ +interface OrderCompleteViewProps { + onReset: () => void +} + +export function OrderCompleteView({ onReset }: OrderCompleteViewProps) { + return ( +
+
+ πŸŽ‰ Payment Complete! +
+

+ The order has been marked as paid and is now complete. +

+ +
+ ) +} diff --git a/examples/spa-manual-payments/src/components/OrderCreator.tsx b/examples/spa-manual-payments/src/components/OrderCreator.tsx index 531aa22d..ebc0b3b9 100644 --- a/examples/spa-manual-payments/src/components/OrderCreator.tsx +++ b/examples/spa-manual-payments/src/components/OrderCreator.tsx @@ -1,7 +1,7 @@ import React from "react" type Props = { - onCreateOrder: () => void + onCreateOrder: () => Promise loading: boolean isAuthenticated: boolean } diff --git a/examples/spa-manual-payments/src/components/StepIndicator.tsx b/examples/spa-manual-payments/src/components/StepIndicator.tsx new file mode 100644 index 00000000..2ed3f3c6 --- /dev/null +++ b/examples/spa-manual-payments/src/components/StepIndicator.tsx @@ -0,0 +1,74 @@ +interface StepIndicatorProps { + currentStep: "create" | "payment" | "complete" +} + +export function StepIndicator({ currentStep }: StepIndicatorProps) { + const steps = [ + { id: "create", label: "Create Order", number: 1 }, + { id: "payment", label: "Process Payment", number: 2 }, + { id: "complete", label: "Order Complete", number: 3 }, + ] + + const getStepStatus = (stepId: string) => { + const stepIndex = steps.findIndex((s) => s.id === stepId) + const currentIndex = steps.findIndex((s) => s.id === currentStep) + + if (stepIndex < currentIndex) return "completed" + if (stepIndex === currentIndex) return "current" + return "upcoming" + } + + const getStepClasses = (status: string) => { + switch (status) { + case "completed": + return "text-green-600" + case "current": + return "text-blue-600" + default: + return "text-gray-400" + } + } + + const getCircleClasses = (status: string) => { + switch (status) { + case "completed": + return "border-green-600 bg-green-100" + case "current": + return "border-blue-600 bg-blue-100" + default: + return "border-gray-300" + } + } + + const getConnectorClasses = (stepId: string) => { + const stepIndex = steps.findIndex((s) => s.id === stepId) + const currentIndex = steps.findIndex((s) => s.id === currentStep) + return stepIndex < currentIndex ? "bg-green-600" : "bg-gray-300" + } + + return ( +
+ {steps.map((step, index) => ( +
+
+
+ {step.number} +
+ {step.label} +
+ {index < steps.length - 1 && ( +
+ )} +
+ ))} +
+ ) +} diff --git a/examples/spa-manual-payments/src/hooks/useAppInitialization.ts b/examples/spa-manual-payments/src/hooks/useAppInitialization.ts new file mode 100644 index 00000000..2a4c5140 --- /dev/null +++ b/examples/spa-manual-payments/src/hooks/useAppInitialization.ts @@ -0,0 +1,43 @@ +import { useState, useEffect } from "react" +import { + getByContextAllProducts, + getCartId, + initializeCart, +} from "@epcc-sdk/sdks-shopper" + +export function useAppInitialization() { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [cartId, setCartId] = useState(null) + + useEffect(() => { + const initializeApp = async () => { + try { + await initializeCart() + + const response = await getByContextAllProducts() + if (response.data?.data && response.data.data.length > 0) { + setIsAuthenticated(true) + } + + const currentCartId = getCartId() + setCartId(currentCartId) + } catch (error) { + console.error("Failed to initialize app:", error) + setIsAuthenticated(false) + } + } + + initializeApp() + }, []) + + const refreshCartId = () => { + const currentCartId = getCartId() + setCartId(currentCartId) + } + + return { + isAuthenticated, + cartId, + refreshCartId, + } +} diff --git a/examples/spa-manual-payments/src/hooks/useOrderCreation.ts b/examples/spa-manual-payments/src/hooks/useOrderCreation.ts new file mode 100644 index 00000000..648af5de --- /dev/null +++ b/examples/spa-manual-payments/src/hooks/useOrderCreation.ts @@ -0,0 +1,155 @@ +import { useState } from "react" +import { + getByContextAllProducts, + getCartId, + manageCarts, + checkoutApi, + getStock, + type OrderResponse, +} from "@epcc-sdk/sdks-shopper" + +export function useOrderCreation() { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const checkProductStock = async (productId: string): Promise => { + try { + const stockResponse = await getStock({ + path: { product_uuid: productId }, + }) + + if (!stockResponse.data?.data?.attributes) { + return true + } + + const available = Number( + stockResponse.data.data.attributes.available || 0, + ) + return available > 0 + } catch (error) { + console.warn(`Could not check stock for product ${productId}:`, error) + return true + } + } + + const createIncompleteOrder = async (): Promise => { + try { + setLoading(true) + setError(null) + + const productsResponse = await getByContextAllProducts() + const allProducts = productsResponse.data?.data || [] + + if (allProducts.length === 0) { + throw new Error("No products available to create order") + } + + const addableProducts = allProducts.filter((product: any) => { + if (product.relationships?.parent) { + return true + } + if (product.attributes?.base_product) { + return false + } + return true + }) + + if (addableProducts.length === 0) { + throw new Error( + "No products available that can be added to cart. All products are base products that require variant selection.", + ) + } + + let firstProduct = null + for (const product of addableProducts) { + if (!product.id) continue + const hasStock = await checkProductStock(product.id) + if (hasStock) { + firstProduct = product + break + } + } + + if (!firstProduct) { + throw new Error( + "No products available with sufficient stock. Please check your inventory or try again later.", + ) + } + + const cartId = getCartId() + if (!cartId) { + throw new Error("No cart found. Please refresh the page.") + } + + await manageCarts({ + path: { cartID: cartId }, + body: { + data: { + type: "cart_item", + id: firstProduct.id, + quantity: 1, + }, + }, + }) + + const customerData = { + customer: { + email: "test@example.com", + name: "Test Customer", + }, + billing_address: { + first_name: "Test", + last_name: "Customer", + company_name: "", + line_1: "123 Test Street", + line_2: "", + city: "Test City", + region: "Test State", + postcode: "12345", + county: "", + country: "US", + }, + shipping_address: { + first_name: "Test", + last_name: "Customer", + company_name: "", + line_1: "123 Test Street", + line_2: "", + city: "Test City", + region: "Test State", + postcode: "12345", + county: "", + country: "US", + phone_number: "", + instructions: "", + }, + } + + const checkoutResponse = await checkoutApi({ + path: { cartID: cartId }, + body: { data: customerData as any }, + }) + + const orderData = checkoutResponse.data?.data + if (!orderData) { + throw new Error("Failed to create order") + } + + window.dispatchEvent(new Event("cart:updated")) + return orderData + } catch (err: any) { + setError(err?.message || "Failed to create order") + console.error("Order creation error:", err) + return null + } finally { + setLoading(false) + } + } + + return { + createIncompleteOrder, + loading, + error, + clearError: () => setError(null), + } +} From c6b7e10d3aa710e1a6511fc4d7156da91616306f Mon Sep 17 00:00:00 2001 From: Sam Blacklock Date: Thu, 17 Jul 2025 17:18:48 +0100 Subject: [PATCH 4/5] feat: update readme tree --- examples/spa-manual-payments/README.md | 12 +++++++++--- examples/spa-manual-payments/index.html | 2 +- .../src/components/ManualPayment.tsx | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/spa-manual-payments/README.md b/examples/spa-manual-payments/README.md index 757394ba..292b369e 100644 --- a/examples/spa-manual-payments/README.md +++ b/examples/spa-manual-payments/README.md @@ -1,6 +1,6 @@ # Manual Payment Processing SPA Example -This example showcases how to implement **manual payment gateway processing** with **Elastic Path Commerce Cloud** in a Single-Page Application (SPA) written in React. +This example showcases how to implement **manual payment gateway processing** with **Elastic Path Commerce Cloud** in a Single-Page Application (SPA) written in React. It builds on the `spa-guest-checkout` example, although omits a number of its features. > **Heads-up:** This project focuses **exclusively** on manual payment processing workflows. It creates minimal test orders solely to demonstrate payment handlingβ€”cart management, product catalogs, customer accounts, etc. are kept deliberately simple and are not the focus. @@ -25,11 +25,17 @@ Key capabilities demonstrated: spa-manual-payments/ β”œβ”€β”€ index.html # Vite entry point β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ App.tsx # Main application with stepper UI +β”‚ β”œβ”€β”€ App.tsx # Main app orchestration & state management β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ AppHeader.tsx # Authentication status & cart ID display +β”‚ β”‚ β”œβ”€β”€ StepIndicator.tsx # Multi-step progress indicator β”‚ β”‚ β”œβ”€β”€ OrderCreator.tsx # Creates test orders for payment demo β”‚ β”‚ β”œβ”€β”€ ManualPayment.tsx # Manual payment processing form -β”‚ β”‚ └── OrderStatus.tsx # Order & payment status display +β”‚ β”‚ β”œβ”€β”€ OrderStatus.tsx # Order & payment status display +β”‚ β”‚ └── OrderCompleteView.tsx # Success screen with reset functionality +β”‚ β”œβ”€β”€ hooks/ +β”‚ β”‚ β”œβ”€β”€ useAppInitialization.ts # App startup & authentication logic +β”‚ β”‚ └── useOrderCreation.ts # Order creation workflow & state β”‚ └── auth/ β”‚ β”œβ”€β”€ CartProvider.tsx # Basic cart initialization β”‚ └── StorefrontProvider.tsx # Elastic Path client setup diff --git a/examples/spa-manual-payments/index.html b/examples/spa-manual-payments/index.html index 5ca536c8..ac77a486 100644 --- a/examples/spa-manual-payments/index.html +++ b/examples/spa-manual-payments/index.html @@ -4,7 +4,7 @@ - SPA Guest Checkout Example - Elastic Path + SPA Manual Payments Example - Elastic Path
diff --git a/examples/spa-manual-payments/src/components/ManualPayment.tsx b/examples/spa-manual-payments/src/components/ManualPayment.tsx index dc2c8389..4929eaaf 100644 --- a/examples/spa-manual-payments/src/components/ManualPayment.tsx +++ b/examples/spa-manual-payments/src/components/ManualPayment.tsx @@ -47,7 +47,7 @@ export function ManualPayment({ order, onPaymentComplete, onError }: Props) { } // Note: we are creating a client-side order state object because we can't fetch the - // updated order from API (requires elevated permissions in SPA) + // updated order from API (requires elevated permissions) const updatedOrder: OrderResponse = { ...order, status: "complete", From 3b1c2ae1a64a2ce78d082fd01f8aa934b0614bd8 Mon Sep 17 00:00:00 2001 From: Sam Blacklock Date: Tue, 22 Jul 2025 15:52:47 +0100 Subject: [PATCH 5/5] feat: add more setup instructions to the readme --- examples/spa-manual-payments/README.md | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/examples/spa-manual-payments/README.md b/examples/spa-manual-payments/README.md index 292b369e..af05b4ee 100644 --- a/examples/spa-manual-payments/README.md +++ b/examples/spa-manual-payments/README.md @@ -42,6 +42,54 @@ spa-manual-payments/ └── README.md # ← you are here ``` +--- + +## Store Setup Requirements + +To use this example, your **Elastic Path Commerce Cloud store** must be properly configured: + +### πŸ”Œ **Payment Gateway Configuration** + +1. **Enable Manual Gateway** in Commerce Manager: + + - Go to **Settings** β†’ **Payments** + - Configure **Manual** gateway + - Enable the gateway + +### πŸ“¦ **Product Setup** + +Your store needs products in a published catalog for the payment demo to work. Learn more about how to publish a catalog [in the docs](https://elasticpath.dev/docs/commerce-manager/product-experience-manager/catalogs/catalog-configuration): + +1. **Product Requirements**: + + - Add at least one **simple product** (not a base product requiring variations) + - Products should have **prices** associated with them + - Products must be **active** and included in the published catalog + +2. **Inventory Configuration** (Optional): + - If using inventory management, ensure products have **stock levels** configured + +### πŸ”‘ **API Access Setup** + +1. **Client Credentials**: + + - Go to **Settings** β†’ **API Keys** and create a new application key + - Note your **Client ID** and **Store URL** and use them in your .env + +### 🚨 **Common Setup Issues** + +**"No products available" Error**: + +- **Cause**: No products in published catalog, catalog not published, or all products are base products +- **Solution**: Create and publish a catalog with at least one simple product + +**404 Inventory Errors**: + +- **Cause**: Product inventory not configured +- **Solution**: Either configure inventory or ignore (example handles gracefully) + +--- + ## How It Works ### 1. Test Order Creation