diff --git a/.husky/pre-commit b/.husky/pre-commit index 28d9be28..3cff7d0e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - pnpm build && pnpm test diff --git a/.vscode/settings.json b/.vscode/settings.json index 184991f8..5991e486 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "biomejs.biome", "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit" - } + "source.fixAll.biome": "explicit" + }, + "biome.configurationPath": "./biome.json" } diff --git a/biome.json b/biome.json index 173dadfd..7440f4c5 100644 --- a/biome.json +++ b/biome.json @@ -1,31 +1,41 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "ignore": [] - }, - "formatter": { - "enabled": true, - "indentStyle": "space" - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "asNeeded" - } - } + "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "includes": ["**"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "asNeeded" + } + } } diff --git a/lerna.json b/lerna.json index 9e33df17..625c7f68 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": false, "npmClient": "pnpm", - "version": "4.20.6", + "version": "4.28.6", "command": { "version": { "preid": "beta" diff --git a/package.json b/package.json index 95ef0fc0..30f6269f 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,10 @@ "dep:minor": "pnpm dep:major -t minor -i" }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "^2.3.8", "husky": "^9.1.7", - "lerna": "^8.2.2", - "typescript": "^5.8.3", - "vitest": "^3.1.1" + "lerna": "^9.0.3", + "typescript": "^5.9.3" }, "pnpm": { "overrides": { @@ -42,9 +41,25 @@ "ws@>=8.0.0 <8.17.1": ">=8.17.1", "micromatch@<4.0.8": ">=4.0.8", "rollup@>=4.0.0 <4.22.4": ">=4.22.4", - "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5" + "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5", + "esbuild@<=0.24.2": ">=0.25.0", + "vite@>=6.2.0 <6.2.6": ">=6.2.6", + "vite@>=6.2.0 <=6.2.6": ">=6.2.7", + "tar-fs@>=2.0.0 <2.1.3": ">=2.1.3", + "form-data": ">=4.0.4", + "axios": ">=1.12.0", + "on-headers@<1.1.0": ">=1.1.0", + "tmp@<=0.2.3": ">=0.2.4", + "vite@>=6.0.0 <=6.3.5": ">=6.3.6", + "tar-fs@>=3.0.0 <3.1.1": ">=3.1.1", + "playwright@<1.55.1": ">=1.55.1", + "vite@>=6.0.0 <=6.4.0": ">=6.4.1", + "vite@>=7.1.0 <=7.1.10": ">=7.1.11" }, "onlyBuiltDependencies": [ + "@biomejs/biome", + "esbuild", + "iframe-resizer", "msw", "nx" ] diff --git a/packages/core/extender.ts b/packages/core/extender.ts index 30006771..382f853e 100644 --- a/packages/core/extender.ts +++ b/packages/core/extender.ts @@ -6,8 +6,7 @@ const integrationClientId = import.meta.env.VITE_INTEGRATION_CLIENT_ID const integrationClientSecret = import.meta.env.VITE_INTEGRATION_CLIENT_SECRET const scope = import.meta.env.VITE_SALES_CHANNEL_SCOPE const domain = import.meta.env.VITE_DOMAIN -let accessToken: Awaited> | undefined = - undefined +let accessToken: Awaited> | undefined export interface CoreTestInterface { accessToken: Awaited> diff --git a/packages/core/package.json b/packages/core/package.json index cb983630..2a6c2f18 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@commercelayer/core", "version": "1.0.0", - "description": "Commerce Layer Components Core SDK", + "description": "Commerce Layer Core", "type": "module", "main": "./dist/index.js", "exports": { @@ -11,12 +11,18 @@ "default": "./dist/index.cjs" } }, - "keywords": ["jamstack", "headless", "ecommerce", "api", "components"], + "keywords": [ + "jamstack", + "headless", + "ecommerce", + "api", + "components" + ], "scripts": { "check-exports": "attw --pack .", "lint": "biome lint --error-on-warnings ./src && tsc", "lint:fix": "pnpm biome lint --write ./src", - "test": "pnpm run lint && vitest --silent", + "test": "pnpm run lint && vitest run --silent", "test:watch": "vitest", "coverage": "vitest run --coverage", "build": "tsup", @@ -31,15 +37,15 @@ }, "license": "MIT", "devDependencies": { - "@arethetypeswrong/cli": "^0.17.4", - "@vitest/coverage-v8": "^3.1.1", - "tsup": "^8.4.0", - "typescript": "^5.8.3", + "@arethetypeswrong/cli": "^0.18.2", + "@vitest/coverage-v8": "^4.0.15", + "tsup": "^8.5.1", + "typescript": "^5.9.3", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.1.1" + "vitest": "^4.0.15" }, "dependencies": { - "@commercelayer/js-auth": "^6.7.2", - "@commercelayer/sdk": "6.39.0" + "@commercelayer/js-auth": "^7.1.0", + "@commercelayer/sdk": "7.4.1" } } diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts new file mode 100644 index 00000000..91c54e8e --- /dev/null +++ b/packages/core/src/auth/index.ts @@ -0,0 +1 @@ +export { getAccessToken } from "./getAccessToken" diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e69de29b..4ced7667 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -0,0 +1,2 @@ +export * from "./auth" +export * from "./prices" diff --git a/packages/core/src/prices/getPrices.ts b/packages/core/src/prices/getPrices.ts index 53d9e737..a64031a9 100644 --- a/packages/core/src/prices/getPrices.ts +++ b/packages/core/src/prices/getPrices.ts @@ -1,8 +1,9 @@ -import type { - ListResponse, - Price, - QueryParamsList, - ResourcesConfig, +import { + type ListResponse, + type Price, + prices, + type QueryParamsList, + type ResourcesConfig, } from "@commercelayer/sdk" import { getSdk } from "#sdk" import type { RequestConfig } from "#types" @@ -27,6 +28,6 @@ export async function getPrices({ params, options, }: GetPricesParams): Promise> { - const sdk = getSdk({ accessToken }) - return await sdk.prices.list(params, options) + getSdk({ accessToken }) + return await prices.list(params, options) } diff --git a/packages/core/src/prices/index.ts b/packages/core/src/prices/index.ts new file mode 100644 index 00000000..dd1333cb --- /dev/null +++ b/packages/core/src/prices/index.ts @@ -0,0 +1,4 @@ +export type { Price } from "@commercelayer/sdk" +export { getPrices } from "./getPrices" +export { retrievePrice } from "./retrievePrice" +export { updatePrice } from "./updatePrice" diff --git a/packages/core/src/prices/retrievePrice.spec.ts b/packages/core/src/prices/retrievePrice.spec.ts index 3d23ac7b..bcbc304d 100644 --- a/packages/core/src/prices/retrievePrice.spec.ts +++ b/packages/core/src/prices/retrievePrice.spec.ts @@ -1,4 +1,3 @@ -import type { QueryFilter } from "@commercelayer/sdk" import { describe, expect } from "vitest" import { coreTest } from "#extender" import { getPrices } from "./getPrices.js" diff --git a/packages/core/src/prices/retrievePrice.ts b/packages/core/src/prices/retrievePrice.ts index 0167d569..815b98ae 100644 --- a/packages/core/src/prices/retrievePrice.ts +++ b/packages/core/src/prices/retrievePrice.ts @@ -1,4 +1,8 @@ -import type { Price, QueryParamsRetrieve } from "@commercelayer/sdk" +import { + type Price, + prices, + type QueryParamsRetrieve, +} from "@commercelayer/sdk" import { getSdk } from "#sdk" import type { RequestConfig } from "#types" @@ -24,6 +28,6 @@ export async function retrievePrice({ params, options, }: RetrievePriceParams): Promise { - const sdk = getSdk({ accessToken }) - return await sdk.prices.retrieve(id, params, options) + getSdk({ accessToken }) + return await prices.retrieve(id, params, options) } diff --git a/packages/core/src/prices/updatePrice.ts b/packages/core/src/prices/updatePrice.ts index 617c4e03..6defbc4b 100644 --- a/packages/core/src/prices/updatePrice.ts +++ b/packages/core/src/prices/updatePrice.ts @@ -1,7 +1,8 @@ -import type { - Price, - PriceUpdate, - QueryParamsRetrieve, +import { + type Price, + type PriceUpdate, + prices, + type QueryParamsRetrieve, } from "@commercelayer/sdk" import { getSdk } from "#sdk" import type { RequestConfig } from "#types" @@ -28,6 +29,6 @@ export async function updatePrice({ params, options, }: UpdatePriceParams): Promise { - const sdk = getSdk({ accessToken }) - return await sdk.prices.update(resource, params, options) + getSdk({ accessToken }) + return await prices.update(resource, params, options) } diff --git a/packages/core/src/sdk/index.ts b/packages/core/src/sdk/index.ts index 2fe821ed..5a7336d3 100644 --- a/packages/core/src/sdk/index.ts +++ b/packages/core/src/sdk/index.ts @@ -4,21 +4,22 @@ import { type JWTWebApp, jwtDecode, } from "@commercelayer/js-auth" -import sdk, { type CommerceLayerClient } from "@commercelayer/sdk" +import sdk from "@commercelayer/sdk" import type { RequestConfig } from "#types" /** * Get the Commerce Layer SDK instance * * @param {string} accessToken - The access token to use for authentication. - * @returns {CommerceLayerClient} - The Commerce Layer SDK instance. + * @returns {void} */ -export function getSdk({ accessToken }: RequestConfig): CommerceLayerClient { +export function getSdk({ accessToken }: RequestConfig): void { const { payload } = jwtDecode(accessToken) const { organization } = payload as | JWTIntegration | JWTWebApp | JWTSalesChannel const slug = organization.slug - return sdk({ accessToken, organization: slug }) + const cl = sdk({ accessToken, organization: slug }) + cl.addRawResponseReader() } diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 39f89612..26e341d9 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -1,11 +1,10 @@ import { defineConfig } from "tsup" -const env = process.env.NODE_ENV - -export default defineConfig((options) => ({ +export default defineConfig(() => ({ entryPoints: ["src/index.ts"], format: ["cjs", "esm"], dts: true, + splitting: true, outDir: "dist", clean: true, treeshake: true, diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 3c65cd6d..a0f1e3e6 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html"], + exclude: ["**/extender.ts"], }, }, plugins: [tsconfigPaths()], diff --git a/packages/docs/package.json b/packages/docs/package.json index 919d6bc7..105496c8 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,67 +1,67 @@ { - "private": true, - "name": "docs", - "version": "1.0.0", - "devDependencies": { - "@babel/core": "^7.26.9", - "@babel/preset-env": "^7.26.9", - "@commercelayer/js-auth": "^6.7.1", - "@commercelayer/sdk": "^6.32.0", - "@mdx-js/react": "^3.1.0", - "@storybook/addon-actions": "^7.6.17", - "@storybook/addon-backgrounds": "^7.6.17", - "@storybook/addon-docs": "^7.6.17", - "@storybook/addon-essentials": "^7.6.17", - "@storybook/addon-interactions": "^7.6.17", - "@storybook/addon-links": "^7.6.17", - "@storybook/addon-mdx-gfm": "^7.6.17", - "@storybook/addon-measure": "^7.6.17", - "@storybook/addon-outline": "^7.6.17", - "@storybook/addons": "^7.6.17", - "@storybook/api": "^7.6.17", - "@storybook/blocks": "^7.6.17", - "@storybook/client-api": "^7.6.17", - "@storybook/client-logger": "^7.6.17", - "@storybook/manager-api": "^7.6.17", - "@storybook/node-logger": "^8.4.2", - "@storybook/react": "^7.6.17", - "@storybook/react-vite": "^7.6.17", - "@storybook/testing-library": "^0.2.2", - "@storybook/theming": "^7.6.17", - "@types/js-cookie": "^3.0.6", - "@types/react": "^18.3.3", - "@vitejs/plugin-react": "^4.3.4", - "babel-loader": "^9.2.1", - "js-cookie": "^3.0.5", - "jwt-decode": "^4.0.0", - "msw": "^2.7.0", - "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "storybook": "^7.6.17", - "type-fest": "^4.35.0", - "typescript": "^5.7.3", - "vite": "^6.1.0", - "vite-tsconfig-paths": "^5.1.4" - }, - "scripts": { - "lint": "eslint src --ext .ts,.tsx", - "lint:fix": "eslint src --ext .ts,.tsx --fix", - "storybook": "storybook dev -s ./public -p 6006", - "build-storybook": "storybook build -s public -o dist" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/commercelayer/commercelayer-react-components.git" - }, - "keywords": [], - "author": "Alessandro Casazza", - "license": "ISC", - "bugs": { - "url": "https://github.com/commercelayer/commercelayer-react-components/issues" - }, - "homepage": "https://github.com/commercelayer/commercelayer-react-components#readme", - "msw": { - "workerDirectory": "public" - } + "private": true, + "name": "docs", + "version": "4.28.3", + "devDependencies": { + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@commercelayer/js-auth": "^7.1.0", + "@commercelayer/sdk": "^7.4.1", + "@mdx-js/react": "^3.1.1", + "@storybook/addon-actions": "^9.0.8", + "@storybook/addon-backgrounds": "^9.0.8", + "@storybook/addon-docs": "^10.1.6", + "@storybook/addon-essentials": "^8.6.14", + "@storybook/addon-interactions": "^8.6.14", + "@storybook/addon-links": "^10.1.6", + "@storybook/addon-mdx-gfm": "^8.6.14", + "@storybook/addon-measure": "^9.0.8", + "@storybook/addon-outline": "^9.0.8", + "@storybook/addons": "^7.6.17", + "@storybook/api": "^7.6.17", + "@storybook/blocks": "^8.6.14", + "@storybook/client-api": "^7.6.17", + "@storybook/client-logger": "^8.6.14", + "@storybook/manager-api": "^8.6.14", + "@storybook/node-logger": "^8.6.14", + "@storybook/react": "^10.1.6", + "@storybook/react-vite": "^10.1.6", + "@storybook/testing-library": "^0.2.2", + "@storybook/theming": "^8.6.14", + "@types/js-cookie": "^3.0.6", + "@types/react": "^19.2.7", + "@vitejs/plugin-react": "^5.1.2", + "babel-loader": "^10.0.0", + "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", + "msw": "^2.12.4", + "prop-types": "^15.8.1", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "storybook": "^10.1.6", + "type-fest": "^5.3.1", + "typescript": "^5.9.3", + "vite": "^7.2.7", + "vite-tsconfig-paths": "^5.1.4" + }, + "scripts": { + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "storybook": "storybook dev -s ./public -p 6006", + "build-storybook": "storybook build -s public -o dist" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/commercelayer/commercelayer-react-components.git" + }, + "keywords": [], + "author": "Alessandro Casazza", + "license": "ISC", + "bugs": { + "url": "https://github.com/commercelayer/commercelayer-react-components/issues" + }, + "homepage": "https://github.com/commercelayer/commercelayer-react-components#readme", + "msw": { + "workerDirectory": "public" + } } diff --git a/packages/docs/stories/examples/customer/001.orders.stories.tsx b/packages/docs/stories/examples/customer/001.orders.stories.tsx index 039a5703..bc3d2e06 100644 --- a/packages/docs/stories/examples/customer/001.orders.stories.tsx +++ b/packages/docs/stories/examples/customer/001.orders.stories.tsx @@ -39,6 +39,7 @@ export const Default: StoryFn = (args) => { { header: 'Amount', accessorKey: 'formatted_total_amount_with_taxes', + id: 'total_amount_cents', className: colClassName, titleClassName } @@ -85,7 +86,7 @@ export const Default: StoryFn = (args) => { className='align-top py-5 border-b' /> diff --git a/packages/document/package.json b/packages/document/package.json index 8b6d5d5c..bf53c858 100644 --- a/packages/document/package.json +++ b/packages/document/package.json @@ -5,44 +5,43 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, "dependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0" + "react": "^19.2.1", + "react-dom": "^19.2.1" }, "devDependencies": { - "@chromatic-com/storybook": "^3.2.6", - "@eslint/js": "^9.24.0", - "@storybook/addon-docs": "^8.6.12", - "@storybook/addon-essentials": "^8.6.12", - "@storybook/addon-interactions": "^8.6.12", - "@storybook/addon-links": "^8.6.12", - "@storybook/addon-mdx-gfm": "^8.6.12", - "@storybook/addon-onboarding": "^8.6.12", - "@storybook/blocks": "^8.6.12", - "@storybook/react": "^8.6.12", - "@storybook/react-vite": "^8.6.12", - "@storybook/test": "^8.6.12", - "@storybook/theming": "^8.6.12", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.0", - "eslint": "^9.24.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "eslint-plugin-storybook": "^0.12.0", - "globals": "^16.0.0", - "msw": "^2.7.4", + "@chromatic-com/storybook": "^4.1.3", + "@eslint/js": "^9.39.1", + "@storybook/addon-docs": "^10.1.6", + "@storybook/addon-essentials": "^8.6.14", + "@storybook/addon-interactions": "^8.6.14", + "@storybook/addon-links": "^10.1.6", + "@storybook/addon-mdx-gfm": "^8.6.14", + "@storybook/addon-onboarding": "^10.1.6", + "@storybook/blocks": "^8.6.14", + "@storybook/react": "^10.1.6", + "@storybook/react-vite": "^10.1.6", + "@storybook/test": "^8.6.14", + "@storybook/theming": "^8.6.14", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-storybook": "^10.1.6", + "globals": "^16.5.0", + "msw": "^2.12.4", "remark-gfm": "^4.0.1", - "storybook": "^8.6.12", - "typescript": "~5.8.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.3.1", + "storybook": "^10.1.6", + "typescript": "~5.9.3", + "typescript-eslint": "^8.49.0", + "vite": "^7.2.7", "vite-tsconfig-paths": "^5.1.4" }, "eslintConfig": { diff --git a/packages/hooks/extender.ts b/packages/hooks/extender.ts new file mode 100644 index 00000000..7e33c378 --- /dev/null +++ b/packages/hooks/extender.ts @@ -0,0 +1,69 @@ +import { getAccessToken } from "@commercelayer/core" +import { test } from "vitest" + +const clientId = import.meta.env.VITE_SALES_CHANNEL_CLIENT_ID +const integrationClientId = import.meta.env.VITE_INTEGRATION_CLIENT_ID +const integrationClientSecret = import.meta.env.VITE_INTEGRATION_CLIENT_SECRET +const scope = import.meta.env.VITE_SALES_CHANNEL_SCOPE +const domain = import.meta.env.VITE_DOMAIN +let accessToken: Awaited> | undefined + +export interface CoreTestInterface { + accessToken: Awaited> + config: { + clientId: string + scope?: string + domain: string + } +} + +/** + * This test is used to run integration tests with the sales channel client. + */ +export const coreTest = test.extend({ + // biome-ignore lint/correctness/noEmptyPattern: need to object destructure as the first argument + accessToken: async ({}, use) => { + if (accessToken == null) { + accessToken = await getAccessToken({ + grantType: "client_credentials", + config: { + clientId, + scope, + domain, + }, + }) + } + use(accessToken) + accessToken = undefined + }, + config: { + clientId, + scope, + domain, + }, +}) + +/** + * This test is used to run integration tests with the integration client. + */ +export const coreIntegrationTest = test.extend({ + // biome-ignore lint/correctness/noEmptyPattern: need to object destructure as the first argument + accessToken: async ({}, use) => { + if (accessToken == null) { + accessToken = await getAccessToken({ + grantType: "client_credentials", + config: { + clientId: integrationClientId, + clientSecret: integrationClientSecret, + domain, + }, + }) + } + use(accessToken) + accessToken = undefined + }, + config: { + clientId: integrationClientId, + domain, + }, +}) diff --git a/packages/hooks/package.json b/packages/hooks/package.json new file mode 100644 index 00000000..b7dcb3ee --- /dev/null +++ b/packages/hooks/package.json @@ -0,0 +1,59 @@ +{ + "name": "@commercelayer/hooks", + "version": "1.0.0", + "description": "Commerce Layer React Hooks", + "type": "module", + "main": "./dist/index.js", + "exports": { + "./package.json": "./package.json", + ".": { + "import": "./dist/index.js", + "default": "./dist/index.cjs" + } + }, + "keywords": [ + "jamstack", + "headless", + "ecommerce", + "api", + "components" + ], + "scripts": { + "check-exports": "attw --pack .", + "lint": "biome lint --error-on-warnings ./src && tsc", + "lint:fix": "pnpm biome lint --write ./src", + "test": "pnpm run lint && vitest run --silent", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "build": "tsup", + "ci": "pnpm build && pnpm check-exports && pnpm lint" + }, + "publishConfig": { + "access": "public" + }, + "author": { + "name": "Alessandro Casazza", + "email": "alessandro@commercelayer.io" + }, + "license": "MIT", + "devDependencies": { + "@arethetypeswrong/cli": "^0.18.2", + "@commercelayer/sdk": "^7.4.1", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^4.0.15", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.15" + }, + "dependencies": { + "@commercelayer/core": "workspace:*" + }, + "peerDependencies": { + "react": ">=19.2.1" + } +} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/hooks/src/prices/usePrices.test.ts b/packages/hooks/src/prices/usePrices.test.ts new file mode 100644 index 00000000..fa2fc6e3 --- /dev/null +++ b/packages/hooks/src/prices/usePrices.test.ts @@ -0,0 +1,133 @@ +import { act, renderHook, waitFor } from "@testing-library/react" +import { describe, expect } from "vitest" +import { coreIntegrationTest, coreTest } from "#extender" +import { usePrices } from "./usePrices" + +describe("usePrices", () => { + coreTest("should return a list of prices", async ({ accessToken }) => { + const token = accessToken?.accessToken + const { result } = renderHook(() => usePrices(token)) + + expect(result.current.prices).toEqual([]) + + act(() => { + result.current.fetchPrices() + }) + + await waitFor(() => { + expect(result.current.prices.length).toBeGreaterThan(0) + }) + }) + + coreIntegrationTest( + "should return a list of prices with an integration token", + async ({ accessToken }) => { + const token = accessToken?.accessToken + const { result } = renderHook(() => usePrices(token)) + + expect(result.current.prices).toEqual([]) + + act(() => { + result.current.fetchPrices() + }) + + await waitFor(() => { + expect(result.current.prices.length).toBeGreaterThan(0) + }) + }, + ) + + coreTest("should handle errors gracefully", async () => { + const token = "invalid-token" + const { result } = renderHook(() => usePrices(token)) + + act(() => { + result.current.fetchPrices() + }) + + await waitFor(() => { + expect(result.current.error).toBeDefined() + expect(result.current.prices).toEqual([]) + }) + }) + + coreTest("should clear prices", async ({ accessToken }) => { + const token = accessToken?.accessToken + const { result } = renderHook(() => usePrices(token)) + + // First fetch some prices + act(() => { + result.current.fetchPrices() + }) + + await waitFor(() => { + expect(result.current.prices.length).toBeGreaterThan(0) + }) + + // Then clear them + act(() => { + result.current.clearPrices() + }) + + expect(result.current.prices).toEqual([]) + }) + + coreTest("should clear errors", async () => { + const token = "invalid-token" + const { result } = renderHook(() => usePrices(token)) + + // Trigger an error + act(() => { + result.current.fetchPrices() + }) + + await waitFor(() => { + expect(result.current.error).toBeDefined() + }) + + // Clear the error + act(() => { + result.current.clearError() + }) + + expect(result.current.error).toBeNull() + }) + + coreTest("should filter prices by parameters", async ({ accessToken }) => { + const token = accessToken?.accessToken + const { result } = renderHook(() => usePrices(token)) + + act(() => { + result.current.fetchPrices({ + filters: { + sku_code_eq: "DIGITALPRODUCT", + }, + }) + }) + + await waitFor(() => { + expect(result.current.prices).toBeDefined() + expect(result.current.error).toBe(null) + }) + }) + + coreTest( + "should show pending state during fetch", + async ({ accessToken }) => { + const token = accessToken?.accessToken + const { result } = renderHook(() => usePrices(token)) + + expect(result.current.isPending).toBe(false) + + act(() => { + result.current.fetchPrices() + }) + + expect(result.current.isPending).toBe(true) + + await waitFor(() => { + expect(result.current.isPending).toBe(false) + }) + }, + ) +}) diff --git a/packages/hooks/src/prices/usePrices.ts b/packages/hooks/src/prices/usePrices.ts new file mode 100644 index 00000000..86112d9c --- /dev/null +++ b/packages/hooks/src/prices/usePrices.ts @@ -0,0 +1,64 @@ +import { getPrices, type Price } from "@commercelayer/core" +import { useCallback, useState, useTransition } from "react" + +interface UsePricesState { + prices: Price[] + error: string | null +} + +interface UsePricesReturn extends UsePricesState { + isPending: boolean + fetchPrices: (params?: Parameters[0]["params"]) => void + clearPrices: () => void + clearError: () => void +} + +export function usePrices(accessToken: string): UsePricesReturn { + const [isPending, startTransition] = useTransition() + const [state, setState] = useState({ + prices: [], + error: null, + }) + + const fetchPrices = useCallback( + (params?: Parameters[0]["params"]) => { + setState((prev: UsePricesState) => ({ ...prev, error: null })) + + startTransition(async () => { + try { + const prices = await getPrices({ + accessToken, + params, + }) + setState((prev: UsePricesState) => ({ + ...prev, + prices: prices, + })) + } catch (error: unknown) { + setState((prev: UsePricesState) => ({ + ...prev, + error: + error instanceof Error ? error.message : "Failed to fetch prices", + })) + } + }) + }, + [accessToken], + ) + + const clearPrices = useCallback(() => { + setState((prev: UsePricesState) => ({ ...prev, prices: [] })) + }, []) + + const clearError = useCallback(() => { + setState((prev: UsePricesState) => ({ ...prev, error: null })) + }, []) + + return { + ...state, + isPending, + fetchPrices, + clearPrices, + clearError, + } +} diff --git a/packages/hooks/tsconfig.json b/packages/hooks/tsconfig.json new file mode 100644 index 00000000..366986a2 --- /dev/null +++ b/packages/hooks/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + /* Base Options: */ + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "lib": ["es2022"], + "noEmit": true, + + /* Strictness */ + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + /* If transpiling with TypeScript: */ + "module": "Preserve", + + /* Relative Paths */ + "baseUrl": ".", + "paths": { + "#sdk": ["src/sdk/index.ts"], + "#types": ["src/types/index.ts"], + "#extender": ["extender.ts"] + } + }, + "exclude": ["node_modules", "dist", "coverage", "*.spec.ts"] +} diff --git a/packages/hooks/tsup.config.ts b/packages/hooks/tsup.config.ts new file mode 100644 index 00000000..39f89612 --- /dev/null +++ b/packages/hooks/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup" + +const env = process.env.NODE_ENV + +export default defineConfig((options) => ({ + entryPoints: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + outDir: "dist", + clean: true, + treeshake: true, +})) diff --git a/packages/hooks/vite-env.d.ts b/packages/hooks/vite-env.d.ts new file mode 100644 index 00000000..c16c20fd --- /dev/null +++ b/packages/hooks/vite-env.d.ts @@ -0,0 +1,13 @@ +/// + +interface ImportMetaEnv { + readonly VITE_SALES_CHANNEL_CLIENT_ID: string + readonly VITE_SALES_CHANNEL_SCOPE: string + readonly VITE_INTEGRATION_CLIENT_ID: string + readonly VITE_INTEGRATION_CLIENT_SECRET: string + readonly VITE_DOMAIN: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/packages/hooks/vitest.config.ts b/packages/hooks/vitest.config.ts new file mode 100644 index 00000000..e5380544 --- /dev/null +++ b/packages/hooks/vitest.config.ts @@ -0,0 +1,15 @@ +import tsconfigPaths from "vite-tsconfig-paths" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + name: "hooks", + environment: "jsdom", + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: ["**/extender.ts"], + }, + }, + plugins: [tsconfigPaths()], +}) diff --git a/packages/react-components/.nx/workspace-data/file-map.json b/packages/react-components/.nx/workspace-data/file-map.json new file mode 100644 index 00000000..43c72b0e --- /dev/null +++ b/packages/react-components/.nx/workspace-data/file-map.json @@ -0,0 +1,1294 @@ +{ + "version": "6.0", + "nxVersion": "20.6.0", + "pathMappings": { + "@commercelayer/react-components": [ + "src/index" + ], + "#components/*": [ + "src/components/*" + ], + "#components-utils/*": [ + "src/components/utils/*" + ], + "#reducers/*": [ + "src/reducers/*" + ], + "#context/*": [ + "src/context/*" + ], + "#typings/*": [ + "src/typings/*" + ], + "#typings": [ + "src/typings/index" + ], + "#utils/*": [ + "src/utils/*" + ], + "#config/*": [ + "src/config/*" + ], + "#hooks/*": [ + "src/hooks/*" + ] + }, + "nxJsonPlugins": [], + "fileMap": { + "projectFileMap": {}, + "nonProjectFiles": [ + { + "file": ".env.example", + "hash": "4313029761429702267" + }, + { + "file": "README.md", + "hash": "17997768238606553932" + }, + { + "file": "mocks/getAccessToken.ts", + "hash": "12394074708125995039" + }, + { + "file": "mocks/handlers.ts", + "hash": "12499954882246532125" + }, + { + "file": "mocks/resources/orders/customer-addresses.ts", + "hash": "3062159261834948446" + }, + { + "file": "mocks/resources/orders/customer-orders-empty.ts", + "hash": "14798830139086094535" + }, + { + "file": "mocks/resources/orders/customer-orders-full.ts", + "hash": "2746152752625492171" + }, + { + "file": "mocks/resources/orders/customer-orders.ts", + "hash": "7113058641369650579" + }, + { + "file": "mocks/server.ts", + "hash": "5934955616392598718" + }, + { + "file": "mocks/setup.ts", + "hash": "9665122635233125102" + }, + { + "file": "package.json", + "hash": "13978673939301467867" + }, + { + "file": "specs/addresses/billing-info.spec.tsx", + "hash": "1329772878926531560" + }, + { + "file": "specs/addresses/invert-addresses.spec.tsx", + "hash": "12910851511072962599" + }, + { + "file": "specs/customers/customer-payments.spec.tsx", + "hash": "16011431952026699603" + }, + { + "file": "specs/e2e/baseFixtures.ts", + "hash": "5966017349827799637" + }, + { + "file": "specs/e2e/checkout/customer/addresses-country-lock.spec.ts", + "hash": "13575317123409741076" + }, + { + "file": "specs/e2e/config/dotenv-config.ts", + "hash": "903216459456041707" + }, + { + "file": "specs/e2e/fixtures/checkout/customer/addresses-country-lock.json", + "hash": "6717452309496923667" + }, + { + "file": "specs/e2e/fixtures/prices-requests.json", + "hash": "12516045920807400121" + }, + { + "file": "specs/e2e/mocks/address-country-lock.mock.json", + "hash": "13756285913762296322" + }, + { + "file": "specs/e2e/mocks/addresses.mock.json", + "hash": "7617941259751412934" + }, + { + "file": "specs/e2e/mocks/order.mock.json", + "hash": "6435175570995894477" + }, + { + "file": "specs/e2e/mocks/prices.mock.json", + "hash": "4949507237215436627" + }, + { + "file": "specs/e2e/mocks/single-price.mock.json", + "hash": "4329131441327390305" + }, + { + "file": "specs/e2e/models/OrderPage.ts", + "hash": "3160458359685251848" + }, + { + "file": "specs/e2e/models/Page.ts", + "hash": "8486912129113750548" + }, + { + "file": "specs/e2e/models/index.ts", + "hash": "1144923561822805068" + }, + { + "file": "specs/e2e/order.spec.ts", + "hash": "10661459600053402015" + }, + { + "file": "specs/e2e/order/add-item-to-hosted-cart.spec.ts", + "hash": "12529622105556346770" + }, + { + "file": "specs/e2e/order/buy-now-mode.spec.ts", + "hash": "13435485510194444651" + }, + { + "file": "specs/e2e/order/order-with-cart-link.spec.ts", + "hash": "90406908587255769" + }, + { + "file": "specs/e2e/prices.spec.ts", + "hash": "14970813890567701275" + }, + { + "file": "specs/e2e/screenshots/customer-addresses-country-lock.jpg", + "hash": "2376972036097758791" + }, + { + "file": "specs/e2e/screenshots/prices.jpg", + "hash": "14921800750174088622" + }, + { + "file": "specs/e2e/single-price.spec.ts", + "hash": "8128018829751989849" + }, + { + "file": "specs/e2e/utils/response.ts", + "hash": "16768066419708606889" + }, + { + "file": "specs/hooks/useCommerceLayer.spec.tsx", + "hash": "5255740947498241186" + }, + { + "file": "specs/hooks/useCustomerContainer.spec.tsx", + "hash": "2956608637327102395" + }, + { + "file": "specs/hooks/useOrderContainer.spec.tsx", + "hash": "4841849676300839338" + }, + { + "file": "specs/in_stock_subscriptions/in_stock_subscriptions.spec.tsx", + "hash": "3648386706013150014" + }, + { + "file": "specs/line_items/line-items.spec.tsx", + "hash": "13577400116202707751" + }, + { + "file": "specs/orders/add-to-cart-button.spec.tsx", + "hash": "17442009357151417635" + }, + { + "file": "specs/orders/order-container.spec.tsx", + "hash": "10048970658837370586" + }, + { + "file": "specs/orders/order-list.spec.tsx", + "hash": "13032779152115423651" + }, + { + "file": "specs/orders/place-order-container.spec.tsx", + "hash": "2159970326140207071" + }, + { + "file": "specs/parcels/parcels.spec.tsx", + "hash": "501863664702650867" + }, + { + "file": "specs/payment_methods/payment-method-name.spec.tsx", + "hash": "9455765861786092585" + }, + { + "file": "specs/payment_methods/payment-method-price.spec.tsx", + "hash": "11078079749800575153" + }, + { + "file": "specs/payment_methods/payment-method-radio-button.spec.tsx", + "hash": "739442231147644608" + }, + { + "file": "specs/payment_methods/payment-method.spec.tsx", + "hash": "10146114709999176352" + }, + { + "file": "specs/payment_methods/payment-methods-container.spec.tsx", + "hash": "12292742260027627609" + }, + { + "file": "specs/prices/prices.spec.tsx", + "hash": "2032479475979980682" + }, + { + "file": "specs/skus/availability.spec.tsx", + "hash": "10565394385586815790" + }, + { + "file": "specs/utils/context.ts", + "hash": "5256172478999498743" + }, + { + "file": "specs/utils/getToken.ts", + "hash": "8369412372642327795" + }, + { + "file": "specs/utils/toJSON.ts", + "hash": "6885329302962511159" + }, + { + "file": "src/components/ExternalFunction.tsx", + "hash": "4477278072570244890" + }, + { + "file": "src/components/MetadataInput.tsx", + "hash": "3009322514610858965" + }, + { + "file": "src/components/SubmitButton.tsx", + "hash": "8959877522409022898" + }, + { + "file": "src/components/addresses/Address.tsx", + "hash": "15364651165636173963" + }, + { + "file": "src/components/addresses/AddressCountrySelector.tsx", + "hash": "8339440043301423595" + }, + { + "file": "src/components/addresses/AddressField.tsx", + "hash": "16019301195478346117" + }, + { + "file": "src/components/addresses/AddressInput.tsx", + "hash": "14453554145221127058" + }, + { + "file": "src/components/addresses/AddressInputSelect.tsx", + "hash": "6505723214910101143" + }, + { + "file": "src/components/addresses/AddressStateSelector.tsx", + "hash": "12980215710326524078" + }, + { + "file": "src/components/addresses/AddressesContainer.tsx", + "hash": "17685111080688659894" + }, + { + "file": "src/components/addresses/AddressesEmpty.tsx", + "hash": "6838612261011283606" + }, + { + "file": "src/components/addresses/BillingAddressContainer.tsx", + "hash": "6516385403048719979" + }, + { + "file": "src/components/addresses/BillingAddressForm.tsx", + "hash": "16185306904812096694" + }, + { + "file": "src/components/addresses/SaveAddressesButton.tsx", + "hash": "13070472713349944290" + }, + { + "file": "src/components/addresses/ShippingAddressContainer.tsx", + "hash": "16681788586621074600" + }, + { + "file": "src/components/addresses/ShippingAddressForm.tsx", + "hash": "2149393492430096940" + }, + { + "file": "src/components/auth/CommerceLayer.tsx", + "hash": "13773260306148143112" + }, + { + "file": "src/components/customers/CustomerAddressForm.tsx", + "hash": "17670718970088594809" + }, + { + "file": "src/components/customers/CustomerContainer.tsx", + "hash": "6407439278442348556" + }, + { + "file": "src/components/customers/CustomerField.tsx", + "hash": "5568030077763803345" + }, + { + "file": "src/components/customers/CustomerInput.tsx", + "hash": "16236670929091576965" + }, + { + "file": "src/components/customers/CustomerPaymentSource.tsx", + "hash": "15301389394150783684" + }, + { + "file": "src/components/customers/CustomerPaymentSourceEmpty.tsx", + "hash": "13743687361403023162" + }, + { + "file": "src/components/customers/MyAccountLink.tsx", + "hash": "9564335873632279592" + }, + { + "file": "src/components/customers/MyIdentityLink.tsx", + "hash": "7387508386037102847" + }, + { + "file": "src/components/customers/SaveCustomerButton.tsx", + "hash": "3594851557839765150" + }, + { + "file": "src/components/errors/Errors.tsx", + "hash": "13949650875176726820" + }, + { + "file": "src/components/gift_cards/GiftCard.tsx", + "hash": "12313159795374386063" + }, + { + "file": "src/components/gift_cards/GiftCardContainer.tsx", + "hash": "3565825021664629282" + }, + { + "file": "src/components/gift_cards/GiftCardCurrencySelector.tsx", + "hash": "7872564585062773479" + }, + { + "file": "src/components/gift_cards/GiftCardInput.tsx", + "hash": "1613870506874450967" + }, + { + "file": "src/components/gift_cards/GiftCardOrCouponCode.tsx", + "hash": "800573932215005956" + }, + { + "file": "src/components/gift_cards/GiftCardOrCouponForm.tsx", + "hash": "13930520297501639295" + }, + { + "file": "src/components/gift_cards/GiftCardOrCouponInput.tsx", + "hash": "11416851489139077966" + }, + { + "file": "src/components/gift_cards/GiftCardOrCouponRemoveButton.tsx", + "hash": "2286148500692455618" + }, + { + "file": "src/components/gift_cards/GiftCardOrCouponSubmit.tsx", + "hash": "3929377468372385293" + }, + { + "file": "src/components/gift_cards/GiftCardRecipientInput.tsx", + "hash": "6369159382540769604" + }, + { + "file": "src/components/in_stock_subscriptions/InStockSubscriptionButton.tsx", + "hash": "32425730784673875" + }, + { + "file": "src/components/in_stock_subscriptions/InStockSubscriptionsContainer.tsx", + "hash": "13410873681323980307" + }, + { + "file": "src/components/line_items/LineItem.tsx", + "hash": "17107016746429034503" + }, + { + "file": "src/components/line_items/LineItemAmount.tsx", + "hash": "8813336253569359075" + }, + { + "file": "src/components/line_items/LineItemCode.tsx", + "hash": "4946784237911184186" + }, + { + "file": "src/components/line_items/LineItemField.tsx", + "hash": "1466906360145479301" + }, + { + "file": "src/components/line_items/LineItemImage.tsx", + "hash": "15682861097708502129" + }, + { + "file": "src/components/line_items/LineItemName.tsx", + "hash": "16722426750166957036" + }, + { + "file": "src/components/line_items/LineItemOption.tsx", + "hash": "9984518596299688993" + }, + { + "file": "src/components/line_items/LineItemOptions.tsx", + "hash": "14116714877090361002" + }, + { + "file": "src/components/line_items/LineItemQuantity.tsx", + "hash": "1714932053770710084" + }, + { + "file": "src/components/line_items/LineItemRemoveLink.tsx", + "hash": "5180150913237420716" + }, + { + "file": "src/components/line_items/LineItemsContainer.tsx", + "hash": "10354553078967764066" + }, + { + "file": "src/components/line_items/LineItemsCount.tsx", + "hash": "1245337282786114933" + }, + { + "file": "src/components/line_items/LineItemsEmpty.tsx", + "hash": "5384899333827251448" + }, + { + "file": "src/components/orders/AddToCartButton.tsx", + "hash": "9990828973612428191" + }, + { + "file": "src/components/orders/AdjustmentAmount.tsx", + "hash": "18270415583907987064" + }, + { + "file": "src/components/orders/CartLink.tsx", + "hash": "3117387206060697879" + }, + { + "file": "src/components/orders/CheckoutLink.tsx", + "hash": "5133862105475549883" + }, + { + "file": "src/components/orders/DiscountAmount.tsx", + "hash": "15282429434595140888" + }, + { + "file": "src/components/orders/GiftCardAmount.tsx", + "hash": "15191158686912663696" + }, + { + "file": "src/components/orders/HostedCart.tsx", + "hash": "14904768400976156662" + }, + { + "file": "src/components/orders/OrderContainer.tsx", + "hash": "10989150616923109263" + }, + { + "file": "src/components/orders/OrderList.tsx", + "hash": "16459616392111329151" + }, + { + "file": "src/components/orders/OrderListEmpty.tsx", + "hash": "12209930124406137334" + }, + { + "file": "src/components/orders/OrderListPaginationButtons.tsx", + "hash": "14622128680582977291" + }, + { + "file": "src/components/orders/OrderListPaginationInfo.tsx", + "hash": "2637848658216855353" + }, + { + "file": "src/components/orders/OrderListRow.tsx", + "hash": "10500306453179847948" + }, + { + "file": "src/components/orders/OrderNumber.tsx", + "hash": "11415925160821920264" + }, + { + "file": "src/components/orders/OrderStorage.tsx", + "hash": "15001346066689031815" + }, + { + "file": "src/components/orders/PaymentMethodAmount.tsx", + "hash": "10586312689512234665" + }, + { + "file": "src/components/orders/PlaceOrderButton.tsx", + "hash": "7477810057224650180" + }, + { + "file": "src/components/orders/PlaceOrderContainer.tsx", + "hash": "5850057881748931599" + }, + { + "file": "src/components/orders/PrivacyAndTermsCheckbox.tsx", + "hash": "6054253585851060667" + }, + { + "file": "src/components/orders/ShippingAmount.tsx", + "hash": "2553816200103060650" + }, + { + "file": "src/components/orders/SubTotalAmount.tsx", + "hash": "13216732528526896146" + }, + { + "file": "src/components/orders/TaxesAmount.tsx", + "hash": "5772480629577127513" + }, + { + "file": "src/components/orders/TotalAmount.tsx", + "hash": "2569744579897020632" + }, + { + "file": "src/components/parcels/ParcelField.tsx", + "hash": "2479022267528224017" + }, + { + "file": "src/components/parcels/ParcelLineItem.tsx", + "hash": "13557646371192171614" + }, + { + "file": "src/components/parcels/ParcelLineItemField.tsx", + "hash": "6591360600051537567" + }, + { + "file": "src/components/parcels/ParcelLineItemsCount.tsx", + "hash": "6499096300952272106" + }, + { + "file": "src/components/parcels/Parcels.tsx", + "hash": "17864277209855903368" + }, + { + "file": "src/components/parcels/ParcelsCount.tsx", + "hash": "16707798467882903599" + }, + { + "file": "src/components/payment_gateways/AdyenGateway.tsx", + "hash": "4289580452088608518" + }, + { + "file": "src/components/payment_gateways/BraintreeGateway.tsx", + "hash": "3370504121648747448" + }, + { + "file": "src/components/payment_gateways/CheckoutComGateway.tsx", + "hash": "10124889903251835095" + }, + { + "file": "src/components/payment_gateways/ExternalGateway.tsx", + "hash": "6375439034708402163" + }, + { + "file": "src/components/payment_gateways/KlarnaGateway.tsx", + "hash": "8857221062271961174" + }, + { + "file": "src/components/payment_gateways/PaymentGateway.tsx", + "hash": "1634593290070982525" + }, + { + "file": "src/components/payment_gateways/PaypalGateway.tsx", + "hash": "943086920234617698" + }, + { + "file": "src/components/payment_gateways/StripeGateway.tsx", + "hash": "7490981033971540846" + }, + { + "file": "src/components/payment_gateways/WireTransferGateway.tsx", + "hash": "14613623917034259673" + }, + { + "file": "src/components/payment_methods/PaymentMethod.tsx", + "hash": "1234769459800219497" + }, + { + "file": "src/components/payment_methods/PaymentMethodName.tsx", + "hash": "1428813489789016037" + }, + { + "file": "src/components/payment_methods/PaymentMethodPrice.tsx", + "hash": "3262543958209802289" + }, + { + "file": "src/components/payment_methods/PaymentMethodRadioButton.tsx", + "hash": "6120765113012241650" + }, + { + "file": "src/components/payment_methods/PaymentMethodsContainer.tsx", + "hash": "13500720427041608545" + }, + { + "file": "src/components/payment_source/AdyenPayment.tsx", + "hash": "12304069213235388152" + }, + { + "file": "src/components/payment_source/BraintreePayment.tsx", + "hash": "2141999897177105813" + }, + { + "file": "src/components/payment_source/CheckoutComPayment.tsx", + "hash": "16025663780874855145" + }, + { + "file": "src/components/payment_source/ExternalPayment.tsx", + "hash": "5694198029386622542" + }, + { + "file": "src/components/payment_source/KlarnaPayment.tsx", + "hash": "6194417480293791944" + }, + { + "file": "src/components/payment_source/PaymentSource.tsx", + "hash": "3131980522562763301" + }, + { + "file": "src/components/payment_source/PaymentSourceBrandIcon.tsx", + "hash": "7694197736077644383" + }, + { + "file": "src/components/payment_source/PaymentSourceBrandName.tsx", + "hash": "13242878935075524294" + }, + { + "file": "src/components/payment_source/PaymentSourceDetail.tsx", + "hash": "2700871439050675681" + }, + { + "file": "src/components/payment_source/PaymentSourceEditButton.tsx", + "hash": "5724414500297371534" + }, + { + "file": "src/components/payment_source/PaypalPayment.tsx", + "hash": "16635726432020900236" + }, + { + "file": "src/components/payment_source/StripeExpressPayment.tsx", + "hash": "5224980357473895994" + }, + { + "file": "src/components/payment_source/StripePayment.tsx", + "hash": "3151020703764682005" + }, + { + "file": "src/components/payment_source/WireTransferPayment.tsx", + "hash": "1758339138210204062" + }, + { + "file": "src/components/prices/Price.tsx", + "hash": "13354606172557409688" + }, + { + "file": "src/components/prices/PricesContainer.tsx", + "hash": "1803311102870345717" + }, + { + "file": "src/components/shipments/Shipment.tsx", + "hash": "8673990529443199199" + }, + { + "file": "src/components/shipments/ShipmentField.tsx", + "hash": "11047884690646347554" + }, + { + "file": "src/components/shipments/ShipmentsContainer.tsx", + "hash": "5525041679044453630" + }, + { + "file": "src/components/shipments/ShipmentsCount.tsx", + "hash": "16071767696779541334" + }, + { + "file": "src/components/shipping_methods/ShippingMethod.tsx", + "hash": "4150518205127715913" + }, + { + "file": "src/components/shipping_methods/ShippingMethodName.tsx", + "hash": "62804487225418935" + }, + { + "file": "src/components/shipping_methods/ShippingMethodPrice.tsx", + "hash": "8131114196466290345" + }, + { + "file": "src/components/shipping_methods/ShippingMethodRadioButton.tsx", + "hash": "7370105366477516681" + }, + { + "file": "src/components/skus/AvailabilityContainer.tsx", + "hash": "8493837441386870299" + }, + { + "file": "src/components/skus/AvailabilityTemplate.tsx", + "hash": "13905481633188154652" + }, + { + "file": "src/components/skus/DeliveryLeadTime.tsx", + "hash": "15234788942791890249" + }, + { + "file": "src/components/skus/SkuField.tsx", + "hash": "16726109204886334920" + }, + { + "file": "src/components/skus/SkuList.tsx", + "hash": "18282669346665222185" + }, + { + "file": "src/components/skus/SkuListsContainer.tsx", + "hash": "12646808348406002892" + }, + { + "file": "src/components/skus/Skus.tsx", + "hash": "2894623678508952544" + }, + { + "file": "src/components/skus/SkusContainer.tsx", + "hash": "2621260794687905962" + }, + { + "file": "src/components/stock_transfers/StockTransfer.tsx", + "hash": "10075257827178763569" + }, + { + "file": "src/components/stock_transfers/StockTransferField.tsx", + "hash": "17895276919683398519" + }, + { + "file": "src/components/utils/AddressCardsTemplate.tsx", + "hash": "9139095990809993770" + }, + { + "file": "src/components/utils/BaseField.tsx", + "hash": "1671775551318462295" + }, + { + "file": "src/components/utils/BaseInput.tsx", + "hash": "373373923192740002" + }, + { + "file": "src/components/utils/BaseOrderPrice.tsx", + "hash": "15544993476466576367" + }, + { + "file": "src/components/utils/BaseSelect.tsx", + "hash": "7898328333096358043" + }, + { + "file": "src/components/utils/ErrorBoundary.tsx", + "hash": "10375724623529426467" + }, + { + "file": "src/components/utils/GenericFieldComponent.tsx", + "hash": "11104367537637121366" + }, + { + "file": "src/components/utils/Parent.tsx", + "hash": "18071932662043446601" + }, + { + "file": "src/components/utils/PaymentCardsTemplate.tsx", + "hash": "12877596280376839012" + }, + { + "file": "src/components/utils/PriceTemplate.tsx", + "hash": "16834136221672670621" + }, + { + "file": "src/components/utils/getAllErrors.tsx", + "hash": "4937860445902786071" + }, + { + "file": "src/config/currency.ts", + "hash": "7590018339509175542" + }, + { + "file": "src/config/orders.json", + "hash": "5810265018806251716" + }, + { + "file": "src/context/AddressChildrenContext.ts", + "hash": "11370219895958502800" + }, + { + "file": "src/context/AddressContext.ts", + "hash": "7106796041241273252" + }, + { + "file": "src/context/AvailabilityContext.ts", + "hash": "10573215499552092852" + }, + { + "file": "src/context/BillingAddressContext.ts", + "hash": "18314463872326897698" + }, + { + "file": "src/context/BillingAddressFormContext.ts", + "hash": "1745047929843375379" + }, + { + "file": "src/context/CommerceLayerContext.ts", + "hash": "17536073353578081739" + }, + { + "file": "src/context/CouponAndGiftCardFormContext.ts", + "hash": "6254790486855579477" + }, + { + "file": "src/context/CustomerAddressFormContext.ts", + "hash": "8473942556290708804" + }, + { + "file": "src/context/CustomerContext.ts", + "hash": "7733788254357684413" + }, + { + "file": "src/context/CustomerPaymentSourceContext.ts", + "hash": "8528577098597819954" + }, + { + "file": "src/context/ExternalFunctionContext.ts", + "hash": "17056658114274960650" + }, + { + "file": "src/context/GiftCardContext.ts", + "hash": "549293836646478064" + }, + { + "file": "src/context/InStockSubscriptionContext.ts", + "hash": "11291144030411681848" + }, + { + "file": "src/context/LineItemChildrenContext.ts", + "hash": "16488573770344719995" + }, + { + "file": "src/context/LineItemContext.ts", + "hash": "17339068972471553791" + }, + { + "file": "src/context/LineItemOptionChildrenContext.ts", + "hash": "3783592903715978551" + }, + { + "file": "src/context/OrderContext.ts", + "hash": "2838145240785047761" + }, + { + "file": "src/context/OrderListChildrenContext.ts", + "hash": "6885009484177260980" + }, + { + "file": "src/context/OrderListPaginationContext.ts", + "hash": "2127638373883872740" + }, + { + "file": "src/context/OrderStorageContext.ts", + "hash": "6999106712689868738" + }, + { + "file": "src/context/ParcelChildrenContext.ts", + "hash": "1699753540805625598" + }, + { + "file": "src/context/ParcelLineItemChildrenContext.ts", + "hash": "6286274595084272914" + }, + { + "file": "src/context/PaymentMethodChildrenContext.ts", + "hash": "11042249994537222885" + }, + { + "file": "src/context/PaymentMethodContext.ts", + "hash": "276518954990775408" + }, + { + "file": "src/context/PaymentSourceContext.ts", + "hash": "9144348834623034090" + }, + { + "file": "src/context/PlaceOrderContext.ts", + "hash": "12250739473525995720" + }, + { + "file": "src/context/PricesContext.ts", + "hash": "5206007700284273420" + }, + { + "file": "src/context/ShipmentChildrenContext.ts", + "hash": "13662940779135847654" + }, + { + "file": "src/context/ShipmentContext.ts", + "hash": "4827157931244165805" + }, + { + "file": "src/context/ShippingAddressContext.ts", + "hash": "9755139573855278061" + }, + { + "file": "src/context/ShippingAddressFormContext.ts", + "hash": "15828573789241022125" + }, + { + "file": "src/context/ShippingMethodChildrenContext.ts", + "hash": "15940626102898090755" + }, + { + "file": "src/context/SkuChildrenContext.ts", + "hash": "17728031729788170290" + }, + { + "file": "src/context/SkuContext.ts", + "hash": "6112515363351810279" + }, + { + "file": "src/context/SkuListsContext.ts", + "hash": "7610524741852781950" + }, + { + "file": "src/context/StockTransferChildrenContext.ts", + "hash": "8660039927288633476" + }, + { + "file": "src/hooks/useCommerceLayer.ts", + "hash": "17530374019718574596" + }, + { + "file": "src/hooks/useCustomerContainer.ts", + "hash": "1148596126380467126" + }, + { + "file": "src/hooks/useOrderContainer.ts", + "hash": "18124636516255691831" + }, + { + "file": "src/index.ts", + "hash": "8889727909280837596" + }, + { + "file": "src/reducers/AddressReducer.ts", + "hash": "5788385017160200272" + }, + { + "file": "src/reducers/AvailabilityReducer.ts", + "hash": "10633714196778689143" + }, + { + "file": "src/reducers/BillingAddressReducer.ts", + "hash": "6971061421560561159" + }, + { + "file": "src/reducers/CustomerReducer.ts", + "hash": "6078762931636995855" + }, + { + "file": "src/reducers/GiftCardReducer.ts", + "hash": "4924997460068021350" + }, + { + "file": "src/reducers/InStockSubscriptionReducer.ts", + "hash": "2805715265769667085" + }, + { + "file": "src/reducers/LineItemReducer.ts", + "hash": "13382048063966633067" + }, + { + "file": "src/reducers/OrderReducer.ts", + "hash": "583910089631752616" + }, + { + "file": "src/reducers/PaymentMethodReducer.ts", + "hash": "16985550059132669934" + }, + { + "file": "src/reducers/PlaceOrderReducer.ts", + "hash": "10015541307429491949" + }, + { + "file": "src/reducers/PriceReducer.ts", + "hash": "5772692655245253230" + }, + { + "file": "src/reducers/ShipmentReducer.ts", + "hash": "2069561022111067454" + }, + { + "file": "src/reducers/ShippingAddressReducer.ts", + "hash": "12805605603443736104" + }, + { + "file": "src/reducers/SkuListsReducer.ts", + "hash": "15488063052326472611" + }, + { + "file": "src/reducers/SkuReducer.ts", + "hash": "2206557443888352390" + }, + { + "file": "src/typings/environment.d.ts", + "hash": "480632733005836266" + }, + { + "file": "src/typings/errors.ts", + "hash": "17845706484672150999" + }, + { + "file": "src/typings/globals.ts", + "hash": "8105832499387190147" + }, + { + "file": "src/typings/index.ts", + "hash": "15306025869318985613" + }, + { + "file": "src/utils/PropsType.ts", + "hash": "12716844381350644279" + }, + { + "file": "src/utils/addressesManager.ts", + "hash": "11319450380500188349" + }, + { + "file": "src/utils/adyen/manageGiftCard.ts", + "hash": "2422236668445414487" + }, + { + "file": "src/utils/baseReducer.ts", + "hash": "8837257405067094769" + }, + { + "file": "src/utils/browserInfo.ts", + "hash": "17224495239215810382" + }, + { + "file": "src/utils/canPlaceOrder.ts", + "hash": "13554448490417217856" + }, + { + "file": "src/utils/checkIncludeResource.ts", + "hash": "6357196086689645168" + }, + { + "file": "src/utils/compareObjAttribute.ts", + "hash": "1027986662675735691" + }, + { + "file": "src/utils/countryStateCity.ts", + "hash": "12140652270984813757" + }, + { + "file": "src/utils/customMessages.ts", + "hash": "3876281094742821885" + }, + { + "file": "src/utils/customerOrderOptions.ts", + "hash": "4716866351915835543" + }, + { + "file": "src/utils/events.ts", + "hash": "7344184284433653427" + }, + { + "file": "src/utils/expressPaymentHelper.ts", + "hash": "203104313203952287" + }, + { + "file": "src/utils/filterChildren.ts", + "hash": "15893840755093237411" + }, + { + "file": "src/utils/formCleaner.ts", + "hash": "16018511934335652365" + }, + { + "file": "src/utils/getAmount.ts", + "hash": "1725196146749370621" + }, + { + "file": "src/utils/getApplicationLink.ts", + "hash": "2028712277244910906" + }, + { + "file": "src/utils/getCardDetails.ts", + "hash": "17398849329508394523" + }, + { + "file": "src/utils/getCustomerIdByToken.ts", + "hash": "504314895394395895" + }, + { + "file": "src/utils/getDomain.ts", + "hash": "12184591448689403424" + }, + { + "file": "src/utils/getErrors.ts", + "hash": "13433878503495784626" + }, + { + "file": "src/utils/getLineItemsCount.ts", + "hash": "13764781773290138444" + }, + { + "file": "src/utils/getLoaderComponent.tsx", + "hash": "12563108746465658643" + }, + { + "file": "src/utils/getPaymentAttributes.ts", + "hash": "3871586142824889187" + }, + { + "file": "src/utils/getPrices.tsx", + "hash": "11823698463708396210" + }, + { + "file": "src/utils/getPublicIp.ts", + "hash": "4135149096604135562" + }, + { + "file": "src/utils/getSdk.ts", + "hash": "14858560611756738780" + }, + { + "file": "src/utils/getSkus.ts", + "hash": "17794536032626350327" + }, + { + "file": "src/utils/hasSubscriptions.ts", + "hash": "3201784536693855584" + }, + { + "file": "src/utils/hooks/useCustomContext.ts", + "hash": "6085075008193176284" + }, + { + "file": "src/utils/hooks/useExternalScript.ts", + "hash": "3239315212182631321" + }, + { + "file": "src/utils/icons.tsx", + "hash": "17662992595528022917" + }, + { + "file": "src/utils/isDate.ts", + "hash": "14904609653068475292" + }, + { + "file": "src/utils/isEmpty.ts", + "hash": "6596929891731412946" + }, + { + "file": "src/utils/isGuestToken.ts", + "hash": "10747875669908100831" + }, + { + "file": "src/utils/isJSON.ts", + "hash": "270589554382302633" + }, + { + "file": "src/utils/jwt.ts", + "hash": "6821300073816394066" + }, + { + "file": "src/utils/localStorage.ts", + "hash": "15441199968808322264" + }, + { + "file": "src/utils/omit.ts", + "hash": "7962313321808423937" + }, + { + "file": "src/utils/organization.ts", + "hash": "4463847555061258292" + }, + { + "file": "src/utils/payment-methods/sortPaymentMethods.ts", + "hash": "8704942629495668396" + }, + { + "file": "src/utils/pick.ts", + "hash": "3369689051388190515" + }, + { + "file": "src/utils/placeholderImages.ts", + "hash": "15800282338272295866" + }, + { + "file": "src/utils/promisify.ts", + "hash": "5670248357101562571" + }, + { + "file": "src/utils/replace.ts", + "hash": "13814783648155632525" + }, + { + "file": "src/utils/scrollbarWidth.ts", + "hash": "5126381275733394663" + }, + { + "file": "src/utils/shipments.ts", + "hash": "13105025196817319713" + }, + { + "file": "src/utils/snakeToCamelCase.ts", + "hash": "15108300771933465369" + }, + { + "file": "src/utils/stripe/retrievePaymentIntent.ts", + "hash": "5820528781726234251" + }, + { + "file": "src/utils/triggerAttributeHelper.ts", + "hash": "4378938554354686730" + }, + { + "file": "src/utils/updateOrderSubscriptionCustomerPaymentSource.ts", + "hash": "4884887114524879370" + }, + { + "file": "src/utils/validateFormFields.ts", + "hash": "8595516885740042112" + }, + { + "file": "tsconfig.json", + "hash": "15033548596562235298" + }, + { + "file": "tsconfig.prod.esm.json", + "hash": "12651144163998481872" + }, + { + "file": "tsconfig.prod.json", + "hash": "15350800236315528219" + }, + { + "file": "vitest.config.mts", + "hash": "678457360611212538" + } + ] + } +} \ No newline at end of file diff --git a/packages/react-components/.nx/workspace-data/nx_files.nxt b/packages/react-components/.nx/workspace-data/nx_files.nxt new file mode 100644 index 00000000..a0748478 Binary files /dev/null and b/packages/react-components/.nx/workspace-data/nx_files.nxt differ diff --git a/packages/react-components/.nx/workspace-data/project-graph.json b/packages/react-components/.nx/workspace-data/project-graph.json new file mode 100644 index 00000000..992e9100 --- /dev/null +++ b/packages/react-components/.nx/workspace-data/project-graph.json @@ -0,0 +1,8 @@ +{ + "nodes": {}, + "externalNodes": {}, + "dependencies": {}, + "version": "6.0", + "errors": [], + "computedAt": 1753282603613 +} \ No newline at end of file diff --git a/packages/react-components/.nx/workspace-data/project-graph.lock b/packages/react-components/.nx/workspace-data/project-graph.lock new file mode 100644 index 00000000..e69de29b diff --git a/packages/react-components/.nx/workspace-data/source-maps.json b/packages/react-components/.nx/workspace-data/source-maps.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/packages/react-components/.nx/workspace-data/source-maps.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/react-components/package.json b/packages/react-components/package.json index f982e2c6..b94015da 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -1,11 +1,15 @@ { "name": "@commercelayer/react-components", - "version": "4.20.6", + "version": "4.28.6", "description": "The Official Commerce Layer React Components", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", "types": "lib/esm/index.d.ts", - "files": ["lib", "package.json", "README.md"], + "files": [ + "lib", + "package.json", + "README.md" + ], "exports": { ".": { "require": "./lib/cjs/index.js", @@ -94,35 +98,73 @@ }, "typesVersions": { "*": { - "addresses/*": ["lib/esm/components/addresses/*.d.ts"], - "auth/*": ["lib/esm/components/auth/*.d.ts"], - "customers/*": ["lib/esm/components/customers/*.d.ts"], - "errors/*": ["lib/esm/components/errors/*.d.ts"], - "gift_cards/*": ["lib/esm/components/gift_cards/*.d.ts"], + "addresses/*": [ + "lib/esm/components/addresses/*.d.ts" + ], + "auth/*": [ + "lib/esm/components/auth/*.d.ts" + ], + "customers/*": [ + "lib/esm/components/customers/*.d.ts" + ], + "errors/*": [ + "lib/esm/components/errors/*.d.ts" + ], + "gift_cards/*": [ + "lib/esm/components/gift_cards/*.d.ts" + ], "in_stock_subscriptions/*": [ "lib/esm/components/in_stock_subscriptions/*.d.ts" ], - "hooks/*": ["lib/esm/hooks/*.d.ts"], - "line_items/*": ["lib/esm/components/line_items/*.d.ts"], - "orders/*": ["lib/esm/components/orders/*.d.ts"], - "parcels/*": ["lib/esm/components/parcels/*.d.ts"], - "payment_methods/*": ["lib/esm/components/payment_methods/*.d.ts"], - "payment_source/*": ["lib/esm/components/payment_source/*.d.ts"], - "prices/*": ["lib/esm/components/prices/*.d.ts"], - "shipments/*": ["lib/esm/components/shipments/*.d.ts"], - "shipping_methods/*": ["lib/esm/components/shipping_methods/*.d.ts"], - "skus/*": ["lib/esm/components/skus/*.d.ts"], - "stock_transfers/*": ["lib/esm/components/stock_transfers/*.d.ts"], - "context/*": ["lib/esm/context/*.d.ts"], - "utils/*": ["lib/esm/utils/*.d.ts"], - "component_utils/*": ["lib/esm/components/utils/*.d.ts"] + "hooks/*": [ + "lib/esm/hooks/*.d.ts" + ], + "line_items/*": [ + "lib/esm/components/line_items/*.d.ts" + ], + "orders/*": [ + "lib/esm/components/orders/*.d.ts" + ], + "parcels/*": [ + "lib/esm/components/parcels/*.d.ts" + ], + "payment_methods/*": [ + "lib/esm/components/payment_methods/*.d.ts" + ], + "payment_source/*": [ + "lib/esm/components/payment_source/*.d.ts" + ], + "prices/*": [ + "lib/esm/components/prices/*.d.ts" + ], + "shipments/*": [ + "lib/esm/components/shipments/*.d.ts" + ], + "shipping_methods/*": [ + "lib/esm/components/shipping_methods/*.d.ts" + ], + "skus/*": [ + "lib/esm/components/skus/*.d.ts" + ], + "stock_transfers/*": [ + "lib/esm/components/stock_transfers/*.d.ts" + ], + "context/*": [ + "lib/esm/context/*.d.ts" + ], + "utils/*": [ + "lib/esm/utils/*.d.ts" + ], + "component_utils/*": [ + "lib/esm/components/utils/*.d.ts" + ] } }, "publishConfig": { "access": "public" }, "scripts": { - "lint": "biome lint --error-on-warnings ./src", + "lint": "biome lint ./src", "lint:fix": "pnpm biome lint --write ./src", "test": "pnpm audit --audit-level high && (pnpm audit || exit 0) && pnpm lint && vitest run --silent", "coverage": "vitest run --coverage", @@ -157,47 +199,47 @@ }, "homepage": "https://github.com/commercelayer/commercelayer-react-components#readme", "dependencies": { - "@adyen/adyen-web": "^6.11.0", - "@commercelayer/organization-config": "^2.2.0", - "@commercelayer/sdk": "^6.36.0", - "@stripe/react-stripe-js": "^3.5.1", - "@stripe/stripe-js": "^6.1.0", - "@tanstack/react-table": "^8.21.2", + "@adyen/adyen-web": "^6.26.0", + "@commercelayer/organization-config": "^2.5.0", + "@commercelayer/sdk": "^7.4.1", + "@stripe/react-stripe-js": "^5.4.1", + "@stripe/stripe-js": "^8.5.3", + "@tanstack/react-table": "^8.21.3", "@types/iframe-resizer": "^4.0.0", - "braintree-web": "^3.117.1", - "frames-react": "^1.2.2", + "braintree-web": "^3.133.0", + "frames-react": "^1.2.3", "iframe-resizer": "^4.3.6", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "rapid-form": "3.1.0" }, "devDependencies": { - "@commercelayer/js-auth": "^6.7.1", - "@faker-js/faker": "^9.6.0", - "@playwright/test": "^1.51.1", - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.2.0", + "@commercelayer/js-auth": "^7.1.0", + "@faker-js/faker": "^10.1.0", + "@playwright/test": "^1.57.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", "@types/braintree-web": "^3.96.17", - "@types/lodash": "^4.17.16", - "@types/node": "^22.13.14", - "@types/prop-types": "^15.7.14", - "@types/react": "^19.0.12", - "@types/react-test-renderer": "^19.0.0", - "@types/react-window": "^1.8.8", - "@vitejs/plugin-react": "^4.3.4", - "@vitest/coverage-v8": "^3.0.9", - "jsdom": "^26.0.0", + "@types/lodash": "^4.17.21", + "@types/node": "^24.10.2", + "@types/prop-types": "^15.7.15", + "@types/react": "^19.2.7", + "@types/react-test-renderer": "^19.1.0", + "@types/react-window": "^2.0.0", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.15", + "jsdom": "^27.3.0", "minimize-js": "^1.4.0", - "msw": "^2.7.3", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-test-renderer": "^19.0.0", - "tsc-alias": "^1.8.11", + "msw": "^2.12.4", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "react-test-renderer": "^19.2.1", + "tsc-alias": "^1.8.16", "tslib": "^2.8.1", - "typescript": "^5.8.2", - "vite": "^6.2.3", + "typescript": "^5.9.3", + "vite": "^7.2.7", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.9" + "vitest": "^4.0.15" }, "peerDependencies": { "react": ">=18.0.0" diff --git a/packages/react-components/src/components/addresses/Address.tsx b/packages/react-components/src/components/addresses/Address.tsx index 62ae2be1..071f0205 100644 --- a/packages/react-components/src/components/addresses/Address.tsx +++ b/packages/react-components/src/components/addresses/Address.tsx @@ -1,21 +1,21 @@ -import { useContext, useState, useEffect, type JSX } from 'react'; -import AddressChildrenContext from '#context/AddressChildrenContext' -import CustomerContext from '#context/CustomerContext' -import BillingAddressContext from '#context/BillingAddressContext' -import ShippingAddressContext from '#context/ShippingAddressContext' -import type { Address as AddressType } from '@commercelayer/sdk' -import isEmpty from 'lodash/isEmpty' -import AddressContext from '#context/AddressContext' -import OrderContext from '#context/OrderContext' +import type { Address as AddressType } from "@commercelayer/sdk" +import isEmpty from "lodash/isEmpty" +import { type JSX, useContext, useEffect, useState } from "react" import AddressCardsTemplate, { type AddressCardsTemplateChildren, type CustomerAddress, - type HandleSelect -} from '#components/utils/AddressCardsTemplate' -import type { DefaultChildrenType } from '#typings/globals' + type HandleSelect, +} from "#components/utils/AddressCardsTemplate" +import AddressChildrenContext from "#context/AddressChildrenContext" +import AddressContext from "#context/AddressContext" +import BillingAddressContext from "#context/BillingAddressContext" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" +import ShippingAddressContext from "#context/ShippingAddressContext" +import type { DefaultChildrenType } from "#typings/globals" interface Props - extends Omit { + extends Omit { children: DefaultChildrenType | AddressCardsTemplateChildren selectedClassName?: string disabledClassName?: string @@ -45,8 +45,8 @@ export function Address(props: Props): JSX.Element { const { children, className, - selectedClassName = '', - disabledClassName = '', + selectedClassName = "", + disabledClassName = "", onSelect, addresses = [], deselect = false, @@ -54,10 +54,10 @@ export function Address(props: Props): JSX.Element { } = props const { addresses: addressesContext } = useContext(CustomerContext) const { setBillingAddress, billingCustomerAddressId } = useContext( - BillingAddressContext + BillingAddressContext, ) const { setShippingAddress, shippingCustomerAddressId } = useContext( - ShippingAddressContext + ShippingAddressContext, ) const { shipToDifferentAddress, billingAddressId, shippingAddressId } = useContext(AddressContext) @@ -82,7 +82,7 @@ export function Address(props: Props): JSX.Element { address.reference != null ) { setBillingAddress(address.id, { - customerAddressId: address.reference + customerAddressId: address.reference, }) } if (shippingCustomerAddressId) { @@ -96,15 +96,15 @@ export function Address(props: Props): JSX.Element { address.reference != null ) { setShippingAddress(address.id, { - customerAddressId: address.reference + customerAddressId: address.reference, }) } }) } if (deselect) { const disabledSaveButton = async (): Promise => { - setBillingAddress && (await setBillingAddress('')) - setShippingAddress && (await setShippingAddress('')) + setBillingAddress && (await setBillingAddress("")) + setShippingAddress && (await setShippingAddress("")) } disabledSaveButton() } @@ -113,14 +113,14 @@ export function Address(props: Props): JSX.Element { billingCustomerAddressId, shippingCustomerAddressId, addressesContext, - shipToDifferentAddress + shipToDifferentAddress, ]) const handleSelect: HandleSelect = async ( k, addressId, customerAddressId, disabled, - address + address, ) => { !disabled && setSelected(k) setBillingAddress && @@ -132,7 +132,7 @@ export function Address(props: Props): JSX.Element { } const countryLock = order?.shipping_country_code_lock const components = - typeof children === 'function' + typeof children === "function" ? [] : items .filter((address) => { @@ -147,19 +147,19 @@ export function Address(props: Props): JSX.Element { }) .map((address, k) => { const addressProps = { - address + address, } const disabled = (setShippingAddress && countryLock && countryLock !== address.country_code) || false - const selectedClass = deselect ? '' : selectedClassName + const selectedClass = deselect ? "" : selectedClassName const addressSelectedClass = - selected === k ? `${className || ''} ${selectedClass}` : className - const customerAddressId: string = address?.reference || '' + selected === k ? `${className || ""} ${selectedClass}` : className + const customerAddressId: string = address?.reference || "" const finalClassName = disabled - ? `${className || ''} ${disabledClassName}` + ? `${className || ""} ${disabledClassName}` : addressSelectedClass return ( @@ -171,7 +171,7 @@ export function Address(props: Props): JSX.Element { address.id, customerAddressId, disabled, - address + address, ) }} data-disabled={disabled} @@ -187,9 +187,9 @@ export function Address(props: Props): JSX.Element { selected, handleSelect, countryLock, - ...props + ...props, } - return typeof children === 'function' ? ( + return typeof children === "function" ? ( {children} ) : ( <>{components} diff --git a/packages/react-components/src/components/addresses/AddressField.tsx b/packages/react-components/src/components/addresses/AddressField.tsx index 7bd30ede..3c75dd4a 100644 --- a/packages/react-components/src/components/addresses/AddressField.tsx +++ b/packages/react-components/src/components/addresses/AddressField.tsx @@ -1,12 +1,12 @@ -import { useContext, type ReactNode, type JSX } from 'react'; -import AddressChildrenContext from '#context/AddressChildrenContext' -import Parent from '#components/utils/Parent' -import type { AddressFieldView } from '#reducers/AddressReducer' -import type { Address } from '@commercelayer/sdk' -import CustomerContext from '#context/CustomerContext' -import type { ChildrenFunction } from '#typings/index' +import type { Address } from "@commercelayer/sdk" +import { type JSX, type ReactNode, useContext } from "react" +import Parent from "#components/utils/Parent" +import AddressChildrenContext from "#context/AddressChildrenContext" +import CustomerContext from "#context/CustomerContext" +import type { AddressFieldView } from "#reducers/AddressReducer" +import type { ChildrenFunction } from "#typings/index" -interface ChildrenProps extends Omit { +interface ChildrenProps extends Omit { address: Address } @@ -14,7 +14,7 @@ type ChildrenProp = ChildrenFunction type Props = | { - type?: 'field' + type?: "field" label?: never onClick?: never children?: ChildrenProp @@ -22,7 +22,7 @@ type Props = className?: string } | { - type?: 'edit' + type?: "edit" label: string | ReactNode onClick: (address: Address) => void children?: ChildrenProp @@ -30,7 +30,7 @@ type Props = className?: string } | { - type?: 'delete' + type?: "delete" label: string onClick: () => void children?: ChildrenProp @@ -38,7 +38,7 @@ type Props = className?: string } | { - type?: 'edit' | 'field' | 'delete' + type?: "edit" | "field" | "delete" label?: never onClick?: never children: ChildrenProp @@ -69,30 +69,30 @@ type Props = * */ export function AddressField(props: Props): JSX.Element { - const { name, type = 'field', label, onClick, ...p } = props + const { name, type = "field", label, onClick, ...p } = props const { address } = useContext(AddressChildrenContext) - const text = name && address ? address?.[name] : '' + const text = name && address ? address?.[name] : "" const { deleteCustomerAddress } = useContext(CustomerContext) const handleClick = (e: React.MouseEvent): void => { e.stopPropagation() e.preventDefault() - if (type === 'delete' && deleteCustomerAddress && address?.reference) { + if (type === "delete" && deleteCustomerAddress && address?.reference) { deleteCustomerAddress({ customerAddressId: address?.reference }) } address && onClick && onClick(address) } const parentProps = { address, - ...props + ...props, } return props.children ? ( {props.children} - ) : type === 'field' ? ( -

+ ) : type === "field" ? ( +

{text}

) : ( - + {label} ) diff --git a/packages/react-components/src/components/addresses/AddressInput.tsx b/packages/react-components/src/components/addresses/AddressInput.tsx index f444d803..f8653bd8 100644 --- a/packages/react-components/src/components/addresses/AddressInput.tsx +++ b/packages/react-components/src/components/addresses/AddressInput.tsx @@ -1,12 +1,12 @@ -import { useContext, useEffect, useMemo, type JSX } from 'react'; -import BaseInput from '#components/utils/BaseInput' -import type { BaseInputComponentProps, AddressInputName } from '#typings' +import { type JSX, useContext, useEffect, useMemo } from "react" +import BaseInput from "#components/utils/BaseInput" import BillingAddressFormContext, { - type AddressValuesKeys -} from '#context/BillingAddressFormContext' -import ShippingAddressFormContext from '#context/ShippingAddressFormContext' -import { businessMandatoryField } from '#utils/validateFormFields' -import CustomerAddressFormContext from '#context/CustomerAddressFormContext' + type AddressValuesKeys, +} from "#context/BillingAddressFormContext" +import CustomerAddressFormContext from "#context/CustomerAddressFormContext" +import ShippingAddressFormContext from "#context/ShippingAddressFormContext" +import type { AddressInputName, BaseInputComponentProps } from "#typings" +import { businessMandatoryField } from "#utils/validateFormFields" type Props = { /** @@ -18,9 +18,9 @@ type Props = { * Used to add a custom validation rule. Accept a regex as param. */ pattern?: RegExp -} & Omit & - Omit & - Omit +} & Omit & + Omit & + Omit /** * The AddressInput component creates a form `input` related to a particular address attribute. @@ -41,7 +41,7 @@ type Props = { * */ export function AddressInput(props: Props): JSX.Element | null { - const { placeholder = '', required, value, className, ...p } = props + const { placeholder = "", required, value, className, ...p } = props const billingAddress = useContext(BillingAddressFormContext) const shippingAddress = useContext(ShippingAddressFormContext) const customerAddress = useContext(CustomerAddressFormContext) @@ -72,7 +72,7 @@ export function AddressInput(props: Props): JSX.Element | null { value, billingAddress?.errors, shippingAddress?.errors, - customerAddress?.errors + customerAddress?.errors, ]) const mandatoryField = billingAddress?.isBusiness @@ -81,17 +81,17 @@ export function AddressInput(props: Props): JSX.Element | null { const reqField = required !== undefined ? required : mandatoryField const errorClassName = billingAddress?.errorClassName || shippingAddress?.errorClassName - const classNameComputed = `${className || ''} ${ - hasError && errorClassName ? errorClassName : '' + const classNameComputed = `${className || ""} ${ + hasError && errorClassName ? errorClassName : "" }` if ( - p.name === 'billing_address_billing_info' && + p.name === "billing_address_billing_info" && billingAddress.requiresBillingInfo === false && required === undefined ) return null if ( - p.name === 'shipping_address_billing_info' && + p.name === "shipping_address_billing_info" && shippingAddress.requiresBillingInfo === false && required === undefined ) diff --git a/packages/react-components/src/components/addresses/AddressesContainer.tsx b/packages/react-components/src/components/addresses/AddressesContainer.tsx index 471a93d7..300c9443 100644 --- a/packages/react-components/src/components/addresses/AddressesContainer.tsx +++ b/packages/react-components/src/components/addresses/AddressesContainer.tsx @@ -1,21 +1,27 @@ +import { + type JSX, + type ReactNode, + useContext, + useEffect, + useReducer, +} from "react" import AddressesContext, { - defaultAddressContext -} from '#context/AddressContext' -import { type ReactNode, useContext, useEffect, useReducer, type JSX } from 'react'; + defaultAddressContext, +} from "#context/AddressContext" +import CommerceLayerContext from "#context/CommerceLayerContext" +import OrderContext from "#context/OrderContext" import addressReducer, { - addressInitialState, type AddressResource, - setAddressErrors, + addressInitialState, + type ICustomerAddress, type SetAddressParams, - setCloneAddress, saveAddresses, - type ICustomerAddress -} from '#reducers/AddressReducer' -import type { BaseError } from '#typings/errors' -import OrderContext from '#context/OrderContext' -import CommerceLayerContext from '#context/CommerceLayerContext' -import { setCustomerOrderParam } from '#utils/localStorage' -import type { TCustomerAddress } from '#reducers/CustomerReducer' + setAddressErrors, + setCloneAddress, +} from "#reducers/AddressReducer" +import type { TCustomerAddress } from "#reducers/CustomerReducer" +import type { BaseError } from "#typings/errors" +import { setCustomerOrderParam } from "#utils/localStorage" interface Props { children: ReactNode @@ -60,34 +66,39 @@ export function AddressesContainer(props: Props): JSX.Element { children, shipToDifferentAddress = false, isBusiness, - invertAddresses = false + invertAddresses = false, } = props const [state, dispatch] = useReducer(addressReducer, addressInitialState) const { order, orderId, updateOrder } = useContext(OrderContext) const config = useContext(CommerceLayerContext) useEffect(() => { - setCustomerOrderParam( - '_save_billing_address_to_customer_address_book', - 'false' - ) - setCustomerOrderParam( - '_save_shipping_address_to_customer_address_book', - 'false' - ) - }, []) + if (order?.status === "draft") { + // Set the customer order parameters to false when the order is in draft status + setCustomerOrderParam( + "_save_billing_address_to_customer_address_book", + "false", + ) + setCustomerOrderParam( + "_save_shipping_address_to_customer_address_book", + "false", + ) + } + }, [order?.status]) useEffect(() => { dispatch({ - type: 'setShipToDifferentAddress', + type: "setShipToDifferentAddress", payload: { - shipToDifferentAddress, + shipToDifferentAddress: shipToDifferentAddress ?? false, isBusiness, - invertAddresses - } + invertAddresses, + }, }) return () => { dispatch({ - type: 'cleanup', - payload: {} + type: "cleanup", + payload: { + shipToDifferentAddress: false, + }, }) } }, [shipToDifferentAddress, isBusiness, invertAddresses]) @@ -98,7 +109,7 @@ export function AddressesContainer(props: Props): JSX.Element { errors, resource, dispatch, - currentErrors: state.errors + currentErrors: state.errors, }) }, setAddress: (params: SetAddressParams) => { @@ -115,11 +126,11 @@ export function AddressesContainer(props: Props): JSX.Element { order, orderId, state, - ...params + ...params, }), setCloneAddress: (id: string, resource: AddressResource): void => { setCloneAddress(id, resource, dispatch) - } + }, } return ( diff --git a/packages/react-components/src/components/addresses/BillingAddressForm.tsx b/packages/react-components/src/components/addresses/BillingAddressForm.tsx index 43071529..a9ab9237 100644 --- a/packages/react-components/src/components/addresses/BillingAddressForm.tsx +++ b/packages/react-components/src/components/addresses/BillingAddressForm.tsx @@ -1,14 +1,14 @@ -import AddressesContext from '#context/AddressContext' -import { useRapidForm } from 'rapid-form' -import { type ReactNode, useContext, useEffect, useRef, type JSX } from 'react'; +import { useRapidForm } from "rapid-form" +import { type JSX, type ReactNode, useContext, useEffect, useRef } from "react" +import AddressesContext from "#context/AddressContext" import BillingAddressFormContext, { type AddressValuesKeys, - type DefaultContextAddress -} from '#context/BillingAddressFormContext' -import type { BaseError, CodeErrorType } from '#typings/errors' -import OrderContext from '#context/OrderContext' -import { getSaveBillingAddressToAddressBook } from '#utils/localStorage' -import type { CustomFieldMessageError } from '#reducers/AddressReducer' + type DefaultContextAddress, +} from "#context/BillingAddressFormContext" +import OrderContext from "#context/OrderContext" +import type { CustomFieldMessageError } from "#reducers/AddressReducer" +import type { BaseError, CodeErrorType } from "#typings/errors" +import { getSaveBillingAddressToAddressBook } from "#utils/localStorage" type Props = { children: ReactNode @@ -20,12 +20,12 @@ type Props = { * Define children input and select classnames assigned in case of validation error. */ errorClassName?: string - fieldEvent?: 'blur' | 'change' + fieldEvent?: "blur" | "change" /** * Callback to customize the error message for a specific field. Called for each error in the form. */ customFieldMessageError?: CustomFieldMessageError -} & Omit +} & Omit /** * Form container for creating or editing an order related billing address or a customer address, depending on the context in use. @@ -45,10 +45,10 @@ export function BillingAddressForm(props: Props): JSX.Element { const { children, errorClassName, - autoComplete = 'on', + autoComplete = "on", reset = false, customFieldMessageError, - fieldEvent = 'change', + fieldEvent = "change", ...p } = props const { @@ -57,7 +57,7 @@ export function BillingAddressForm(props: Props): JSX.Element { errors, reset: resetForm, setValue: setValueForm, - setError: setErrorForm + setError: setErrorForm, } = useRapidForm({ fieldEvent }) const { setAddressErrors, setAddress, isBusiness } = useContext(AddressesContext) @@ -66,22 +66,22 @@ export function BillingAddressForm(props: Props): JSX.Element { order, include, addResourceToInclude, - includeLoaded + includeLoaded, } = useContext(OrderContext) const ref = useRef(null) useEffect(() => { - if (!include?.includes('billing_address')) { + if (!include?.includes("billing_address")) { addResourceToInclude({ - newResource: 'billing_address' + newResource: "billing_address", }) } else if (!includeLoaded?.billing_address) { addResourceToInclude({ - newResourceLoaded: { billing_address: true } + newResourceLoaded: { billing_address: true }, }) } if (customFieldMessageError != null && Object.keys(values).length > 0) { for (const name in values) { - if (Object.prototype.hasOwnProperty.call(values, name)) { + if (Object.hasOwn(values, name)) { const field = values[name] const fieldName = field.name const value = field.value @@ -91,14 +91,14 @@ export function BillingAddressForm(props: Props): JSX.Element { fieldName != null && value != null ) { - values[fieldName.replace('shipping_address_', '')] = value + values[fieldName.replace("shipping_address_", "")] = value const customMessage = customFieldMessageError({ field: fieldName, value, - values + values, }) if (customMessage != null) { - if (typeof customMessage === 'string') { + if (typeof customMessage === "string") { if (inError) { const errorMsg = errors[fieldName]?.message if (errorMsg != null && errorMsg !== customMessage) { @@ -108,8 +108,8 @@ export function BillingAddressForm(props: Props): JSX.Element { } else { setErrorForm({ name: fieldName, - code: 'VALIDATION_ERROR', - message: customMessage + code: "VALIDATION_ERROR", + message: customMessage, }) } } else { @@ -123,19 +123,19 @@ export function BillingAddressForm(props: Props): JSX.Element { if (errorMsg != null && errorMsg !== message) { // @ts-expect-error no type errors[field].message = message - setValueForm(field, value ?? '') + setValueForm(field, value ?? "") } } else { setErrorForm({ name: field, - code: 'VALIDATION_ERROR', - message + code: "VALIDATION_ERROR", + message, }) } } else { if (fieldInError) { delete errors[field] - setValueForm(field, value ?? '') + setValueForm(field, value ?? "") } } }) @@ -152,30 +152,29 @@ export function BillingAddressForm(props: Props): JSX.Element { const message = errors[fieldName]?.message formErrors.push({ code: code as CodeErrorType, - message: message ?? '', - resource: 'billing_address', - field: fieldName + message: message ?? "", + resource: "billing_address", + field: fieldName, }) } - setAddressErrors(formErrors, 'billing_address') + setAddressErrors(formErrors, "billing_address") } else if (values && Object.keys(values).length > 0) { - setAddressErrors([], 'billing_address') + setAddressErrors([], "billing_address") for (const name in values) { const field = values[name] if ( field?.value || - (field?.required === false && field?.type !== 'checkbox') + (field?.required === false && field?.type !== "checkbox") ) { - values[name.replace('billing_address_', '')] = field.value - + values[name.replace("billing_address_", "")] = field.value + delete values[name] } - if (field?.type === 'checkbox') { - + if (field?.type === "checkbox") { delete values[name] saveAddressToCustomerAddressBook({ - type: 'billing_address', - value: field.checked + type: "billing_address", + value: field.checked, }) } } @@ -183,16 +182,25 @@ export function BillingAddressForm(props: Props): JSX.Element { // @ts-expect-error no type values: { ...values, - ...(isBusiness && { business: isBusiness }) + ...(isBusiness && { business: isBusiness }), }, - resource: 'billing_address' + resource: "billing_address", }) } const checkboxChecked = ref.current?.querySelector( - '[name="billing_address_save_to_customer_book"]' + '[name="billing_address_save_to_customer_book"]', // @ts-expect-error no type no types )?.checked || getSaveBillingAddressToAddressBook() + if (checkboxChecked) { + ref.current + ?.querySelector('[name="billing_address_save_to_customer_book"]') + ?.setAttribute("checked", "true") + saveAddressToCustomerAddressBook({ + type: "billing_address", + value: true, + }) + } if ( reset && ((values != null && Object.keys(values).length > 0) || @@ -201,40 +209,40 @@ export function BillingAddressForm(props: Props): JSX.Element { ) { if (saveAddressToCustomerAddressBook) { saveAddressToCustomerAddressBook({ - type: 'billing_address', - value: false + type: "billing_address", + value: false, }) } if (ref) { ref.current?.reset() // @ts-expect-error no type resetForm({ target: ref.current }) - setAddressErrors([], 'billing_address') + setAddressErrors([], "billing_address") // @ts-expect-error no type - setAddress({ values: {}, resource: 'billing_address' }) + setAddress({ values: {}, resource: "billing_address" }) } } }, [errors, values, reset, include, includeLoaded, isBusiness]) const setValue = ( name: AddressValuesKeys, - value: string | number | readonly string[] + value: string | number | readonly string[], ): void => { setValueForm(name, value as string) const field: any = { - [name.replace('billing_address_', '')]: value + [name.replace("billing_address_", "")]: value, } setAddress({ values: { ...values, ...field, - ...(isBusiness && { business: isBusiness }) + ...(isBusiness && { business: isBusiness }), }, - resource: 'billing_address' + resource: "billing_address", }) } const providerValues = { isBusiness, - values: values as DefaultContextAddress['values'], + values: values as DefaultContextAddress["values"], validation, setValue, errorClassName, @@ -243,7 +251,7 @@ export function BillingAddressForm(props: Props): JSX.Element { resetField: (name: string) => { // @ts-expect-error no type resetForm({ currentTarget: ref.current }, name) - } + }, } return ( diff --git a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx index 8bc672ec..4a5fcbe3 100644 --- a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx +++ b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx @@ -1,28 +1,28 @@ -import { type ReactNode, useContext, useState, type JSX } from 'react'; -import Parent from '#components/utils/Parent' -import type { ChildrenFunction } from '#typings/index' -import AddressContext from '#context/AddressContext' +import type { Order } from "@commercelayer/sdk" +import isFunction from "lodash/isFunction" +import { type JSX, type ReactNode, useContext, useState } from "react" +import Parent from "#components/utils/Parent" +import AddressContext from "#context/AddressContext" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" +import type { TCustomerAddress } from "#reducers/CustomerReducer" +import type { ChildrenFunction } from "#typings/index" import { + addressesController, countryLockController, - addressesController -} from '#utils/addressesManager' -import OrderContext from '#context/OrderContext' -import CustomerContext from '#context/CustomerContext' -import isFunction from 'lodash/isFunction' -import type { TCustomerAddress } from '#reducers/CustomerReducer' -import type { Order } from '@commercelayer/sdk' -import { validateValue } from '#utils/validateFormFields' -import { formCleaner } from '#utils/formCleaner' +} from "#utils/addressesManager" +import { formCleaner } from "#utils/formCleaner" +import { validateValue } from "#utils/validateFormFields" interface TOnClick { success: boolean order?: Order } -interface ChildrenProps extends Omit {} +interface ChildrenProps extends Omit {} interface Props - extends Omit { + extends Omit { children?: ChildrenFunction label?: string | ReactNode onClick?: (params: TOnClick) => void @@ -33,7 +33,7 @@ interface Props export function SaveAddressesButton(props: Props): JSX.Element { const { children, - label = 'Continue to delivery', + label = "Continue to delivery", resource, disabled = false, addressId, @@ -49,38 +49,36 @@ export function SaveAddressesButton(props: Props): JSX.Element { saveAddresses, billingAddressId, shippingAddressId, - invertAddresses + invertAddresses, } = useContext(AddressContext) - const { order } = useContext(OrderContext) + const { order, setOrderErrors } = useContext(OrderContext) const { customerEmail: email, addresses, isGuest, - createCustomerAddress + createCustomerAddress, } = useContext(CustomerContext) const [forceDisable, setForceDisable] = useState(disabled) let customerEmail = !!( - !!(isGuest === true || typeof isGuest === 'undefined') && + !!(isGuest === true || typeof isGuest === "undefined") && !order?.customer_email ) - if (email != null && email !== '') { + if (email != null && email !== "") { const isValidEmail = validateValue( email, - 'customer_email', - 'email', - 'orders' + "customer_email", + "email", + "orders", ) customerEmail = Object.keys(isValidEmail).length > 0 } const shippingAddressCleaned: any = Object.keys(shippingAddress ?? {}).reduce( (acc, key) => { - return { - ...acc, - // @ts-expect-error type mismatch - [key.replace(`shipping_address_`, '')]: shippingAddress[key].value - } + // @ts-expect-error type mismatch + acc[key.replace("shipping_address_", "")] = shippingAddress[key].value + return acc }, - {} + {}, ) const { billingDisable, shippingDisable } = addressesController({ invertAddresses, @@ -91,7 +89,7 @@ export function SaveAddressesButton(props: Props): JSX.Element { shippingAddressId, billingAddressId, errors, - requiredMetadataFields + requiredMetadataFields, }) const countryLockDisable = countryLockController({ countryCodeLock: order?.shipping_country_code_lock, @@ -101,7 +99,7 @@ export function SaveAddressesButton(props: Props): JSX.Element { billing_address: billingAddress, shipping_address: shippingAddress, shippingAddressId, - lineItems: order?.line_items + lineItems: order?.line_items, }) // NOTE: This is a temporary fix to avoid the button to be disabled when the user is editing an address const invertAddressesDisable = @@ -115,11 +113,12 @@ export function SaveAddressesButton(props: Props): JSX.Element { const handleClick = async (): Promise => { if (errors && Object.keys(errors).length === 0 && !disable) { + setOrderErrors?.([]) let response: { success: boolean order?: Order } = { - success: false + success: false, } setForceDisable(true) // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check @@ -132,16 +131,16 @@ export function SaveAddressesButton(props: Props): JSX.Element { customerEmail: email, customerAddress: { resource: invertAddresses - ? 'shipping_address' - : 'billing_address', - id: addressId - } + ? "shipping_address" + : "billing_address", + id: addressId, + }, }) break } case order != null && saveAddresses != null: { response = await saveAddresses({ - customerEmail: email + customerEmail: email, }) break } @@ -152,7 +151,7 @@ export function SaveAddressesButton(props: Props): JSX.Element { if (addressId) address.id = addressId createCustomerAddress(address as TCustomerAddress) response = { - success: true + success: true, } break } @@ -167,13 +166,13 @@ export function SaveAddressesButton(props: Props): JSX.Element { label, resource, handleClick, - disabled: disable + disabled: disable, } return children ? ( {children} ) : ( ) diff --git a/packages/react-components/src/components/orders/GiftCardAmount.tsx b/packages/react-components/src/components/orders/GiftCardAmount.tsx index feee2342..8e472af0 100644 --- a/packages/react-components/src/components/orders/GiftCardAmount.tsx +++ b/packages/react-components/src/components/orders/GiftCardAmount.tsx @@ -1,14 +1,13 @@ -import BaseOrderPrice from "../utils/BaseOrderPrice" -import type { BaseAmountComponent } from "#typings" - -import { useContext, type JSX } from "react" +import { type JSX, useContext } from "react" +import Parent from "#components/utils/Parent" import OrderContext from "#context/OrderContext" +import type { BaseAmountComponent } from "#typings" import { manageGiftCard } from "#utils/adyen/manageGiftCard" -import Parent from "#components/utils/Parent" +import BaseOrderPrice from "../utils/BaseOrderPrice" export function GiftCardAmount(props: BaseAmountComponent): JSX.Element | null { - const { manageAdyenGiftCard, order } = useContext(OrderContext) - if (manageAdyenGiftCard) { + const { managePaymentProviderGiftCards, order } = useContext(OrderContext) + if (managePaymentProviderGiftCards) { const giftCardData = manageGiftCard({ order }) if (!giftCardData) return null const parentProps = { diff --git a/packages/react-components/src/components/orders/OrderContainer.tsx b/packages/react-components/src/components/orders/OrderContainer.tsx index d9d5fee5..da3d4a5c 100644 --- a/packages/react-components/src/components/orders/OrderContainer.tsx +++ b/packages/react-components/src/components/orders/OrderContainer.tsx @@ -1,35 +1,36 @@ +import type { Order, OrderCreate } from "@commercelayer/sdk" import { - useEffect, - useReducer, + type JSX, useContext, + useEffect, useMemo, + useReducer, useState, - type JSX, } from "react" +import CommerceLayerContext from "#context/CommerceLayerContext" +import OrderContext, { defaultOrderContext } from "#context/OrderContext" +import OrderStorageContext from "#context/OrderStorageContext" import orderReducer, { + type AddResourceToInclude, + addToCart, createOrder, getApiOrder, - setOrderErrors, - setOrder, + getOrderByFields, type OrderCodeType, - type AddResourceToInclude, orderInitialState, - type UpdateOrderArgs, + paymentSourceRequest, + type ResourceIncluded, type SaveAddressToCustomerAddressBook, + setOrder, + setOrderErrors, + type UpdateOrderArgs, updateOrder, - type ResourceIncluded, - addToCart, - paymentSourceRequest, } from "#reducers/OrderReducer" -import CommerceLayerContext from "#context/CommerceLayerContext" -import OrderContext, { defaultOrderContext } from "#context/OrderContext" import type { BaseMetadataObject } from "#typings" -import OrderStorageContext from "#context/OrderStorageContext" -import type { OrderCreate, Order } from "@commercelayer/sdk" import type { BaseError } from "#typings/errors" +import type { DefaultChildrenType } from "#typings/globals" import compareObjAttribute from "#utils/compareObjAttribute" import useCustomContext from "#utils/hooks/useCustomContext" -import type { DefaultChildrenType } from "#typings/globals" interface Props { children: DefaultChildrenType @@ -49,10 +50,6 @@ interface Props { * Callback called when the order is updated */ fetchOrder?: (order: Order) => void - /** - * Indicate if Adyen gift card management is enabled - */ - manageAdyenGiftCard?: boolean } /** @@ -89,14 +86,7 @@ interface Props { * */ export function OrderContainer(props: Props): JSX.Element { - const { - orderId, - children, - metadata, - attributes, - fetchOrder, - manageAdyenGiftCard, - } = props + const { orderId, children, metadata, attributes, fetchOrder } = props const [state, dispatch] = useReducer(orderReducer, orderInitialState) const [lock, setLock] = useState(false) const [lockOrder, setLockOrder] = useState(true) @@ -273,7 +263,10 @@ export function OrderContainer(props: Props): JSX.Element { } return { ...state, - manageAdyenGiftCard, + managePaymentProviderGiftCards: + // @ts-expect-error no type + state.order?.payment_source?.payment_request_data?.payment_method + ?.type === "giftcard", paymentSourceRequest: async ( params: Parameters[number], ): ReturnType => @@ -362,6 +355,7 @@ export function OrderContainer(props: Props): JSX.Element { include: state.include, state, }), + getOrderByFields, } }, [state, config.accessToken, persistKey]) return ( diff --git a/packages/react-components/src/components/orders/OrderList.tsx b/packages/react-components/src/components/orders/OrderList.tsx index 10a66c81..e3790d02 100644 --- a/packages/react-components/src/components/orders/OrderList.tsx +++ b/packages/react-components/src/components/orders/OrderList.tsx @@ -19,7 +19,7 @@ import { import { sortDescIcon, sortAscIcon } from '#utils/icons' import filterChildren from '#utils/filterChildren' import type { DefaultChildrenType, TRange } from '#typings/globals' -import type { QueryPageSize } from '@commercelayer/sdk' +import type { Order, QueryPageSize, QuerySort } from '@commercelayer/sdk' type RowComponent = 'OrderListRow' | 'OrderListEmpty' type PaginationComponent = @@ -43,35 +43,35 @@ export type TOrderListColumn = type PaginationProps = | { - /** - * Show table pagination. Default is false. - */ - showPagination: true - /** - * Number of rows per page. Default is 10. Max is 25. - */ - pageSize?: TRange<1, 26> - } + /** + * Show table pagination. Default is false. + */ + showPagination: true + /** + * Number of rows per page. Default is 10. Max is 25. + */ + pageSize?: TRange<1, 26> + } | { - /** - * Show table pagination. Default is false. - */ - showPagination?: false - pageSize?: never - } + /** + * Show table pagination. Default is false. + */ + showPagination?: false + pageSize?: never + } type SubscriptionFields = | { - /** - * Subscriptions id - Use to fetch subscriptions and shows its orders - */ - id?: string - type?: 'subscriptions' - } + /** + * Subscriptions id - Use to fetch subscriptions and shows its orders + */ + id?: string + type?: 'subscriptions' + } | { - id?: never - type?: 'orders' - } + id?: never + type?: 'orders' + } type Props = { /** @@ -150,21 +150,29 @@ export function OrderList({ }) const { orders, subscriptions, getCustomerOrders, getCustomerSubscriptions } = useContext(CustomerContext) + + // Calculate default server side sorting value compatible with Commerce Layer SDK if defined in component props + const defaultSdkSorting = sortBy.length && sortBy[0] != null ? { [sortBy[0].id]: sortBy[0].desc ? 'desc' : 'asc' } : undefined + useEffect(() => { + // Calculate server side sorting value compatible with Commerce Layer SDK following current sorting state + const sdkSorting = sorting.length && sorting[0] != null ? { [sorting[0].id]: sorting[0].desc ? 'desc' : 'asc' } : defaultSdkSorting if (type === 'orders' && getCustomerOrders != null) { getCustomerOrders({ pageNumber: pageIndex + 1, - pageSize: currentPageSize as QueryPageSize + pageSize: currentPageSize as QueryPageSize, + sortBy: sdkSorting as QuerySort }) } if (type === 'subscriptions' && getCustomerSubscriptions != null) { getCustomerSubscriptions({ pageNumber: pageIndex + 1, pageSize: currentPageSize as QueryPageSize, + sortBy: sdkSorting as QuerySort, id }) } - }, [pageIndex, currentPageSize, id != null]) + }, [pageIndex, currentPageSize, sorting, id != null]) const data = useMemo(() => { if (type === 'orders') { return orders ?? [] @@ -198,7 +206,10 @@ export function OrderList({ getSortedRowModel: getSortedRowModel(), getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), + // Server side pagination manualPagination: true, + // Server side sorting + manualSorting: true, pageCount, state: { sorting, @@ -238,9 +249,8 @@ export function OrderList({ > diff --git a/packages/react-components/src/components/orders/PlaceOrderButton.tsx b/packages/react-components/src/components/orders/PlaceOrderButton.tsx index 29d6f228..02faaf9a 100644 --- a/packages/react-components/src/components/orders/PlaceOrderButton.tsx +++ b/packages/react-components/src/components/orders/PlaceOrderButton.tsx @@ -1,22 +1,23 @@ +import type { Order } from "@commercelayer/sdk" +import isFunction from "lodash/isFunction" import { + type JSX, + type MouseEvent, type ReactNode, useContext, useEffect, useRef, useState, - type MouseEvent, - type JSX, } from "react" -import Parent from "../utils/Parent" -import type { ChildrenFunction } from "#typings/index" -import PlaceOrderContext from "#context/PlaceOrderContext" -import isFunction from "lodash/isFunction" -import PaymentMethodContext from "#context/PaymentMethodContext" import OrderContext from "#context/OrderContext" -import getCardDetails from "#utils/getCardDetails" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PlaceOrderContext from "#context/PlaceOrderContext" +import useCommerceLayer from "#hooks/useCommerceLayer" import type { BaseError } from "#typings/errors" -import type { Order } from "@commercelayer/sdk" +import type { ChildrenFunction } from "#typings/index" +import getCardDetails from "#utils/getCardDetails" import { checkPaymentIntent } from "#utils/stripe/retrievePaymentIntent" +import Parent from "../utils/Parent" interface ChildrenProps extends Omit { /** @@ -73,6 +74,7 @@ export function PlaceOrderButton(props: Props): JSX.Element { const [notPermitted, setNotPermitted] = useState(true) const [forceDisable, setForceDisable] = useState(disabled) const [isLoading, setIsLoading] = useState(false) + const { sdkClient } = useCommerceLayer() const { currentPaymentMethodRef, loading, @@ -82,13 +84,16 @@ export function PlaceOrderButton(props: Props): JSX.Element { setPaymentMethodErrors, currentCustomerPaymentSourceId, } = useContext(PaymentMethodContext) - const { order } = useContext(OrderContext) + const { order, setOrderErrors, errors } = useContext(OrderContext) const isFree = order?.total_amount_with_taxes_cents === 0 // biome-ignore lint/correctness/useExhaustiveDependencies: Need to test useEffect(() => { if (loading) setNotPermitted(loading) else { if (paymentType === currentPaymentMethodType && paymentType) { + const paymentSourceStatus = + // @ts-expect-error no type + order?.payment_source?.payment_response?.status?.toLowerCase() const card = getCardDetails({ customerPayment: { payment_source: paymentSource, @@ -111,6 +116,12 @@ export function PlaceOrderButton(props: Props): JSX.Element { ) { setNotPermitted(false) } + if ( + !currentPaymentMethodRef?.current?.onsubmit && + paymentSourceStatus === "declined" + ) { + setNotPermitted(true) + } } else if (isFree && isPermitted) { setNotPermitted(false) } else { @@ -122,13 +133,20 @@ export function PlaceOrderButton(props: Props): JSX.Element { } }, [ isPermitted, - paymentType, - currentPaymentMethodRef?.current?.onsubmit, + paymentType != null, + !currentPaymentMethodRef?.current?.onsubmit, loading, currentPaymentMethodType, - order, - paymentSource, + order?.id, + paymentSource?.id, ]) + useEffect(() => { + if (errors && errors.length > 0) { + setNotPermitted(true) + setIsLoading(false) + setForceDisable(false) + } + }, [errors]) // biome-ignore lint/correctness/useExhaustiveDependencies: Need to test useEffect(() => { // PayPal redirect flow @@ -141,7 +159,7 @@ export function PlaceOrderButton(props: Props): JSX.Element { ) { handleClick() } - }, [options?.paypalPayerId, paymentType]) + }, [options?.paypalPayerId, paymentType != null]) // biome-ignore lint/correctness/useExhaustiveDependencies: Need to test useEffect(() => { // Stripe redirect flow @@ -195,14 +213,18 @@ export function PlaceOrderButton(props: Props): JSX.Element { ]) // biome-ignore lint/correctness/useExhaustiveDependencies: Need to test useEffect(() => { - // Adyen redirect flow if (order?.status != null && ["draft", "pending"].includes(order?.status)) { - const resultCode = + // Adyen redirect flow + const isAuthorized = // @ts-expect-error no type order?.payment_source?.payment_response?.resultCode === "Authorised" const paymentDetails = // @ts-expect-error no type order?.payment_source?.payment_request_details?.details != null + const paymentStatus = order?.payment_status + const paymentMethodType = + // @ts-expect-error no type + order?.payment_source?.payment_response?.paymentMethod?.type if ( paymentType === "adyen_payments" && options?.adyen?.redirectResult && @@ -245,19 +267,11 @@ export function PlaceOrderButton(props: Props): JSX.Element { }) } else if ( paymentType === "adyen_payments" && - options?.adyen?.MD && - options?.adyen?.PaRes && - autoPlaceOrder - ) { - handleClick() - } else if ( - paymentType === "adyen_payments" && - resultCode && - // @ts-expect-error no type - ref?.current?.disabled === false && - currentCustomerPaymentSourceId == null && + isAuthorized && + paymentDetails && autoPlaceOrder && - status === "standby" + status === "standby" && + !options?.adyen?.redirectResult ) { // NOTE: This is a workaround for the case when the user reloads the page after selecting a customer payment source if ( @@ -268,11 +282,20 @@ export function PlaceOrderButton(props: Props): JSX.Element { ) { handleClick() } + } else if ( + paymentType === "adyen_payments" && + isAuthorized && + paymentStatus === "authorized" && + paymentMethodType === "giftcard" && + autoPlaceOrder && + status === "standby" && + !options?.adyen?.redirectResult + ) { + handleClick() } } }, [ - options?.adyen, - paymentType, + options?.adyen?.redirectResult != null, // @ts-expect-error no type order?.payment_source?.payment_response?.resultCode, ]) @@ -288,8 +311,7 @@ export function PlaceOrderButton(props: Props): JSX.Element { order: order, }) } - }, [order, order?.payment_status, order?.status, paymentType, onClick]) - // biome-ignore lint/correctness/useExhaustiveDependencies: Need to test + }, [order?.id, order?.payment_status, order?.status, paymentType != null]) useEffect(() => { // Checkout.com redirect flow if ( @@ -299,23 +321,154 @@ export function PlaceOrderButton(props: Props): JSX.Element { ["draft", "pending"].includes(order?.status) && autoPlaceOrder ) { - handleClick() + // @ts-expect-error no type + const paymentResponse = order?.payment_source?.payment_response + const paymentStatus = paymentResponse?.status + if (paymentStatus && paymentStatus.toLowerCase() === "pending") { + async function placingOrder(): Promise { + const res = await setPaymentSource({ + paymentSourceId: paymentSource?.id, + paymentResource: "checkout_com_payments", + attributes: { + _details: 1, + }, + }) + // @ts-expect-error no type + const paymentStatus: string = res?.payment_response?.status + const isValidStatus = ["authorized", "captured"].includes( + paymentStatus?.toLowerCase(), + ) + if (paymentStatus && isValidStatus) { + handleClick() + } else { + if (options?.checkoutCom) { + options.checkoutCom.session_id = undefined + } + setPaymentMethodErrors([ + { + code: "PAYMENT_INTENT_AUTHENTICATION_FAILURE", + resource: "payment_methods", + field: currentPaymentMethodType, + message: paymentStatus, + }, + ]) + } + } + placingOrder() + } + } else if ( + paymentType === "checkout_com_payments" && + order?.status && + status && + ["pending"].includes(order?.status) && + ["placing"].includes(status) && + autoPlaceOrder + ) { + /** + * Place order with Checkout.com using express payments + */ + const paymentSourceStatus = + // @ts-expect-error no type + order?.payment_source?.payment_response?.status + if ( + paymentSourceStatus && + ["captured", "authorized"].includes(paymentSourceStatus.toLowerCase()) + ) { + setPlaceOrder?.({ + paymentSource, + }).then((placed) => { + if (placed?.placed) { + onClick?.(placed) + setPlaceOrderStatus?.({ status: "placing" }) + } else { + setPlaceOrderStatus?.({ status: "standby" }) + } + }) + } } - }, [options?.checkoutCom, paymentType]) - // biome-ignore lint/correctness/useExhaustiveDependencies: Need to test + }, [options?.checkoutCom?.session_id, order?.payment_source?.id, status]) useEffect(() => { if (ref?.current != null && setButtonRef != null) { setButtonRef(ref) } - }, [ref]) + }, [ref?.current]) + useEffect(() => { + switch (status) { + case "disabled": + case "placing": + setNotPermitted(true) + break + default: + setNotPermitted(false) + break + } + }, [status != null]) const handleClick = async ( e?: MouseEvent, ): Promise => { e?.preventDefault() e?.stopPropagation() - setIsLoading(true) + const sdk = sdkClient() + if (sdk == null) return + if (order == null) return let isValid = true - setForceDisable(true) + let currentPaymentStatus = "unpaid" + + const isStripePayment = paymentType === "stripe_payments" + if (!isStripePayment) { + /** + * Check if the order is already placed or in draft status to avoid placing it again + * and to prevent placing a draft order + * @see https://docs.commercelayer.io/core/how-tos/placing-orders/checkout/placing-the-order + */ + const { status, payment_status: paymentStatus } = + await sdk.orders.retrieve(order?.id, { + fields: ["status", "payment_status", "payment_source"], + include: ["payment_source"], + }) + const isAlreadyPlaced = status === "placed" + const isDraftOrder = status === "draft" + currentPaymentStatus = paymentStatus ?? "unpaid" + + if (isAlreadyPlaced) { + /** + * Order already placed + */ + setPlaceOrderStatus?.({ status: "placing" }) + onClick?.({ + placed: true, + order: order, + }) + return + } + if (isDraftOrder) { + /** + * Draft order cannot be placed + */ + setPlaceOrderStatus?.({ status: "standby" }) + onClick?.({ + placed: false, + order: order, + errors: [ + { + code: "VALIDATION_ERROR", + resource: "orders", + message: "Draft order cannot be placed", + }, + ], + }) + setOrderErrors([ + { + code: "VALIDATION_ERROR", + resource: "orders", + message: "Draft order cannot be placed", + }, + ]) + return + } + } + setIsLoading(true) + // setForceDisable(true) const checkPaymentSource = paymentType !== "stripe_payments" ? await setPaymentSource({ @@ -324,6 +477,9 @@ export function PlaceOrderButton(props: Props): JSX.Element { paymentSourceId: paymentSource?.id, }) : paymentSource + const checkPaymentSourceStatus = + // @ts-expect-error no type + checkPaymentSource?.payment_response?.status?.toLowerCase() const card = paymentType && getCardDetails({ @@ -351,11 +507,32 @@ export function PlaceOrderButton(props: Props): JSX.Element { ) { isValid = true } - } else if (card?.brand) { + } else if ( + currentPaymentMethodRef?.current?.onsubmit && + options?.checkoutCom?.session_id && + // @ts-expect-error no type + checkPaymentSource?.payment_response?.status && + // @ts-expect-error no type + checkPaymentSource?.payment_response?.status?.toLowerCase() === "declined" + ) { + /** + * Permit to place order with declined payment using Checkout.com + */ + isValid = (await currentPaymentMethodRef.current?.onsubmit({ + // @ts-expect-error no type + paymentSource: checkPaymentSource, + setPlaceOrder, + onclickCallback: onClick, + })) as boolean + } else if (card?.brand && checkPaymentSourceStatus !== "declined") { isValid = true } + if (currentPaymentStatus === "partially_authorized") { + isValid = false + } if (isValid && setPlaceOrderStatus != null) { setPlaceOrderStatus({ status: "placing" }) + setForceDisable(true) } const placed = isValid && @@ -376,8 +553,8 @@ export function PlaceOrderButton(props: Props): JSX.Element { setPlaceOrderStatus({ status: "standby" }) } } else { - setForceDisable(false) setIsLoading(false) + setPlaceOrderStatus?.({ status: "standby" }) } } const disabledButton = disabled !== undefined ? disabled : notPermitted diff --git a/packages/react-components/src/components/orders/PlaceOrderContainer.tsx b/packages/react-components/src/components/orders/PlaceOrderContainer.tsx index 4c8641d7..3f7bcce2 100644 --- a/packages/react-components/src/components/orders/PlaceOrderContainer.tsx +++ b/packages/react-components/src/components/orders/PlaceOrderContainer.tsx @@ -1,4 +1,4 @@ -import PlaceOrderContext from '#context/PlaceOrderContext' +import PlaceOrderContext from "#context/PlaceOrderContext" import { type ReactNode, type RefObject, @@ -6,18 +6,18 @@ import { useEffect, useReducer, type JSX, -} from 'react'; +} from "react" import placeOrderReducer, { placeOrderInitialState, type PlaceOrderOptions, placeOrderPermitted, setButtonRef, - setPlaceOrderStatus -} from '#reducers/PlaceOrderReducer' -import OrderContext from '#context/OrderContext' -import CommerceLayerContext from '#context/CommerceLayerContext' -import { setPlaceOrder } from '../../reducers/PlaceOrderReducer' -import useCustomContext from '#utils/hooks/useCustomContext' + setPlaceOrderStatus, +} from "#reducers/PlaceOrderReducer" +import OrderContext from "#context/OrderContext" +import CommerceLayerContext from "#context/CommerceLayerContext" +import { setPlaceOrder } from "../../reducers/PlaceOrderReducer" +import useCustomContext from "#utils/hooks/useCustomContext" interface Props { children: ReactNode @@ -27,7 +27,7 @@ export function PlaceOrderContainer(props: Props): JSX.Element { const { children, options } = props const [state, dispatch] = useReducer( placeOrderReducer, - placeOrderInitialState + placeOrderInitialState, ) const { order, @@ -35,53 +35,54 @@ export function PlaceOrderContainer(props: Props): JSX.Element { setOrderErrors, include, addResourceToInclude, - includeLoaded + includeLoaded, } = useCustomContext({ context: OrderContext, - contextComponentName: 'OrderContainer', - currentComponentName: 'PlaceOrderContainer', - key: 'order' + contextComponentName: "OrderContainer", + currentComponentName: "PlaceOrderContainer", + key: "order", }) const config = useContext(CommerceLayerContext) + // biome-ignore lint/correctness/useExhaustiveDependencies: Infinite loop useEffect(() => { - if (!include?.includes('shipments.available_shipping_methods')) { + if (!include?.includes("shipments.available_shipping_methods")) { addResourceToInclude({ newResource: [ - 'shipments.available_shipping_methods', - 'shipments.stock_line_items.line_item', - 'shipments.shipping_method', - 'shipments.stock_transfers.line_item', - 'shipments.stock_location' - ] + "shipments.available_shipping_methods", + "shipments.stock_line_items.line_item", + "shipments.shipping_method", + "shipments.stock_transfers.line_item", + "shipments.stock_location", + ], }) - } else if (!includeLoaded?.['shipments.available_shipping_methods']) { + } else if (!includeLoaded?.["shipments.available_shipping_methods"]) { addResourceToInclude({ newResourceLoaded: { - 'shipments.available_shipping_methods': true, - 'shipments.stock_line_items.line_item': true, - 'shipments.shipping_method': true, - 'shipments.stock_transfers.line_item': true, - 'shipments.stock_location': true - } + "shipments.available_shipping_methods": true, + "shipments.stock_line_items.line_item": true, + "shipments.shipping_method": true, + "shipments.stock_transfers.line_item": true, + "shipments.stock_location": true, + }, }) } - if (!include?.includes('billing_address')) { + if (!include?.includes("billing_address")) { addResourceToInclude({ - newResource: 'billing_address' + newResource: "billing_address", }) } else if (!includeLoaded?.billing_address) { addResourceToInclude({ - newResourceLoaded: { billing_address: true } + newResourceLoaded: { billing_address: true }, }) } - if (!include?.includes('shipping_address')) { + if (!include?.includes("shipping_address")) { addResourceToInclude({ - newResource: 'shipping_address', - resourcesIncluded: include + newResource: "shipping_address", + resourcesIncluded: include, }) } else if (!includeLoaded?.shipping_address) { addResourceToInclude({ - newResourceLoaded: { shipping_address: true } + newResourceLoaded: { shipping_address: true }, }) } if (order) { @@ -90,8 +91,8 @@ export function PlaceOrderContainer(props: Props): JSX.Element { dispatch, order, options: { - ...options - } + ...options, + }, }) } }, [order, include, includeLoaded]) @@ -99,12 +100,12 @@ export function PlaceOrderContainer(props: Props): JSX.Element { ...state, setPlaceOrder: async ({ paymentSource, - currentCustomerPaymentSourceId + currentCustomerPaymentSourceId, }: { - paymentSource?: Parameters['0']['paymentSource'] + paymentSource?: Parameters["0"]["paymentSource"] currentCustomerPaymentSourceId?: Parameters< typeof setPlaceOrder - >['0']['currentCustomerPaymentSourceId'] + >["0"]["currentCustomerPaymentSourceId"] }) => await setPlaceOrder({ config, @@ -114,10 +115,10 @@ export function PlaceOrderContainer(props: Props): JSX.Element { paymentSource, include, setOrder, - currentCustomerPaymentSourceId + currentCustomerPaymentSourceId, }), setPlaceOrderStatus: ({ - status + status, }: Parameters[0]) => { setPlaceOrderStatus({ status, dispatch }) }, @@ -127,13 +128,13 @@ export function PlaceOrderContainer(props: Props): JSX.Element { dispatch, order, options: { - ...options - } + ...options, + }, }) }, setButtonRef: (ref: RefObject) => { setButtonRef(ref, dispatch) - } + }, } return ( diff --git a/packages/react-components/src/components/orders/TotalAmount.tsx b/packages/react-components/src/components/orders/TotalAmount.tsx index 388dbeaa..e142cc2a 100644 --- a/packages/react-components/src/components/orders/TotalAmount.tsx +++ b/packages/react-components/src/components/orders/TotalAmount.tsx @@ -1,14 +1,13 @@ -import BaseOrderPrice from "../utils/BaseOrderPrice" -import type { BaseAmountComponent } from "#typings" - -import { useContext, type JSX } from "react" +import { type JSX, useContext } from "react" import Parent from "#components/utils/Parent" import OrderContext from "#context/OrderContext" +import type { BaseAmountComponent } from "#typings" import { manageGiftCard } from "#utils/adyen/manageGiftCard" +import BaseOrderPrice from "../utils/BaseOrderPrice" export function TotalAmount(props: BaseAmountComponent): JSX.Element | null { - const { manageAdyenGiftCard, order } = useContext(OrderContext) - if (manageAdyenGiftCard) { + const { managePaymentProviderGiftCards, order } = useContext(OrderContext) + if (managePaymentProviderGiftCards) { const giftCardData = manageGiftCard({ order }) if (!giftCardData) return diff --git a/packages/react-components/src/components/payment_gateways/AdyenGateway.tsx b/packages/react-components/src/components/payment_gateways/AdyenGateway.tsx index 8003ece9..c7153b9d 100644 --- a/packages/react-components/src/components/payment_gateways/AdyenGateway.tsx +++ b/packages/react-components/src/components/payment_gateways/AdyenGateway.tsx @@ -1,19 +1,20 @@ -import type { GatewayBaseType } from '#components/payment_gateways/PaymentGateway' -import CommerceLayerContext from '#context/CommerceLayerContext' -import CustomerContext from '#context/CustomerContext' -import OrderContext from '#context/OrderContext' -import PaymentMethodChildrenContext from '#context/PaymentMethodChildrenContext' -import PaymentMethodContext from '#context/PaymentMethodContext' -import PaymentSourceContext from '#context/PaymentSourceContext' -import type { PaymentResource } from '#reducers/PaymentMethodReducer' -import type { StripeElementLocale } from '@stripe/stripe-js' -import isEmpty from 'lodash/isEmpty' -import { useContext, type JSX } from 'react' -import AdyenPayment from '#components/payment_source/AdyenPayment' -import PaymentCardsTemplate from '../utils/PaymentCardsTemplate' -import { jwt } from '#utils/jwt' -import getCardDetails from '#utils/getCardDetails' -import { getPaymentAttributes } from '#utils/getPaymentAttributes' +import type { StripeElementLocale } from "@stripe/stripe-js" +import isEmpty from "lodash/isEmpty" +import { type JSX, useContext } from "react" +import type { GatewayBaseType } from "#components/payment_gateways/PaymentGateway" +import AdyenPayment from "#components/payment_source/AdyenPayment" +import CommerceLayerContext from "#context/CommerceLayerContext" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" +import PaymentMethodChildrenContext from "#context/PaymentMethodChildrenContext" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PaymentSourceContext from "#context/PaymentSourceContext" +import type { PaymentResource } from "#reducers/PaymentMethodReducer" +import getCardDetails from "#utils/getCardDetails" +import { getPaymentAttributes } from "#utils/getPaymentAttributes" +import { hasSubscriptions } from "#utils/hasSubscriptions" +import { jwt } from "#utils/jwt" +import PaymentCardsTemplate from "../utils/PaymentCardsTemplate" type Props = GatewayBaseType @@ -24,8 +25,6 @@ export function AdyenGateway(props: Props): JSX.Element | null { handleEditClick, children, templateCustomerCards, - loading, - loaderComponent, templateCustomerSaveToWallet, ...p } = props @@ -35,23 +34,23 @@ export function AdyenGateway(props: Props): JSX.Element | null { const { payments, isGuest } = useContext(CustomerContext) const { currentPaymentMethodId, config, paymentSource } = useContext(PaymentMethodContext) - const paymentResource: PaymentResource = 'adyen_payments' + const paymentResource: PaymentResource = "adyen_payments" const locale = order?.language_code as StripeElementLocale if (!readonly && payment?.id !== currentPaymentMethodId) return null // @ts-expect-error no type const clientKey = paymentSource?.public_key - const environment = accessToken && jwt(accessToken).test ? 'test' : 'live' + const environment = accessToken && jwt(accessToken).test ? "test" : "live" const adyenConfig = getPaymentAttributes({ resource: paymentResource, config: config ?? {}, - keys: ['adyen_payments'] + keys: ["adyen_payments"], }) const paymentConfig = adyenConfig?.adyenPayment - const customerPayments = + let customerPayments = !isEmpty(payments) && payments ? payments.filter((customerPayment) => { return ( - customerPayment.payment_source?.type === 'adyen_payments' || + customerPayment.payment_source?.type === "adyen_payments" || customerPayment.payment_method != null ) }) @@ -59,9 +58,9 @@ export function AdyenGateway(props: Props): JSX.Element | null { if (readonly || showCard) { const card = getCardDetails({ customerPayment: { - payment_source: paymentSource + payment_source: paymentSource, }, - paymentType: paymentResource + paymentType: paymentResource, }) const value = { ...card, showCard, handleEditClick, readonly } return isEmpty(card) ? null : ( @@ -70,11 +69,18 @@ export function AdyenGateway(props: Props): JSX.Element | null { ) } - const hasStoredPaymentMethods = + let hasStoredPaymentMethods = // @ts-expect-error missing type paymentSource?.payment_methods?.storedPaymentMethods != null && // @ts-expect-error missing type paymentSource?.payment_methods?.storedPaymentMethods.length > 0 + if (order && hasSubscriptions(order)) { + /** + * When the order has subscriptions, we do not show stored payment methods + */ + hasStoredPaymentMethods = false + customerPayments = [] + } if (!isGuest && templateCustomerCards) { return ( <> diff --git a/packages/react-components/src/components/payment_gateways/CheckoutComGateway.tsx b/packages/react-components/src/components/payment_gateways/CheckoutComGateway.tsx index c471871d..fe4fae51 100644 --- a/packages/react-components/src/components/payment_gateways/CheckoutComGateway.tsx +++ b/packages/react-components/src/components/payment_gateways/CheckoutComGateway.tsx @@ -1,19 +1,19 @@ -import CheckoutComPayment from '#components/payment_source/CheckoutComPayment' -import type { GatewayBaseType } from '#components/payment_gateways/PaymentGateway' -import CustomerContext from '#context/CustomerContext' -import OrderContext from '#context/OrderContext' -import PaymentMethodChildrenContext from '#context/PaymentMethodChildrenContext' -import PaymentMethodContext from '#context/PaymentMethodContext' -import PaymentSourceContext from '#context/PaymentSourceContext' +import type { StripeElementLocale } from "@stripe/stripe-js" +import isEmpty from "lodash/isEmpty" +import React, { type JSX } from "react" +import type { GatewayBaseType } from "#components/payment_gateways/PaymentGateway" +import CheckoutComPayment from "#components/payment_source/CheckoutComPayment" +import PaymentCardsTemplate from "#components/utils/PaymentCardsTemplate" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" +import PaymentMethodChildrenContext from "#context/PaymentMethodChildrenContext" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PaymentSourceContext from "#context/PaymentSourceContext" import { getPaymentConfig, - type PaymentResource -} from '#reducers/PaymentMethodReducer' -import type { StripeElementLocale } from '@stripe/stripe-js' -import isEmpty from 'lodash/isEmpty' -import React, { type JSX } from 'react'; -import PaymentCardsTemplate from '#components/utils/PaymentCardsTemplate' -import getCardDetails from '#utils/getCardDetails' + type PaymentResource, +} from "#reducers/PaymentMethodReducer" +import getCardDetails from "#utils/getCardDetails" type Props = GatewayBaseType @@ -25,8 +25,6 @@ export function CheckoutComGateway(props: Props): JSX.Element | null { children, templateCustomerCards, show, - loading, - loaderComponent, templateCustomerSaveToWallet, ...p } = props @@ -35,14 +33,14 @@ export function CheckoutComGateway(props: Props): JSX.Element | null { const { payments, isGuest } = React.useContext(CustomerContext) const { currentPaymentMethodId, config, paymentSource } = React.useContext(PaymentMethodContext) - const paymentResource: PaymentResource = 'checkout_com_payments' + const paymentResource: PaymentResource = "checkout_com_payments" const locale = order?.language_code as StripeElementLocale if (!readonly && payment?.id !== currentPaymentMethodId) return null // @ts-expect-error no type const publicKey = paymentSource?.public_key const paymentConfig = config - ? getPaymentConfig<'checkout_com_payments'>(paymentResource, config) + ? getPaymentConfig<"checkout_com_payments">(paymentResource, config) : {} const customerPayments = !isEmpty(payments) && payments @@ -53,9 +51,9 @@ export function CheckoutComGateway(props: Props): JSX.Element | null { if (readonly || showCard) { const card = getCardDetails({ customerPayment: { - payment_source: paymentSource + payment_source: paymentSource, }, - paymentType: paymentResource + paymentType: paymentResource, }) const value = { ...card, showCard, handleEditClick, readonly } return !card.brand ? null : ( @@ -79,7 +77,7 @@ export function CheckoutComGateway(props: Props): JSX.Element | null { templateCustomerSaveToWallet={templateCustomerSaveToWallet} publicKey={publicKey} locale={locale} - {...paymentConfig} + {...paymentConfig.checkoutComPayment} /> ) diff --git a/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx b/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx index 33e4458e..4cfad664 100644 --- a/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx +++ b/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx @@ -1,26 +1,27 @@ -import OrderContext from '#context/OrderContext' -import PaymentMethodChildrenContext from '#context/PaymentMethodChildrenContext' -import PaymentMethodContext from '#context/PaymentMethodContext' -import type { PaymentResource } from '#reducers/PaymentMethodReducer' -import type { LoaderType } from '#typings' -import { useContext, useEffect, useState, type JSX } from 'react'; -import type { PaymentSourceProps } from '../payment_source/PaymentSource' -import getLoaderComponent from '#utils/getLoaderComponent' -import AdyenGateway from './AdyenGateway' -import StripeGateway from './StripeGateway' -import BraintreeGateway from './BraintreeGateway' -import PaypalGateway from './PaypalGateway' -import WireTransferGateway from './WireTransferGateway' -import CustomerContext from '#context/CustomerContext' -import CheckoutComGateway from './CheckoutComGateway' -import KlarnaGateway from './KlarnaGateway' +import { type JSX, useContext, useEffect, useState } from "react" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" +import PaymentMethodChildrenContext from "#context/PaymentMethodChildrenContext" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PlaceOrderContext from "#context/PlaceOrderContext" +import type { PaymentResource } from "#reducers/PaymentMethodReducer" +import type { LoaderType } from "#typings" +import getLoaderComponent from "#utils/getLoaderComponent" import { + getCkoAttributes, getExternalPaymentAttributes, getPaypalAttributes, - getStripeAttributes -} from '#utils/getPaymentAttributes' -import ExternalGateway from './ExternalGateway' -import PlaceOrderContext from '#context/PlaceOrderContext' + getStripeAttributes, +} from "#utils/getPaymentAttributes" +import type { PaymentSourceProps } from "../payment_source/PaymentSource" +import AdyenGateway from "./AdyenGateway" +import BraintreeGateway from "./BraintreeGateway" +import CheckoutComGateway from "./CheckoutComGateway" +import ExternalGateway from "./ExternalGateway" +import KlarnaGateway from "./KlarnaGateway" +import PaypalGateway from "./PaypalGateway" +import StripeGateway from "./StripeGateway" +import WireTransferGateway from "./WireTransferGateway" export type GatewayBaseType = Props & { show: boolean @@ -44,7 +45,7 @@ export function PaymentGateway({ templateCustomerSaveToWallet, onClickCustomerCards, show, - loader = 'Loading...', + loader = "Loading...", ...p }: Props): JSX.Element | null { const loaderComponent = getLoaderComponent(loader) @@ -58,7 +59,7 @@ export function PaymentGateway({ config, currentPaymentMethodType, setPaymentSource, - paymentSource + paymentSource, } = useContext(PaymentMethodContext) const paymentResource = readonly ? currentPaymentMethodType @@ -71,24 +72,27 @@ export function PaymentGateway({ !expressPayments ) { let attributes: Record | undefined = {} - if (config != null && paymentResource === 'paypal_payments') { + if (config != null && paymentResource === "paypal_payments") { attributes = getPaypalAttributes(paymentResource, config) } - if (config != null && paymentResource === 'external_payments') { + if (config != null && paymentResource === "external_payments") { attributes = getExternalPaymentAttributes(paymentResource, config) } - if (config != null && paymentResource === 'stripe_payments') { + if (config != null && paymentResource === "stripe_payments") { attributes = getStripeAttributes(paymentResource, config) - if (attributes != null && attributes['return_url'] == null) { - attributes['return_url'] = window.location.href + if (attributes != null && attributes["return_url"] == null) { + attributes["return_url"] = window.location.href } } + if (config != null && paymentResource === "checkout_com_payments") { + attributes = getCkoAttributes(paymentResource, config) + } const setPaymentSources = async (): Promise => { if (order != null) { await setPaymentSource({ paymentResource, order, - attributes + attributes, }) } if (getCustomerPaymentSources) getCustomerPaymentSources() @@ -118,7 +122,7 @@ export function PaymentGateway({ if (expressPayments && show) setLoading(false) if ( order?.status != null && - !['draft', 'pending'].includes(order?.status) && + !["draft", "pending"].includes(order?.status) && show && order?.payment_source?.id != null ) { @@ -130,12 +134,15 @@ export function PaymentGateway({ }, [order?.payment_method?.id, show, paymentSource]) useEffect(() => { - if (status === 'placing') setLoading(true) - if (status === 'standby' && loading) setLoading(false) + if (status === "placing") setLoading(true) + if (status === "standby" && loading) setLoading(false) + if (order && order.status === "placed" && loading) { + setLoading(false) + } return () => { setLoading(true) } - }, [status]) + }, [status, order?.status]) const gatewayConfig = { readonly, @@ -148,30 +155,30 @@ export function PaymentGateway({ onClickCustomerCards, loaderComponent, templateCustomerSaveToWallet, - ...p + ...p, } if (currentPaymentMethodType !== paymentResource) return null if (loading) return loaderComponent switch (paymentResource) { - case 'adyen_payments': + case "adyen_payments": return {children} - case 'braintree_payments': + case "braintree_payments": return {children} - case 'checkout_com_payments': + case "checkout_com_payments": return ( {children} ) - case 'external_payments': + case "external_payments": return {children} - case 'klarna_payments': + case "klarna_payments": return {children} - case 'stripe_payments': + case "stripe_payments": return {children} - case 'wire_transfers': + case "wire_transfers": return ( {children} ) - case 'paypal_payments': + case "paypal_payments": return {children} default: return null diff --git a/packages/react-components/src/components/payment_gateways/StripeGateway.tsx b/packages/react-components/src/components/payment_gateways/StripeGateway.tsx index 6ee87e5c..f3b9b69f 100644 --- a/packages/react-components/src/components/payment_gateways/StripeGateway.tsx +++ b/packages/react-components/src/components/payment_gateways/StripeGateway.tsx @@ -1,19 +1,19 @@ -import type { GatewayBaseType } from '#components/payment_gateways/PaymentGateway' -import StripePayment from '#components/payment_source/StripePayment' -import CustomerContext from '#context/CustomerContext' -import OrderContext from '#context/OrderContext' -import PaymentMethodChildrenContext from '#context/PaymentMethodChildrenContext' -import PaymentMethodContext from '#context/PaymentMethodContext' -import PaymentSourceContext from '#context/PaymentSourceContext' +import type { StripeElementLocale } from "@stripe/stripe-js" +import isEmpty from "lodash/isEmpty" +import { type JSX, useContext } from "react" +import type { GatewayBaseType } from "#components/payment_gateways/PaymentGateway" +import StripePayment from "#components/payment_source/StripePayment" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" +import PaymentMethodChildrenContext from "#context/PaymentMethodChildrenContext" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PaymentSourceContext from "#context/PaymentSourceContext" import { getPaymentConfig, - type PaymentResource -} from '#reducers/PaymentMethodReducer' -import getCardDetails from '#utils/getCardDetails' -import type { StripeElementLocale } from '@stripe/stripe-js' -import isEmpty from 'lodash/isEmpty' -import { useContext, type JSX } from 'react'; -import PaymentCardsTemplate from '../utils/PaymentCardsTemplate' + type PaymentResource, +} from "#reducers/PaymentMethodReducer" +import getCardDetails from "#utils/getCardDetails" +import PaymentCardsTemplate from "../utils/PaymentCardsTemplate" type Props = GatewayBaseType @@ -25,8 +25,6 @@ export function StripeGateway(props: Props): JSX.Element | null { children, templateCustomerCards, show, - loading, - loaderComponent, templateCustomerSaveToWallet, ...p } = props @@ -35,16 +33,18 @@ export function StripeGateway(props: Props): JSX.Element | null { const { payments, isGuest } = useContext(CustomerContext) const { currentPaymentMethodId, config, paymentSource } = useContext(PaymentMethodContext) - const paymentResource: PaymentResource = 'stripe_payments' + const paymentResource: PaymentResource = "stripe_payments" const locale = order?.language_code as StripeElementLocale if (!readonly && payment?.id !== currentPaymentMethodId) return null // @ts-expect-error no type const publishableKey = paymentSource?.publishable_key // @ts-expect-error no type + const connectedAccount = paymentSource?.connected_account + // @ts-expect-error no type const clientSecret = paymentSource?.client_secret const stripeConfig = config - ? getPaymentConfig<'stripe_payments'>(paymentResource, config).stripePayment + ? getPaymentConfig<"stripe_payments">(paymentResource, config).stripePayment : {} const customerPayments = !isEmpty(payments) && payments @@ -55,21 +55,21 @@ export function StripeGateway(props: Props): JSX.Element | null { if (readonly || showCard) { const card = getCardDetails({ customerPayment: { - payment_source: paymentSource + payment_source: paymentSource, }, - paymentType: paymentResource + paymentType: paymentResource, }) - if (card?.brand === '') { + if (card?.brand === "") { card.brand = // @ts-expect-error missing type - paymentSource?.payment_instrument?.issuer_type ?? 'credit-card' + paymentSource?.payment_instrument?.issuer_type ?? "credit-card" } const value = { ...card, showCard, handleEditClick, readonly, - paymentSource + paymentSource, } return card?.brand == null ? null : ( @@ -91,6 +91,7 @@ export function StripeGateway(props: Props): JSX.Element | null { show={show} templateCustomerSaveToWallet={templateCustomerSaveToWallet} publishableKey={publishableKey} + connectedAccount={connectedAccount} clientSecret={clientSecret} expressPayments={expressPayments} locale={locale} @@ -103,6 +104,7 @@ export function StripeGateway(props: Props): JSX.Element | null { order?: Order - paymentSource?: Order['payment_source'] + paymentSource?: Order["payment_source"] } type Props = { @@ -47,6 +48,11 @@ type Props = { * Customize the loader component */ loader?: LoaderType + /** + * Show loader while fetching payment methods + * @default undefined + */ + showLoader?: boolean /** * Auto select the payment method when there is only one available */ @@ -58,8 +64,8 @@ type Props = { /** * Sort payment methods by an array of strings */ - sortBy?: Array -} & Omit & + sortBy?: Array +} & Omit & ( | { clickableContainer: true @@ -77,17 +83,18 @@ export function PaymentMethod({ children, className, activeClass, - loader = 'Loading...', + loader = "Loading...", clickableContainer, autoSelectSinglePaymentMethod, expressPayments, + showLoader, hide, onClick, sortBy, ...p }: Props): JSX.Element { const [loading, setLoading] = useState(true) - const [paymentSelected, setPaymentSelected] = useState('') + const [paymentSelected, setPaymentSelected] = useState("") const [paymentSourceCreated, setPaymentSourceCreated] = useState(false) const { paymentMethods, @@ -96,12 +103,13 @@ export function PaymentMethod({ setLoading: setLoadingPlaceOrder, paymentSource, setPaymentSource, - config + config, + errors, } = useCustomContext({ context: PaymentMethodContext, - contextComponentName: 'PaymentMethodsContainer', - currentComponentName: 'PaymentMethod', - key: 'paymentMethods' + contextComponentName: "PaymentMethodsContainer", + currentComponentName: "PaymentMethod", + key: "paymentMethods", }) const { order } = useContext(OrderContext) const { getCustomerPaymentSources } = useContext(CustomerContext) @@ -119,12 +127,16 @@ export function PaymentMethod({ await setPaymentMethod({ paymentResource, paymentMethodId }) const ps = await setPaymentSource({ paymentResource, - order + order, }) if (ps && paymentMethod && onClick != null) { onClick({ payment: paymentMethod, order, paymentSource: ps }) setTimeout(() => { - setLoading(false) + if (showLoader && errors?.length === 0) { + setLoading(showLoader) + } else { + setLoading(false) + } }, 200) } setLoadingPlaceOrder({ loading: false }) @@ -132,7 +144,7 @@ export function PaymentMethod({ selectExpressPayment() } } - }, [!isEmpty(paymentMethods), expressPayments]) + }, [!isEmpty(paymentMethods), expressPayments, errors?.length]) useEffect(() => { if ( paymentMethods != null && @@ -144,6 +156,10 @@ export function PaymentMethod({ if (autoSelectSinglePaymentMethod != null && !expressPayments) { const autoSelect = async (): Promise => { const isSingle = paymentMethods.length === 1 + const paymentSourceStatus = paymentSource + ? // @ts-expect-error no type + paymentSource.payment_response?.status?.toLowerCase() + : null if (isSingle) { const [paymentMethod] = paymentMethods ?? [] if (paymentMethod && !paymentSource) { @@ -154,25 +170,35 @@ export function PaymentMethod({ paymentMethod?.payment_source_type as PaymentResource await setPaymentMethod({ paymentResource, paymentMethodId }) let attributes: Record | undefined = {} - if (config != null && paymentResource === 'paypal_payments') { + if (config != null && paymentResource === "paypal_payments") { attributes = getPaypalAttributes(paymentResource, config) } - if (config != null && paymentResource === 'external_payments') { + if (config != null && paymentResource === "external_payments") { attributes = getExternalPaymentAttributes( paymentResource, - config + config, ) } + if ( + config != null && + paymentResource === "checkout_com_payments" + ) { + attributes = getCkoAttributes(paymentResource, config) + } const ps = await setPaymentSource({ paymentResource, order, - attributes + attributes, }) if (ps && paymentMethod && onClick != null) { setPaymentSourceCreated(true) onClick({ payment: paymentMethod, order, paymentSource: ps }) setTimeout(() => { - setLoading(false) + if (showLoader && errors?.length === 0) { + setLoading(showLoader) + } else { + setLoading(false) + } }, 200) } if (getCustomerPaymentSources) { @@ -180,39 +206,82 @@ export function PaymentMethod({ } setLoadingPlaceOrder({ loading: false }) } - if (typeof autoSelectSinglePaymentMethod === 'function') { + if (typeof autoSelectSinglePaymentMethod === "function") { autoSelectSinglePaymentMethod() } } else { setTimeout(() => { - setLoading(false) + if ( + showLoader && + errors?.length === 0 && + paymentSourceStatus !== "declined" + ) { + setLoading(showLoader) + } else { + setLoading(false) + } }, 200) } } autoSelect() } } - }, [!isEmpty(paymentMethods), order?.payment_source != null]) + }, [!isEmpty(paymentMethods), order?.payment_source != null, errors?.length]) useEffect(() => { if (paymentMethods) { const isSingle = paymentMethods.length === 1 + const paymentSourceStatus = paymentSource + ? // @ts-expect-error no type + paymentSource.payment_response?.status?.toLowerCase() + : null if (isSingle && autoSelectSinglePaymentMethod) { if (paymentSource) { setTimeout(() => { - setLoading(false) + if ( + showLoader && + errors?.length === 0 && + paymentSourceStatus !== "declined" + ) { + setLoading(showLoader) + } else { + setLoading(false) + } }, 200) } } else { - setLoading(false) + if ( + showLoader && + errors?.length === 0 && + paymentSourceStatus !== "declined" + ) { + setLoading(showLoader) + } else { + setLoading(false) + } } } if (currentPaymentMethodId) setPaymentSelected(currentPaymentMethodId) return () => { setLoading(true) - setPaymentSelected('') + setPaymentSelected("") } - }, [paymentMethods, currentPaymentMethodId]) - + }, [paymentMethods, currentPaymentMethodId, errors?.length]) + useEffect(() => { + const status = + // @ts-expect-error no type + order?.payment_source?.payment_response?.status?.toLowerCase() + // If showLoader is undefined, we don't change the loading + if (showLoader && status) { + if (status.toLowerCase() === "declined") { + setLoading(false) + } else { + setLoading(true) + } + } else { + setLoading(false) + } + // @ts-expect-error no type + }, [showLoader, order?.payment_source?.payment_response?.status]) const sortedPaymentMethods = paymentMethods != null && sortBy != null ? sortPaymentMethods(paymentMethods, sortBy) @@ -223,19 +292,20 @@ export function PaymentMethod({ if (Array.isArray(hide)) { const source = payment?.payment_source_type as PaymentResource return !hide?.includes(source) - } else if (typeof hide === 'function') { + } + if (typeof hide === "function") { return hide(payment) } return true }) - .map((payment, k) => { + .map((payment) => { const isActive = currentPaymentMethodId === payment?.id const paymentMethodProps = { payment, clickableContainer, paymentSelected, setPaymentSelected, - expressPayments + expressPayments, } const paymentResource = payment?.payment_source_type as PaymentResource const onClickable = !clickableContainer @@ -245,12 +315,12 @@ export function PaymentMethod({ const paymentMethodId = payment?.id const currentPaymentMethodId = order?.payment_method?.id if (paymentMethodId === currentPaymentMethodId) return - if (status === 'placing') return + if (status === "placing") return setLoadingPlaceOrder({ loading: true }) setPaymentSelected(payment.id) const { order: updatedOrder } = await setPaymentMethod({ paymentResource, - paymentMethodId + paymentMethodId, }) if (onClick) onClick({ payment, order: updatedOrder }) setLoadingPlaceOrder({ loading: false }) @@ -258,9 +328,9 @@ export function PaymentMethod({ return (
{ if (onClickable != null) { diff --git a/packages/react-components/src/components/payment_methods/PaymentMethodsContainer.tsx b/packages/react-components/src/components/payment_methods/PaymentMethodsContainer.tsx index a65a5f27..dfa41f1c 100644 --- a/packages/react-components/src/components/payment_methods/PaymentMethodsContainer.tsx +++ b/packages/react-components/src/components/payment_methods/PaymentMethodsContainer.tsx @@ -1,39 +1,44 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import PaymentMethodContext, { - defaultPaymentMethodContext -} from '#context/PaymentMethodContext' import { + type JSX, type ReactNode, useContext, useEffect, - useReducer, useMemo, - type JSX -} from 'react' + useReducer, +} from "react" +import CommerceLayerContext from "#context/CommerceLayerContext" +import OrderContext from "#context/OrderContext" +import PaymentMethodContext, { + defaultPaymentMethodContext, +} from "#context/PaymentMethodContext" import paymentMethodReducer, { - paymentMethodInitialState, getPaymentMethods, type PaymentMethodConfig, - setPaymentMethodConfig, type PaymentRef, - setPaymentRef -} from '#reducers/PaymentMethodReducer' -import OrderContext from '#context/OrderContext' -import CommerceLayerContext from '#context/CommerceLayerContext' -import type { BaseError } from '#typings/errors' -import useCustomContext from '#utils/hooks/useCustomContext' -import { isEmpty } from '#utils/isEmpty' -import { setCustomerOrderParam } from '#utils/localStorage' + paymentMethodInitialState, + setPaymentMethodConfig, + setPaymentRef, +} from "#reducers/PaymentMethodReducer" +import type { BaseError } from "#typings/errors" +import useCustomContext from "#utils/hooks/useCustomContext" +import { isEmpty } from "#utils/isEmpty" +import { setCustomerOrderParam } from "#utils/localStorage" interface Props { + /** + * The children components to render inside the PaymentMethodsContainer. + */ children: ReactNode + /** + * Optional configuration for payment methods. + */ config?: PaymentMethodConfig } export function PaymentMethodsContainer(props: Props): JSX.Element { const { children, config } = props const [state, dispatch] = useReducer( paymentMethodReducer, - paymentMethodInitialState + paymentMethodInitialState, ) const { order, @@ -42,27 +47,27 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { include, addResourceToInclude, updateOrder, - includeLoaded + includeLoaded, } = useCustomContext({ context: OrderContext, - contextComponentName: 'OrderContainer', - currentComponentName: 'PaymentMethodsContainer', - key: 'order' + contextComponentName: "OrderContainer", + currentComponentName: "PaymentMethodsContainer", + key: "order", }) const credentials = useContext(CommerceLayerContext) async function getPayMethods(): Promise { order && (await getPaymentMethods({ order, dispatch })) } useEffect(() => { - if (!include?.includes('available_payment_methods')) { + if (!include?.includes("available_payment_methods")) { addResourceToInclude({ newResource: [ - 'available_payment_methods', - 'payment_source', - 'payment_method', - 'line_items.line_item_options.sku_option', - 'line_items.item' - ] + "available_payment_methods", + "payment_source", + "payment_method", + "line_items.line_item_options.sku_option", + "line_items.item", + ], }) } else if (!includeLoaded?.available_payment_methods) { addResourceToInclude({ @@ -70,9 +75,9 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { available_payment_methods: true, payment_source: true, payment_method: true, - 'line_items.line_item_options.sku_option': true, - 'line_items.item': true - } + "line_items.line_item_options.sku_option": true, + "line_items.item": true, + }, }) } if (config && isEmpty(state.config)) @@ -80,29 +85,29 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { if (credentials && order && !state.paymentMethods) { getPayMethods() } - if (order?.payment_source) { - dispatch({ - type: 'setPaymentSource', - payload: { - paymentSource: order?.payment_source - } - }) - } if (order?.payment_source === null) { // Reset save customer payment source to wallet param if the payment source is null - setCustomerOrderParam('_save_payment_source_to_customer_wallet', 'false') + setCustomerOrderParam("_save_payment_source_to_customer_wallet", "false") dispatch({ - type: 'setPaymentSource', + type: "setPaymentSource", payload: { - paymentSource: undefined - } + paymentSource: undefined, + }, }) } + if ( + order?.id && + order?.payment_source == null && + !["draft", "pending"].includes(order?.status) && + !state.paymentMethods + ) { + getOrder(order.id) + } }, [ order, credentials, include?.length, - Object.keys(includeLoaded ?? []).length + Object.keys(includeLoaded ?? []).length, ]) const contextValue = useMemo(() => { return { @@ -123,7 +128,7 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { updateOrder, order, dispatch, - setOrderErrors + setOrderErrors, }), setPaymentSource: async (args: any) => await defaultPaymentMethodContext.setPaymentSource({ @@ -133,13 +138,13 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { dispatch, getOrder, updateOrder, - order + order, }), updatePaymentSource: async (args: any) => { await defaultPaymentMethodContext.updatePaymentSource({ ...args, config: credentials, - dispatch + dispatch, }) }, destroyPaymentSource: async (args: any) => { @@ -148,9 +153,9 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { dispatch, config: credentials, updateOrder, - orderId: order?.id + orderId: order?.id, }) - } + }, } }, [state]) return ( diff --git a/packages/react-components/src/components/payment_source/AdyenPayment.tsx b/packages/react-components/src/components/payment_source/AdyenPayment.tsx index dbb72708..a31013aa 100644 --- a/packages/react-components/src/components/payment_source/AdyenPayment.tsx +++ b/packages/react-components/src/components/payment_source/AdyenPayment.tsx @@ -1,15 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { - type FormEvent, - useContext, - useEffect, - useRef, - useState, - type JSX, -} from "react" -import PaymentMethodContext from "#context/PaymentMethodContext" -import type { PaymentSourceProps } from "./PaymentSource" -import { setCustomerOrderParam } from "#utils/localStorage" + import { type AdditionalDetailsData, AdyenCheckout, @@ -24,22 +14,55 @@ import { type UIElement, type UIElementProps, } from "@adyen/adyen-web/auto" +import type { + AdyenPayment as AdyenPaymentType, + Order, +} from "@commercelayer/sdk" +import { + type FormEvent, + type JSX, + useContext, + useEffect, + useRef, + useState, +} from "react" import Parent from "#components/utils/Parent" -import browserInfo, { cleanUrlBy } from "#utils/browserInfo" -import PlaceOrderContext from "#context/PlaceOrderContext" +import CommerceLayerContext from "#context/CommerceLayerContext" +import CustomerContext from "#context/CustomerContext" import OrderContext from "#context/OrderContext" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PlaceOrderContext from "#context/PlaceOrderContext" +import browserInfo, { cleanUrlBy } from "#utils/browserInfo" import { getPublicIP } from "#utils/getPublicIp" -import CustomerContext from "#context/CustomerContext" +import { hasSubscriptions } from "#utils/hasSubscriptions" +import { setCustomerOrderParam } from "#utils/localStorage" +import type { PaymentSourceProps } from "./PaymentSource" interface PaymentMethodsStyle { card?: CardConfiguration["styles"] paypal?: PayPalConfiguration["style"] } +type PaymentMethodType = + | "scheme" + | "giftcard" + | "paypal" + | "applepay" + | "googlepay" + | (string & {}) + /** * Configuration options for the Adyen payment component. */ export interface AdyenPaymentConfig { + /** + * Payment methods to be used for subscriptions. + * This is an array of payment method types that are supported for subscription payments. + * For example, it can include "scheme" for card payments. + * @default all available payment methods + * @example ["scheme"] + */ + subscriptionPaymentMethods?: PaymentMethodType[] /** * Optional CSS class name for the card container. */ @@ -76,6 +99,17 @@ export interface AdyenPaymentConfig { recurringDetailReference: string shopperReference: string | undefined }) => Promise + /** + * Callback function to be called when the Adyen component is ready. + * @returns void. + */ + onReady?: () => void + /** + * onSelect callback function to be called when a payment method is selected. + * @param component - The selected payment method component. + * @returns void. + */ + onSelect?: (component: UIElement) => void giftcardErrorComponent?: (message: string) => JSX.Element } @@ -101,6 +135,9 @@ export function AdyenPayment({ styles, onDisableStoredPaymentMethod, giftcardErrorComponent, + onReady, + onSelect, + subscriptionPaymentMethods, } = { ...defaultConfig, ...config, @@ -118,8 +155,10 @@ export function AdyenPayment({ setPaymentRef, currentCustomerPaymentSourceId, } = useContext(PaymentMethodContext) - const { order, updateOrder } = useContext(OrderContext) - const { placeOrderButtonRef, setPlaceOrder } = useContext(PlaceOrderContext) + const { order, updateOrder, getOrderByFields } = useContext(OrderContext) + const authConfig = useContext(CommerceLayerContext) + const { placeOrderButtonRef, setPlaceOrder, status } = + useContext(PlaceOrderContext) const { customers } = useContext(CustomerContext) const ref = useRef(null) const dropinRef = useRef(null) @@ -212,6 +251,7 @@ export function AdyenPayment({ CheckoutAdvancedFlowResponse & { paymentMethodType?: string message?: string + paymentStatus?: Order["payment_status"] } > => { const url = cleanUrlBy() @@ -228,7 +268,17 @@ export function AdyenPayment({ control?.payment_response?.paymentMethod?.type ?? // @ts-expect-error no type control?.payment_request_data?.payment_method?.type - if (controlCode === "Authorised" && paymentMethodType !== "giftcard") { + const getOrderStatus = await getOrderByFields({ + orderId: order?.id ?? "", + fields: ["status", "payment_status"], + config: authConfig, + }) + const paymentStatus = getOrderStatus?.payment_status + if ( + controlCode === "Authorised" && + paymentMethodType !== "giftcard" && + paymentStatus !== "partially_authorized" + ) { return { resultCode: controlCode, } @@ -243,13 +293,11 @@ export function AdyenPayment({ redirect_from_issuer_method: "GET", shopper_ip: shopperIp, shopperInteraction: "Ecommerce", - recurringProcessingModel: "CardOnFile", browser_info: { ...browserInfo(), }, }, } - // biome-ignore lint/performance/noDelete: Need to test delete attributes.payment_request_data.paymentMethod try { await setPaymentSource({ @@ -263,65 +311,44 @@ export function AdyenPayment({ resultCode: "Error", } } - // Authorize remaining amount with other payment method after gift card - if ( - ["Cancelled", "Refused"].includes(controlCode) && - paymentMethodType === "giftcard" && - currentPaymentMethodType !== "giftcard" - ) { - const availableGiftCardAmount = Number.parseInt( - // @ts-expect-error no type - control?.payment_response?.additionalData - ?.currentBalanceValue as string, - ) - const totalPartialAmount = - order?.total_amount_with_taxes_cents != null && - availableGiftCardAmount != null - ? order?.total_amount_with_taxes_cents - availableGiftCardAmount - : 0 - await updateOrder({ - id: order.id, - attributes: { - _authorization_amount_cents: totalPartialAmount, - _place: true, - }, - }) - await setPaymentSource({ - paymentSourceId: paymentSource?.id, - paymentResource: "adyen_payments", - attributes: { - // @ts-expect-error no type - payment_request_data: control?.payment_request_data, - }, - }) - await updateOrder({ - id: order.id, - attributes: { - _authorize: true, - }, - }) - // Add gift card amount as payment method attribute - return { - resultCode: "Authorised", - paymentMethodType: currentPaymentMethodType, - } - } // First gift card authorization for partial or total amount if (currentPaymentMethodType === "giftcard") { - const firstAuthorization = await setPaymentSource({ + // Request balance check if the gift card can cover the total amount + const giftCardBalanceCheck = (await setPaymentSource({ paymentSourceId: paymentSource?.id, paymentResource: "adyen_payments", attributes: { - _authorize: 1, + _balance: true, }, + })) as AdyenPaymentType + const currentBalance = giftCardBalanceCheck?.balance ?? 0 + const totalAmount = order?.total_amount_with_taxes_cents ?? 0 + const attributes = + currentBalance >= totalAmount + ? { + _authorize: true, + } + : { + _authorization_amount_cents: currentBalance, + _authorize: true, + } + const { order: orderUpdated } = await updateOrder({ + id: order.id, + attributes, }) - // @ts-expect-error no type - const resultCode = firstAuthorization?.payment_response?.resultCode + const resultCode = + // @ts-expect-error no type + orderUpdated?.payment_source?.payment_response?.resultCode const refusalReasonCode = // @ts-expect-error no type - firstAuthorization?.payment_response?.refusalReasonCode - // @ts-expect-error no type - const errorCode = firstAuthorization?.payment_response?.errorCode + orderUpdated?.payment_source?.payment_response?.refusalReasonCode + const errorCode = + // @ts-expect-error no type + orderUpdated?.payment_source?.payment_response?.errorCode + const action = + // @ts-expect-error no type + orderUpdated?.payment_source?.payment_response?.action + const paymentStatus = orderUpdated?.payment_status if ( (["Cancelled", "Refused"].includes(resultCode) && refusalReasonCode !== "12") || @@ -329,9 +356,9 @@ export function AdyenPayment({ ) { const message = // @ts-expect-error no type - firstAuthorization?.payment_response?.refusalReason ?? + orderUpdated?.payment_response?.refusalReason ?? // @ts-expect-error no type - firstAuthorization?.payment_response?.message + orderUpdated?.payment_response?.message return { resultCode: errorCode ? "Refused" : resultCode, @@ -341,6 +368,8 @@ export function AdyenPayment({ return { resultCode: "Authorised", paymentMethodType: currentPaymentMethodType, + action, + paymentStatus, } } const res = await setPaymentSource({ @@ -441,7 +470,7 @@ export function AdyenPayment({ } } - // biome-ignore lint/correctness/useExhaustiveDependencies: + // biome-ignore lint/correctness/useExhaustiveDependencies: Infinite loop useEffect(() => { const paymentMethodsResponse = { // @ts-expect-error no type @@ -460,10 +489,32 @@ export function AdyenPayment({ "Payment methods are not available. Please, check your Adyen configuration.", ) } - const showStoredPaymentMethods = + let showStoredPaymentMethods = // @ts-expect-error no type paymentSource?.payment_methods?.storedPaymentMethods != null ?? false - + if (order && hasSubscriptions(order)) { + /** + * If the order has subscriptions, we don't show stored payment methods + */ + showStoredPaymentMethods = false + /** + * Need to reset stored payment methods + * to avoid showing them when the order has subscriptions + */ + paymentMethodsResponse.storedPaymentMethods = [] + /** + * Remove scheme payment methods + * because they are not supported in subscriptions + */ + paymentMethodsResponse.paymentMethods = + subscriptionPaymentMethods != null && + subscriptionPaymentMethods.length > 0 + ? paymentMethodsResponse.paymentMethods.filter( + (pm: { type: PaymentMethodType }) => + subscriptionPaymentMethods.includes(pm.type), + ) + : paymentMethodsResponse.paymentMethods + } const options = { locale: order?.language_code ?? locale, environment, @@ -493,7 +544,10 @@ export function AdyenPayment({ }, onSubmit: (state, element, actions) => { const handleSubmit = async (): Promise => { - const { resultCode, action, message } = await onSubmit(state, element) + const { resultCode, action, message, paymentStatus } = await onSubmit( + state, + element, + ) if (["Cancelled", "Refused"].includes(resultCode)) { actions.reject() if (message) { @@ -505,7 +559,9 @@ export function AdyenPayment({ actions.resolve({ resultCode, }) - dropinRef.current?.mount("#adyen-dropin") + if (paymentStatus === "partially_authorized") { + dropinRef.current?.mount("#adyen-dropin") + } setGiftcardError(null) } } @@ -580,6 +636,12 @@ export function AdyenPayment({ setPaymentRef({ ref }) } } + if (onSelect) { + onSelect(component) + } + }, + onReady() { + if (onReady) onReady() }, }).mount("#adyen-dropin") if (dropin && checkout) { @@ -588,7 +650,8 @@ export function AdyenPayment({ setLoadAdyen(true) } } - if (!dropinRef.current) { + const html = document.getElementById("adyen-dropin") + if (!dropinRef.current && status === "standby" && html) { initializeAdyen() } } @@ -596,7 +659,7 @@ export function AdyenPayment({ setPaymentRef({ ref: { current: null } }) setLoadAdyen(false) } - }, [clientKey, ref != null]) + }, [clientKey, ref != null, status]) return !clientKey && !loadAdyen && !checkout ? null : (
boolean + type: string + submit: () => unknown + tokenize: () => Promise<{ + data: { + token: string + } + }> +} + +interface CheckoutWebComponent { + appearance?: Partial + showPayButton?: boolean + publicKey: string + environment: "sandbox" | "production" + locale?: string + paymentSession: string + onReady?: () => void + submit?: () => unknown + onPaymentCompleted?: ( + component: Component, + paymentResponse: { + status: string + id: string + type: string + }, + ) => Promise + onChange?: (component: Component) => void + onError?: (component: Component, error: unknown) => void + create?: (type: "flow") => { + mount: (element: HTMLElement | null) => void + } + componentOptions?: { + card?: { + displayCardholderName?: "hidden" | "top" | "bottom" + } + } +} export interface CheckoutComConfig { containerClassName?: string hintLabel?: string name?: string - success_url?: string - failure_url?: string + success_url: string + failure_url: string options?: { - style: FramesStyle + appearance: Appearance } [key: string]: unknown } -const systemLanguages: FramesLanguages[] = [ - 'DE-DE', - 'EN-GB', - 'ES-ES', - 'FR-FR', - 'IT-IT', - 'KO-KR', - 'NL-NL' -] - -const defaultOptions = { - style: { - base: { - color: 'black', - fontSize: '18px' - }, - autofill: { - backgroundColor: 'yellow' - }, - hover: { - color: 'blue' - }, - focus: { - color: 'blue' - }, - valid: { - color: 'green' - }, - invalid: { - color: 'red' - }, - placeholder: { - base: { - color: 'gray' - }, - focus: { - border: 'solid 1px blue' - } - } - } -} - -type Props = PaymentMethodConfig['checkoutComPayment'] & - JSX.IntrinsicElements['div'] & { +type Props = Partial & + JSX.IntrinsicElements["div"] & { show?: boolean publicKey: string locale?: string - templateCustomerSaveToWallet?: PaymentSourceProps['templateCustomerSaveToWallet'] + templateCustomerSaveToWallet?: PaymentSourceProps["templateCustomerSaveToWallet"] } export function CheckoutComPayment({ publicKey, - options = defaultOptions, - locale = 'EN-GB', + options, ...p }: Props): JSX.Element | null { const ref = useRef(null) const loaded = useExternalScript(scriptUrl) - const { - setPaymentRef, - currentPaymentMethodType, - paymentSource, - setPaymentSource, - setPaymentMethodErrors - } = useContext(PaymentMethodContext) + const { setPaymentRef, setPaymentSource } = useContext(PaymentMethodContext) + const { accessToken } = useContext(CommerceLayerContext) const { order } = useContext(OrderContext) + const { setPlaceOrderStatus } = useContext(PlaceOrderContext) const { containerClassName, templateCustomerSaveToWallet, - successUrl = window.location.href, - failureUrl = window.location.href, show, ...divProps } = p - const handleSubmit = async (): Promise => { - const savePaymentSourceToCustomerWallet: string = + useEffect(() => { + const ps = order?.payment_source + if (loaded && window && ps && accessToken) { // @ts-expect-error no type - ref?.current?.elements?.save_payment_source_to_customer_wallet?.checked - if (savePaymentSourceToCustomerWallet) { - setCustomerOrderParam( - '_save_payment_source_to_customer_wallet', - savePaymentSourceToCustomerWallet - ) - } - if (window.Frames) { - window.Frames.cardholder = { - name: order?.billing_address?.full_name, - billingAddress: { - addressLine1: order?.billing_address?.line_1, - addressLine2: order?.billing_address?.line_2 ?? '', - zip: order?.billing_address?.zip_code ?? '', - city: order?.billing_address?.city, - state: order?.billing_address?.state_code, - country: order?.billing_address?.country_code - }, - phone: order?.billing_address?.phone - } - try { - const data = await window.Frames.submitCard() - if (data.token && paymentSource && currentPaymentMethodType) { - const ps = (await setPaymentSource({ - paymentSourceId: paymentSource.id, - paymentResource: currentPaymentMethodType, - attributes: { - token: data.token, - payment_type: 'token', - success_url: successUrl, - failure_url: failureUrl, - _authorize: true - } - })) as PaymentSourceObject['checkout_com_payments'] - if (ps?.redirect_uri) { - window.location.href = ps.redirect_uri - } + const publicKey = ps.public_key + // @ts-expect-error no type + const paymentSession = ps.payment_session + // @ts-expect-error no type + if (window?.CheckoutWebComponents) { + const environment = jwt(accessToken).test ? "sandbox" : "production" + const locale = order?.language_code ?? "en" + const loadFlow = async () => { + // @ts-expect-error no type + const checkout = await window.CheckoutWebComponents({ + appearance: { + ...options?.appearance, + }, + showPayButton: false, + publicKey, + environment, + locale, + paymentSession, + componentOptions: { + card: { + displayCardholderName: "hidden", + }, + }, + onChange: (component) => { + if (ref.current) { + if (component.isValid()) { + ref.current.onsubmit = async (): Promise => { + const element = ref.current?.elements + const savePaymentSourceToCustomerWallet = + // @ts-expect-error no type + element?.save_payment_source_to_customer_wallet?.checked + if (savePaymentSourceToCustomerWallet) + setCustomerOrderParam( + "_save_payment_source_to_customer_wallet", + savePaymentSourceToCustomerWallet, + ) + const { data } = await component.tokenize() + const token = data?.token + const paymentSource = await setPaymentSource({ + paymentSourceId: ps.id, + paymentResource: "checkout_com_payments", + attributes: { + token, + _authorize: true, + }, + }) + if (paymentSource) { + // @ts-expect-error no type + const response = paymentSource.payment_response + const paymentStatus = response?.status.toLowerCase() + // @ts-expect-error no type + const securityRedirect = paymentSource?.redirect_uri + const isStatusPending = paymentStatus === "pending" + if (isStatusPending && securityRedirect) { + window.location.href = securityRedirect + return false + } + if (paymentStatus === "declined") return false + } + return false + } + setPaymentRef?.({ ref }) + } + } + }, + onError: (component, error) => { + console.error("onError", { error }, "Component", component.type) + }, + onPaymentCompleted: async (_component, paymentResponse) => { + if (paymentResponse.status.toLowerCase() === "approved") { + await setPaymentSource({ + paymentSourceId: ps.id, + paymentResource: "checkout_com_payments", + attributes: { + token: paymentResponse.id, + _authorize: true, + }, + }) + setPlaceOrderStatus?.({ + status: "placing", + }) + } + }, + } satisfies CheckoutWebComponent) + const flowComponent = checkout.create("flow") + flowComponent.mount(document.getElementById("flow-container")) } - } catch (error: any) { - console.error(error) - setPaymentMethodErrors([ - { - code: 'PAYMENT_INTENT_AUTHENTICATION_FAILURE', - resource: 'payment_methods', - field: currentPaymentMethodType, - message: error?.message as string - } - ]) + loadFlow() } } - return false - } - const lang = - `${locale.toUpperCase()}-${locale.toUpperCase()}` as FramesLanguages - const localization = systemLanguages.includes(lang) ? lang : 'EN-GB' + }, [loaded, order?.payment_source?.id, accessToken]) return loaded && show ? (
- { - if (e.isValid && ref.current) { - ref.current.onsubmit = async () => { - return await handleSubmit() - } - setPaymentRef({ ref }) - } - }} - cardTokenized={(data) => data} - > - - - - +
{templateCustomerSaveToWallet && ( - + {templateCustomerSaveToWallet} )} diff --git a/packages/react-components/src/components/payment_source/PaymentSource.tsx b/packages/react-components/src/components/payment_source/PaymentSource.tsx index 23522d51..9ec0d05d 100644 --- a/packages/react-components/src/components/payment_source/PaymentSource.tsx +++ b/packages/react-components/src/components/payment_source/PaymentSource.tsx @@ -1,13 +1,13 @@ -import { useContext, useState, useEffect, type JSX } from "react" +import { type JSX, useContext, useEffect, useState } from "react" +import CustomerContext from "#context/CustomerContext" +import OrderContext from "#context/OrderContext" import PaymentMethodChildrenContext from "#context/PaymentMethodChildrenContext" import PaymentMethodContext from "#context/PaymentMethodContext" -import CustomerContext from "#context/CustomerContext" -import PaymentGateway from "../payment_gateways/PaymentGateway" import type { PaymentResource } from "#reducers/PaymentMethodReducer" import type { LoaderType } from "#typings/index" -import type { CustomerCardsTemplateChildren } from "../utils/PaymentCardsTemplate" import getCardDetails from "#utils/getCardDetails" -import OrderContext from "#context/OrderContext" +import PaymentGateway from "../payment_gateways/PaymentGateway" +import type { CustomerCardsTemplateChildren } from "../utils/PaymentCardsTemplate" export interface CustomerCardsProps { handleClick: () => void @@ -35,6 +35,7 @@ export function PaymentSource(props: PaymentSourceProps): JSX.Element { const { order } = useContext(OrderContext) const { payments } = useContext(CustomerContext) const { + errors, currentPaymentMethodId, paymentSource, destroyPaymentSource, @@ -48,6 +49,9 @@ export function PaymentSource(props: PaymentSourceProps): JSX.Element { const isCustomerPaymentSource = currentCustomerPaymentSourceId != null && currentCustomerPaymentSourceId === paymentSource?.id + const checkPaymentSourceStatus = + // @ts-expect-error no type + paymentSource?.payment_response?.status?.toLowerCase() if (readonly) { setShow(true) setShowCard(true) @@ -58,7 +62,7 @@ export function PaymentSource(props: PaymentSourceProps): JSX.Element { const card = getCardDetails({ paymentType: payment?.payment_source_type as PaymentResource, customerPayment: { - payment_source: paymentSource, + payment_source: paymentSource ?? order?.payment_source, }, }) if (isCustomerPaymentSource && card.brand === "") { @@ -68,9 +72,16 @@ export function PaymentSource(props: PaymentSourceProps): JSX.Element { ? card.issuer_type : "credit-card" } - if (card.brand) { + if ( + card.brand && + errors?.length === 0 && + checkPaymentSourceStatus !== "declined" + ) { setShowCard(true) } + if (checkPaymentSourceStatus === "declined") { + setShowCard(false) + } setShow(true) } else if ( expressPayments && @@ -90,6 +101,7 @@ export function PaymentSource(props: PaymentSourceProps): JSX.Element { readonly, order?.status, expressPayments, + errors?.length, ]) const handleEditClick = async (e: MouseEvent): Promise => { e.stopPropagation() diff --git a/packages/react-components/src/components/payment_source/StripePayment.tsx b/packages/react-components/src/components/payment_source/StripePayment.tsx index b4da6a45..7cee18e4 100644 --- a/packages/react-components/src/components/payment_source/StripePayment.tsx +++ b/packages/react-components/src/components/payment_source/StripePayment.tsx @@ -1,82 +1,85 @@ -import { useContext, useEffect, useRef, useState, type JSX } from 'react'; -import PaymentMethodContext from '#context/PaymentMethodContext' -import { - Elements, - PaymentElement, - useElements, - useStripe -} from '@stripe/react-stripe-js' +import { Elements, PaymentElement, useElements } from "@stripe/react-stripe-js" import type { Stripe, + StripeConstructorOptions, StripeElementLocale, StripeElements, StripeElementsOptions, - StripePaymentElementOptions -} from '@stripe/stripe-js' -import type { PaymentMethodConfig } from '#reducers/PaymentMethodReducer' -import type { PaymentSourceProps } from './PaymentSource' -import Parent from '#components/utils/Parent' -import { setCustomerOrderParam } from '#utils/localStorage' -import OrderContext from '#context/OrderContext' -import { StripeExpressPayment } from './StripeExpressPayment' + StripePaymentElementChangeEvent, + StripePaymentElementOptions, +} from "@stripe/stripe-js" +import { type JSX, useContext, useEffect, useRef, useState } from "react" +import Parent from "#components/utils/Parent" +import OrderContext from "#context/OrderContext" +import PaymentMethodContext from "#context/PaymentMethodContext" +import PlaceOrderContext from "#context/PlaceOrderContext" +import useCommerceLayer from "#hooks/useCommerceLayer" +import type { PaymentMethodConfig } from "#reducers/PaymentMethodReducer" +import { setCustomerOrderParam } from "#utils/localStorage" +import type { PaymentSourceProps } from "./PaymentSource" +import { StripeExpressPayment } from "./StripeExpressPayment" export interface StripeConfig { containerClassName?: string hintLabel?: string name?: string options?: StripePaymentElementOptions - appearance?: StripeElementsOptions['appearance'] + appearance?: StripeElementsOptions["appearance"] + // biome-ignore lint/suspicious/noExplicitAny: No type available [key: string]: any } interface StripePaymentFormProps { options?: StripePaymentElementOptions - templateCustomerSaveToWallet?: PaymentSourceProps['templateCustomerSaveToWallet'] + templateCustomerSaveToWallet?: PaymentSourceProps["templateCustomerSaveToWallet"] + stripe?: Stripe | null } -type SubmitEvent = React.FormEvent - interface OnSubmitArgs { - event: SubmitEvent + event: HTMLFormElement | null stripe: Stripe | null elements: StripeElements | null } const defaultOptions: StripePaymentElementOptions = { layout: { - type: 'accordion', + type: "accordion", defaultCollapsed: false, radios: true, - spacedAccordionItems: false + spacedAccordionItems: false, }, - fields: { billingDetails: 'never' } + fields: { billingDetails: "never" }, } -const defaultAppearance: StripeElementsOptions['appearance'] = { - theme: 'stripe', +const defaultAppearance: StripeElementsOptions["appearance"] = { + theme: "stripe", variables: { - colorText: '#32325d', - fontFamily: '"Helvetica Neue", Helvetica, sans-serif' - } + colorText: "#32325d", + fontFamily: '"Helvetica Neue", Helvetica, sans-serif', + }, } +let selectedPaymentMethodType: string | null = null + function StripePaymentForm({ options = defaultOptions, - templateCustomerSaveToWallet + templateCustomerSaveToWallet, + stripe, }: StripePaymentFormProps): JSX.Element { const ref = useRef(null) const { currentPaymentMethodType, setPaymentMethodErrors, setPaymentRef } = useContext(PaymentMethodContext) - const { order } = useContext(OrderContext) - const stripe = useStripe() + const { order, setOrderErrors } = useContext(OrderContext) + const { sdkClient } = useCommerceLayer() + const { setPlaceOrderStatus } = useContext(PlaceOrderContext) const elements = useElements() useEffect(() => { if (ref.current && stripe && elements) { ref.current.onsubmit = async () => { return await onSubmit({ - event: ref.current as any, + event: ref.current, stripe, - elements + elements, }) } setPaymentRef({ ref }) @@ -88,73 +91,134 @@ function StripePaymentForm({ const onSubmit = async ({ event, stripe, - elements + elements, }: OnSubmitArgs): Promise => { if (!stripe) return false - + const sdk = sdkClient() + if (sdk == null) return false + if (order == null) return false + if ( + selectedPaymentMethodType && + !["apple_pay", "google_pay"].includes(selectedPaymentMethodType) + ) { + const { status } = await sdk.orders.retrieve(order?.id, { + fields: ["status"], + }) + const isDraftOrder = status === "draft" + if (isDraftOrder) { + /** + * Draft order cannot be placed + */ + setOrderErrors([ + { + code: "VALIDATION_ERROR", + resource: "orders", + message: "Draft order cannot be placed", + }, + ]) + setPlaceOrderStatus?.({ + status: "disabled", + }) + return false + } + } const savePaymentSourceToCustomerWallet: string = // @ts-expect-error no type event?.elements?.save_payment_source_to_customer_wallet?.checked if (savePaymentSourceToCustomerWallet) setCustomerOrderParam( - '_save_payment_source_to_customer_wallet', - savePaymentSourceToCustomerWallet + "_save_payment_source_to_customer_wallet", + savePaymentSourceToCustomerWallet, ) if (elements != null) { const billingInfo = order?.billing_address - const email = order?.customer_email ?? '' + const email = order?.customer_email ?? "" const billingDetails = { - name: billingInfo?.full_name ?? '', + name: billingInfo?.full_name ?? "", email, phone: billingInfo?.phone, address: { city: billingInfo?.city, country: billingInfo?.country_code, line1: billingInfo?.line_1, - line2: billingInfo?.line_2 ?? '', - postal_code: billingInfo?.zip_code ?? '', - state: billingInfo?.state_code - } + line2: billingInfo?.line_2 ?? "", + postal_code: billingInfo?.zip_code ?? "", + state: billingInfo?.state_code, + }, } const url = new URL(window.location.href) - const cleanUrl = `${url.origin}${url.pathname}?accessToken=${url.searchParams.get('accessToken')}` + const cleanUrl = `${url.origin}${url.pathname}?accessToken=${url.searchParams.get("accessToken")}` const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: cleanUrl, payment_method_data: { - billing_details: billingDetails - } + billing_details: billingDetails, + }, }, - redirect: 'if_required' + redirect: "if_required", }) if (error) { console.error(error) setPaymentMethodErrors([ { - code: 'PAYMENT_INTENT_AUTHENTICATION_FAILURE', - resource: 'payment_methods', + code: "PAYMENT_INTENT_AUTHENTICATION_FAILURE", + resource: "payment_methods", field: currentPaymentMethodType, - message: error.message ?? '' - } + message: error.message ?? "", + }, ]) return false - } else { - return true } + return true } return false } + async function handleChange(event: StripePaymentElementChangeEvent) { + console.debug("StripePaymentElement onChange event", { event }) + selectedPaymentMethodType = event.value.type + // Handle change events from the PaymentElement + if ( + event.complete && + ["apple_pay", "google_pay"].includes(event.value.type) + ) { + const sdk = sdkClient() + if (sdk == null) return + if (order == null) return + const { status } = await sdk.orders.retrieve(order?.id, { + fields: ["status"], + }) + const isDraftOrder = status === "draft" + if (isDraftOrder) { + /** + * Draft order cannot be placed + */ + setOrderErrors([ + { + code: "VALIDATION_ERROR", + resource: "orders", + message: "Draft order cannot be placed", + }, + ]) + setPlaceOrderStatus?.({ + status: "disabled", + }) + return + } + } + } + return ( {/* */} {templateCustomerSaveToWallet && ( - + {templateCustomerSaveToWallet} )} @@ -162,14 +226,15 @@ function StripePaymentForm({ ) } -type Props = PaymentMethodConfig['stripePayment'] & - Omit & - Partial & { +type Props = PaymentMethodConfig["stripePayment"] & + Omit & + Partial & { show?: boolean publishableKey: string locale?: StripeElementLocale clientSecret: string expressPayments?: boolean + connectedAccount?: string } export function StripePayment({ @@ -177,8 +242,9 @@ export function StripePayment({ show, options, clientSecret, - locale = 'auto', + locale = "auto", expressPayments = false, + connectedAccount, ...p }: Props): JSX.Element | null { const [isLoaded, setIsLoaded] = useState(false) @@ -192,14 +258,19 @@ export function StripePayment({ } = p useEffect(() => { if (show && publishableKey) { - import('@stripe/stripe-js').then(({ loadStripe }) => { + import("@stripe/stripe-js").then(({ loadStripe }) => { const getStripe = async (): Promise => { - const res = await loadStripe(publishableKey, { - locale - }) + const options = { + locale, + ...(connectedAccount ? { stripeAccount: connectedAccount } : {}), + } satisfies StripeConstructorOptions + const res = await loadStripe(publishableKey, options) if (res != null) { setStripe(res) setIsLoaded(true) + } else { + console.error("Stripe failed to load") + setIsLoaded(false) } } getStripe() @@ -208,11 +279,11 @@ export function StripePayment({ return () => { setIsLoaded(false) } - }, [show, publishableKey]) + }, [show, publishableKey, connectedAccount]) const elementsOptions: StripeElementsOptions = { clientSecret, appearance: { ...defaultAppearance, ...appearance }, - fonts + fonts, } return isLoaded && stripe != null && clientSecret != null ? (
@@ -221,6 +292,7 @@ export function StripePayment({ ) : ( diff --git a/packages/react-components/src/components/utils/BaseInput.tsx b/packages/react-components/src/components/utils/BaseInput.tsx index c7ad04d6..122a6998 100644 --- a/packages/react-components/src/components/utils/BaseInput.tsx +++ b/packages/react-components/src/components/utils/BaseInput.tsx @@ -1,18 +1,18 @@ -import React, { type ForwardRefRenderFunction, type JSX } from 'react'; -import type { BaseInputComponentProps } from '#typings/index' -import Parent from './Parent' +import React, { type ForwardRefRenderFunction, type JSX } from "react" +import type { BaseInputComponentProps } from "#typings/index" +import Parent from "./Parent" export type BaseInputProps = BaseInputComponentProps & - Omit & - Omit + Omit & + Omit const BaseInput: ForwardRefRenderFunction = ( props, - ref + ref, ) => { const { children, ...p } = props const input = - props.type === 'textarea' ? ( + props.type === "textarea" ? (