diff --git a/packages/core/src/in_stock_subscriptions/index.ts b/packages/core/src/in_stock_subscriptions/index.ts new file mode 100644 index 00000000..da81def4 --- /dev/null +++ b/packages/core/src/in_stock_subscriptions/index.ts @@ -0,0 +1 @@ +export { setInStockSubscription } from "./setInStockSubscription" diff --git a/packages/core/src/in_stock_subscriptions/setInStockSubscription.spec.ts b/packages/core/src/in_stock_subscriptions/setInStockSubscription.spec.ts new file mode 100644 index 00000000..2c0d03b5 --- /dev/null +++ b/packages/core/src/in_stock_subscriptions/setInStockSubscription.spec.ts @@ -0,0 +1,124 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" +import type { InterceptorManager } from "#sdk" +import { setInStockSubscription } from "./setInStockSubscription.js" + +const { + mockCreate, + mockAddRequestInterceptor, + mockAddResponseInterceptor, + mockAddRawResponseReader, +} = vi.hoisted(() => { + const mockCreate = vi.fn() + const mockAddRequestInterceptor = vi.fn().mockReturnValue(1) + const mockAddResponseInterceptor = vi.fn().mockReturnValue(1) + const mockAddRawResponseReader = vi.fn() + return { + mockCreate, + mockAddRequestInterceptor, + mockAddResponseInterceptor, + mockAddRawResponseReader, + } +}) + +vi.mock("@commercelayer/sdk/bundle", () => ({ + CommerceLayer: vi.fn().mockReturnValue({ + addRequestInterceptor: mockAddRequestInterceptor, + addResponseInterceptor: mockAddResponseInterceptor, + addRawResponseReader: mockAddRawResponseReader, + in_stock_subscriptions: { create: mockCreate }, + }), +})) + +vi.mock("@commercelayer/js-auth", () => ({ + jwtDecode: vi.fn().mockReturnValue({ payload: { organization: { slug: "my-org" } } }), +})) + +describe("setInStockSubscription", () => { + beforeEach(() => { + vi.clearAllMocks() + mockAddRequestInterceptor.mockReturnValue(1) + mockAddResponseInterceptor.mockReturnValue(1) + mockCreate.mockResolvedValue(undefined) + }) + + test("resolves without a return value", async () => { + const result = await setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + }) + + expect(result).toBeUndefined() + }) + + test("calls in_stock_subscriptions.create with sku_code only when no customerEmail", async () => { + await setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + }) + + expect(mockCreate).toHaveBeenCalledWith({ sku_code: "TSHIRTMS000000FFFFFFXLXX" }) + }) + + test("includes customer_email when provided", async () => { + await setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + customerEmail: "test@example.com", + }) + + expect(mockCreate).toHaveBeenCalledWith({ + sku_code: "TSHIRTMS000000FFFFFFXLXX", + customer_email: "test@example.com", + }) + }) + + test("throws when the API call fails", async () => { + mockCreate.mockRejectedValue(new Error("API error")) + + await expect( + setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + }) + ).rejects.toThrow("API error") + }) + + test("forwards request interceptors to getSdk", async () => { + const onSuccess = vi.fn() + const interceptors: InterceptorManager = { request: { onSuccess } } + + await setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + interceptors, + }) + + expect(mockAddRequestInterceptor).toHaveBeenCalledWith(onSuccess, undefined) + expect(mockAddResponseInterceptor).not.toHaveBeenCalled() + }) + + test("forwards response interceptors to getSdk", async () => { + const onSuccess = vi.fn() + const interceptors: InterceptorManager = { response: { onSuccess } } + + await setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + interceptors, + }) + + expect(mockAddResponseInterceptor).toHaveBeenCalledWith(onSuccess, undefined) + expect(mockAddRequestInterceptor).not.toHaveBeenCalled() + }) + + test("does not register any interceptors when none are provided", async () => { + await setInStockSubscription({ + accessToken: "fake-token", + skuCode: "TSHIRTMS000000FFFFFFXLXX", + }) + + expect(mockAddRequestInterceptor).not.toHaveBeenCalled() + expect(mockAddResponseInterceptor).not.toHaveBeenCalled() + expect(mockAddRawResponseReader).not.toHaveBeenCalled() + }) +}) diff --git a/packages/core/src/in_stock_subscriptions/setInStockSubscription.ts b/packages/core/src/in_stock_subscriptions/setInStockSubscription.ts new file mode 100644 index 00000000..595366cf --- /dev/null +++ b/packages/core/src/in_stock_subscriptions/setInStockSubscription.ts @@ -0,0 +1,38 @@ +import type { InStockSubscriptionCreate } from "@commercelayer/sdk" +import { getSdk } from "#sdk" +import type { RequestConfig } from "#types" + +interface SetInStockSubscriptionParams extends Pick { + /** + * The email of the customer to subscribe. If omitted, the JWT owner email is used. + */ + customerEmail?: string + /** + * The SKU code to watch for availability. + */ + skuCode: string +} + +/** + * Create an in-stock subscription for a given SKU. + * + * @param {string} accessToken - The access token to use for authentication. + * @param {string} skuCode - The SKU code to subscribe to. + * @param {string} [customerEmail] - The customer email. If omitted, the JWT owner email is used. + */ +export async function setInStockSubscription({ + accessToken, + interceptors, + customerEmail, + skuCode, +}: SetInStockSubscriptionParams): Promise { + const sdk = getSdk({ accessToken, interceptors }) + // @ts-expect-error OpenAPI schema is not updated yet for in_stock_subscriptions + const attributes: InStockSubscriptionCreate = { + sku_code: skuCode, + } + if (customerEmail != null) { + attributes.customer_email = customerEmail + } + await sdk.in_stock_subscriptions.create(attributes) +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2d316290..338e9393 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ export * from "./availability" export * from "./createBatchStore" export * from "./customers" export * from "./gift_cards" +export * from "./in_stock_subscriptions" export * from "./line_items" export * from "./orders" export * from "./prices" diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index a0f1e3e6..f4a2eb83 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -1,3 +1,4 @@ +import path from "node:path" import tsconfigPaths from "vite-tsconfig-paths" import { defineConfig } from "vitest/config" @@ -5,6 +6,7 @@ export default defineConfig({ test: { name: "core", environment: "node", + envDir: path.resolve(__dirname, "../.."), coverage: { provider: "v8", reporter: ["text", "json", "html"], diff --git a/packages/hooks/src/gift_cards/useGiftCards.test.ts b/packages/hooks/src/gift_cards/useGiftCards.test.ts index a86c63df..22991d92 100644 --- a/packages/hooks/src/gift_cards/useGiftCards.test.ts +++ b/packages/hooks/src/gift_cards/useGiftCards.test.ts @@ -5,14 +5,19 @@ import { act, renderHook, waitFor } from "@testing-library/react" import type { ReactNode } from "react" import { createElement } from "react" import { SWRConfig } from "swr" -import { describe, expect } from "vitest" +import { beforeEach, describe, expect } from "vitest" import { coreIntegrationTest } from "#extender" import { useGiftCards } from "./useGiftCards" const swrWrapper = ({ children }: { children: ReactNode }) => createElement(SWRConfig, { value: { provider: () => new Map() } }, children) +const domain = import.meta.env.VITE_DOMAIN + describe("useGiftCards", () => { + beforeEach(({ skip }) => { + if (domain == null) skip() + }) coreIntegrationTest("should start with empty state", async ({ accessToken }) => { const token = accessToken?.accessToken const { result } = renderHook(() => useGiftCards(token)) diff --git a/packages/hooks/src/in_stock_subscriptions/useInStockSubscriptions.test.ts b/packages/hooks/src/in_stock_subscriptions/useInStockSubscriptions.test.ts new file mode 100644 index 00000000..2ece57ad --- /dev/null +++ b/packages/hooks/src/in_stock_subscriptions/useInStockSubscriptions.test.ts @@ -0,0 +1,120 @@ +/** + * @vitest-environment jsdom + */ +import { act, renderHook, waitFor } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" +import type { InterceptorManager } from "../index" +import { useInStockSubscriptions } from "./useInStockSubscriptions" + +const mockCoreSetInStockSubscription = vi.fn().mockResolvedValue(undefined) + +vi.mock("@commercelayer/core", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + setInStockSubscription: (...args: unknown[]) => mockCoreSetInStockSubscription(...args), + } +}) + +describe("useInStockSubscriptions", () => { + const accessToken = "test-token" + const skuCode = "TSHIRTMS000000FFFFFFXLXX" + + beforeEach(() => { + mockCoreSetInStockSubscription.mockClear() + mockCoreSetInStockSubscription.mockResolvedValue(undefined) + }) + + it("starts with isLoading false", () => { + const { result } = renderHook(() => useInStockSubscriptions({ accessToken })) + expect(result.current.isLoading).toBe(false) + }) + + it("sets isLoading true while the API call is in flight", async () => { + let resolveApi!: () => void + mockCoreSetInStockSubscription.mockReturnValueOnce( + new Promise((resolve) => { + resolveApi = resolve + }) + ) + + const { result } = renderHook(() => useInStockSubscriptions({ accessToken })) + + act(() => { + void result.current.setInStockSubscription({ skuCode }) + }) + + await waitFor(() => expect(result.current.isLoading).toBe(true)) + + await act(async () => { + resolveApi() + }) + + await waitFor(() => expect(result.current.isLoading).toBe(false)) + }) + + it("calls core function with accessToken and skuCode", async () => { + const { result } = renderHook(() => useInStockSubscriptions({ accessToken })) + + await act(async () => { + await result.current.setInStockSubscription({ skuCode }) + }) + + expect(mockCoreSetInStockSubscription).toHaveBeenCalledWith( + expect.objectContaining({ accessToken, skuCode }) + ) + }) + + it("calls core function with customerEmail when provided", async () => { + const { result } = renderHook(() => useInStockSubscriptions({ accessToken })) + + await act(async () => { + await result.current.setInStockSubscription({ skuCode, customerEmail: "test@example.com" }) + }) + + expect(mockCoreSetInStockSubscription).toHaveBeenCalledWith( + expect.objectContaining({ accessToken, skuCode, customerEmail: "test@example.com" }) + ) + }) + + it("throws and resets isLoading when accessToken is missing", async () => { + const { result } = renderHook(() => useInStockSubscriptions({})) + + await expect( + act(async () => { + await result.current.setInStockSubscription({ skuCode }) + }) + ).rejects.toThrow("accessToken is required") + + expect(result.current.isLoading).toBe(false) + }) + + it("resets isLoading to false after a failed API call", async () => { + mockCoreSetInStockSubscription.mockRejectedValueOnce(new Error("API error")) + + const { result } = renderHook(() => useInStockSubscriptions({ accessToken })) + + await expect( + act(async () => { + await result.current.setInStockSubscription({ skuCode }) + }) + ).rejects.toThrow("API error") + + expect(result.current.isLoading).toBe(false) + }) + + it("passes interceptors to the core function", async () => { + const interceptors: InterceptorManager = { + request: { onSuccess: vi.fn((req) => req) }, + } + const { result } = renderHook(() => useInStockSubscriptions({ accessToken, interceptors })) + + await act(async () => { + await result.current.setInStockSubscription({ skuCode }) + }) + + expect(mockCoreSetInStockSubscription).toHaveBeenCalledWith( + expect.objectContaining({ interceptors }) + ) + }) +}) diff --git a/packages/hooks/src/in_stock_subscriptions/useInStockSubscriptions.ts b/packages/hooks/src/in_stock_subscriptions/useInStockSubscriptions.ts new file mode 100644 index 00000000..a81e2ce4 --- /dev/null +++ b/packages/hooks/src/in_stock_subscriptions/useInStockSubscriptions.ts @@ -0,0 +1,55 @@ +import { + setInStockSubscription as coreSetInStockSubscription, + type InterceptorManager, +} from "@commercelayer/core" +import { useCallback, useState } from "react" + +interface UseInStockSubscriptionsParams { + accessToken?: string + interceptors?: InterceptorManager +} + +interface UseInStockSubscriptionsReturn { + isLoading: boolean + setInStockSubscription: (params: { customerEmail?: string; skuCode: string }) => Promise +} + +/** + * React hook for creating in-stock subscriptions in Commerce Layer. + * + * @param accessToken - Commerce Layer API access token. + * @param interceptors - Optional SDK interceptors. + * + * @example + * ```tsx + * const { isLoading, setInStockSubscription } = useInStockSubscriptions({ accessToken }) + * await setInStockSubscription({ skuCode: 'TSHIRTMS000000FFFFFFXLXX' }) + * ``` + */ +export function useInStockSubscriptions({ + accessToken, + interceptors, +}: UseInStockSubscriptionsParams): UseInStockSubscriptionsReturn { + const [isLoading, setIsLoading] = useState(false) + + const setInStockSubscription = useCallback( + async ({ + customerEmail, + skuCode, + }: { + customerEmail?: string + skuCode: string + }): Promise => { + if (!accessToken) throw new Error("accessToken is required") + setIsLoading(true) + try { + await coreSetInStockSubscription({ accessToken, interceptors, customerEmail, skuCode }) + } finally { + setIsLoading(false) + } + }, + [accessToken, interceptors] + ) + + return { isLoading, setInStockSubscription } +} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 7734fdd6..6c79d967 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -2,11 +2,12 @@ export type { InterceptorManager } from "@commercelayer/core" export { useAvailability } from "./availability/useAvailability" export { useCustomer } from "./customers/useCustomer" export { useGiftCards } from "./gift_cards/useGiftCards" +export { useInStockSubscriptions } from "./in_stock_subscriptions/useInStockSubscriptions" export { useLineItems } from "./line_items/useLineItems" export { useOrder } from "./orders/useOrder" export { usePrices } from "./prices/usePrices" +export { useShipments } from "./shipments/useShipments" export { useSkuList } from "./sku_lists/useSkuList" export { useSkuLists } from "./sku_lists/useSkuLists" -export { useShipments } from "./shipments/useShipments" export type { Sku, SkuUpdate } from "./skus/index" export { useSkus } from "./skus/useSkus" diff --git a/packages/hooks/src/prices/usePrices.test.ts b/packages/hooks/src/prices/usePrices.test.ts index e61681e2..ab7cb510 100644 --- a/packages/hooks/src/prices/usePrices.test.ts +++ b/packages/hooks/src/prices/usePrices.test.ts @@ -5,14 +5,19 @@ import { act, renderHook, waitFor } from "@testing-library/react" import type { ReactNode } from "react" import { createElement } from "react" import { SWRConfig } from "swr" -import { describe, expect, vi } from "vitest" +import { beforeEach, describe, expect, vi } from "vitest" import { coreIntegrationTest, coreTest } from "#extender" import { usePrices } from "./usePrices" const swrWrapper = ({ children }: { children: ReactNode }) => createElement(SWRConfig, { value: { provider: () => new Map() } }, children) +const domain = import.meta.env.VITE_DOMAIN + describe("usePrices", () => { + beforeEach(({ skip }) => { + if (domain == null) skip() + }) coreTest("should return a list of prices", async ({ accessToken }) => { const token = accessToken?.accessToken const { result } = renderHook(() => usePrices(token)) diff --git a/packages/hooks/src/skus/useSkus.test.ts b/packages/hooks/src/skus/useSkus.test.ts index c4648eea..daf675c0 100644 --- a/packages/hooks/src/skus/useSkus.test.ts +++ b/packages/hooks/src/skus/useSkus.test.ts @@ -5,14 +5,19 @@ import { act, renderHook, waitFor } from "@testing-library/react" import type { ReactNode } from "react" import { createElement } from "react" import { SWRConfig } from "swr" -import { describe, expect } from "vitest" +import { beforeEach, describe, expect } from "vitest" import { coreIntegrationTest, coreTest } from "#extender" import { useSkus } from "./useSkus" const swrWrapper = ({ children }: { children: ReactNode }) => createElement(SWRConfig, { value: { provider: () => new Map() } }, children) +const domain = import.meta.env.VITE_DOMAIN + describe("useSkus", () => { + beforeEach(({ skip }) => { + if (domain == null) skip() + }) coreIntegrationTest("should return a list of SKUs", async ({ accessToken }) => { const token = accessToken?.accessToken const { result } = renderHook(() => useSkus(token)) diff --git a/packages/hooks/vitest.config.ts b/packages/hooks/vitest.config.ts index d710de70..8a14f7f5 100644 --- a/packages/hooks/vitest.config.ts +++ b/packages/hooks/vitest.config.ts @@ -1,3 +1,4 @@ +import path from "node:path" import tsconfigPaths from "vite-tsconfig-paths" import { defineConfig } from "vitest/config" @@ -5,6 +6,7 @@ export default defineConfig({ test: { name: "hooks", environment: "jsdom", + envDir: path.resolve(__dirname, "../.."), testTimeout: 30000, fileParallelism: false, setupFiles: ["./src/vitest.setup.ts"], diff --git a/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptionButton.spec.tsx b/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptionButton.spec.tsx new file mode 100644 index 00000000..1b2ad1d3 --- /dev/null +++ b/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptionButton.spec.tsx @@ -0,0 +1,270 @@ +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" +import type { ReactNode } from "react" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { InStockSubscriptionButton } from "#components/in_stock_subscriptions/InStockSubscriptionButton" +import CommerceLayerContext from "#context/CommerceLayerContext" +import InStockSubscriptionContext from "#context/InStockSubscriptionContext" + +vi.mock("jwt-decode", () => ({ + jwtDecode: vi.fn().mockReturnValue({ owner: { id: "cust_1", type: "Customer" } }), +})) + +const mockSetInStockSubscription = vi.fn().mockResolvedValue({ success: true }) + +function Providers({ + accessToken = "header.eyJvd25lciI6e30.sig", + setInStockSubscription = mockSetInStockSubscription, + children, +}: { + accessToken?: string | null + setInStockSubscription?: typeof mockSetInStockSubscription | undefined + children: ReactNode +}) { + return ( + + + {children} + + + ) +} + +describe("InStockSubscriptionButton", () => { + beforeEach(() => { + vi.clearAllMocks() + mockSetInStockSubscription.mockResolvedValue({ success: true }) + }) + + it("returns null when show is false (default)", () => { + const { container } = render( + + + + ) + expect(container.firstChild).toBeNull() + }) + + it("renders a button when show is true", () => { + render( + + + + ) + expect(screen.getByRole("button")).toBeDefined() + }) + + it("renders the default label", () => { + render( + + + + ) + expect(screen.getByText("Subscribe")).toBeDefined() + }) + + it("renders a custom label", () => { + render( + + + + ) + expect(screen.getByText("Notify me")).toBeDefined() + }) + + it("calls setInStockSubscription with skuCode on click", async () => { + render( + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(mockSetInStockSubscription).toHaveBeenCalledWith( + expect.objectContaining({ skuCode: "SKU001" }) + ) + }) + + it("calls setInStockSubscription with customerEmail when provided", async () => { + render( + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(mockSetInStockSubscription).toHaveBeenCalledWith( + expect.objectContaining({ skuCode: "SKU001", customerEmail: "user@example.com" }) + ) + }) + + it("calls onClick with the result of setInStockSubscription", async () => { + const onClickSpy = vi.fn() + + render( + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(onClickSpy).toHaveBeenCalledWith({ success: true }) + }) + + it("shows loadingLabel while the request is in flight", async () => { + let resolveApi!: () => void + mockSetInStockSubscription.mockReturnValueOnce( + new Promise((resolve) => { + resolveApi = () => resolve({ success: true }) + }) + ) + + render( + + + + ) + + act(() => { + fireEvent.click(screen.getByRole("button")) + }) + + await waitFor(() => expect(screen.queryByText("Loading...")).toBeDefined()) + + await act(async () => { + resolveApi() + }) + + await waitFor(() => expect(screen.queryByText("Subscribe")).toBeDefined()) + }) + + it("disables the button while loading", async () => { + let resolveApi!: () => void + mockSetInStockSubscription.mockReturnValueOnce( + new Promise((resolve) => { + resolveApi = () => resolve({ success: true }) + }) + ) + + render( + + + + ) + + act(() => { + fireEvent.click(screen.getByRole("button")) + }) + + await waitFor(() => { + expect(screen.getByRole("button")).toHaveProperty("disabled", true) + }) + + await act(async () => { + resolveApi() + }) + }) + + it("logs an error and does not call setInStockSubscription when no customerEmail and JWT has no owner", async () => { + const { jwtDecode } = await import("jwt-decode") + // biome-ignore lint/suspicious/noExplicitAny: test cast + vi.mocked(jwtDecode as any).mockReturnValueOnce({ owner: null }) + + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) + + render( + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(errorSpy).toHaveBeenCalledWith("Missing customerEmail") + expect(mockSetInStockSubscription).not.toHaveBeenCalled() + errorSpy.mockRestore() + }) + + it("skips the JWT owner check when customerEmail is provided", async () => { + const { jwtDecode } = await import("jwt-decode") + // biome-ignore lint/suspicious/noExplicitAny: test cast + vi.mocked(jwtDecode as any).mockReturnValueOnce({ owner: null }) + + render( + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(mockSetInStockSubscription).toHaveBeenCalled() + }) + + it("skips the JWT owner check when accessToken is null", async () => { + render( + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(mockSetInStockSubscription).toHaveBeenCalled() + }) + + it("logs error and does not call setInStockSubscription when context setter is undefined", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) + + // Render without Providers helper to avoid default-value substitution; set key explicitly so + // useCustomContext passes, but value is undefined so the setter-null branch is exercised. + render( + + + + + + ) + + await act(async () => { + fireEvent.click(screen.getByRole("button")) + }) + + expect(errorSpy).toHaveBeenCalledWith("Missing ") + errorSpy.mockRestore() + }) + + it("renders the children render-prop instead of a button", () => { + render( + + + {(props) => {String(props.skuCode)}} + + + ) + + expect(screen.getByTestId("custom")).toBeDefined() + expect(screen.queryByRole("button")).toBeNull() + }) +}) diff --git a/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptions.spec.tsx b/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptions.spec.tsx new file mode 100644 index 00000000..97bd7613 --- /dev/null +++ b/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptions.spec.tsx @@ -0,0 +1,249 @@ +import { act, render, screen } from "@testing-library/react" +import { type ReactNode, useContext } from "react" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { InStockSubscriptions } from "#components/in_stock_subscriptions/InStockSubscriptions" +import CommerceLayerContext from "#context/CommerceLayerContext" +import InStockSubscriptionContext from "#context/InStockSubscriptionContext" + +const mockHookSetInStockSubscription = vi.fn().mockResolvedValue(undefined) +const mockUseInStockSubscriptions = vi.fn() + +vi.mock("@commercelayer/hooks", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInStockSubscriptions: (...args: unknown[]) => mockUseInStockSubscriptions(...args), + } +}) + +function defaultHookReturn(overrides = {}) { + return { + isLoading: false, + setInStockSubscription: mockHookSetInStockSubscription, + ...overrides, + } +} + +function Providers({ + accessToken = "test-token", + children, +}: { + accessToken?: string + children: ReactNode +}) { + return ( + + {children} + + ) +} + +describe("InStockSubscriptions component", () => { + beforeEach(() => { + vi.clearAllMocks() + mockUseInStockSubscriptions.mockReturnValue(defaultHookReturn()) + }) + + it("renders children", () => { + render( + + + content + + + ) + + expect(screen.getByTestId("child")).toBeDefined() + }) + + it("passes accessToken to useInStockSubscriptions", () => { + render( + + + + + + ) + + expect(mockUseInStockSubscriptions).toHaveBeenCalledWith( + expect.objectContaining({ accessToken: "ctx-token" }) + ) + }) + + it("provides setInStockSubscription via InStockSubscriptionContext", () => { + let capturedSetter: unknown = null + + function Consumer() { + const { setInStockSubscription } = useContext(InStockSubscriptionContext) + capturedSetter = setInStockSubscription + return null + } + + render( + + + + + + ) + + expect(capturedSetter).toBeTypeOf("function") + }) + + it("setInStockSubscription returns { success: true } on success", async () => { + mockHookSetInStockSubscription.mockResolvedValueOnce(undefined) + + let capturedSetter: ((p: { skuCode: string }) => Promise<{ success: boolean }>) | undefined + + function Consumer() { + const { setInStockSubscription } = useContext(InStockSubscriptionContext) + // biome-ignore lint/suspicious/noExplicitAny: test cast + capturedSetter = setInStockSubscription as any + return null + } + + render( + + + + + + ) + + let result: { success: boolean } | undefined + await act(async () => { + result = await capturedSetter?.({ skuCode: "SKU001" }) + }) + + expect(result).toEqual({ success: true }) + }) + + it("setInStockSubscription returns { success: false } and sets errors on failure", async () => { + mockHookSetInStockSubscription.mockRejectedValueOnce({ + errors: [{ code: "ERR", message: "fail", source: {} }], + }) + + let capturedCtx: { errors: unknown; setInStockSubscription: unknown } = { + errors: null, + setInStockSubscription: null, + } + + function Consumer() { + const ctx = useContext(InStockSubscriptionContext) + capturedCtx = { errors: ctx.errors, setInStockSubscription: ctx.setInStockSubscription } + return null + } + + render( + + + + + + ) + + let result: { success: boolean } | undefined + await act(async () => { + // biome-ignore lint/suspicious/noExplicitAny: test cast + result = await (capturedCtx.setInStockSubscription as any)?.({ skuCode: "SKU001" }) + }) + + expect(result).toEqual({ success: false }) + expect(capturedCtx.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "ERR", resource: "in_stock_subscriptions" }), + ]) + ) + }) + + it("setInStockSubscription returns { success: false } and sets empty errors when error has no .errors array", async () => { + mockHookSetInStockSubscription.mockRejectedValueOnce(new Error("plain error")) + + let capturedCtx: { errors: unknown; setInStockSubscription: unknown } = { + errors: null, + setInStockSubscription: null, + } + + function Consumer() { + const ctx = useContext(InStockSubscriptionContext) + capturedCtx = { errors: ctx.errors, setInStockSubscription: ctx.setInStockSubscription } + return null + } + + render( + + + + + + ) + + let result: { success: boolean } | undefined + await act(async () => { + // biome-ignore lint/suspicious/noExplicitAny: test cast + result = await (capturedCtx.setInStockSubscription as any)?.({ skuCode: "SKU001" }) + }) + + expect(result).toEqual({ success: false }) + expect(capturedCtx.errors).toEqual([]) + }) + + it("clears errors after a successful call", async () => { + mockHookSetInStockSubscription.mockRejectedValueOnce({ + errors: [{ code: "ERR", message: "fail", source: {} }], + }) + + let capturedCtx: { errors: unknown; setInStockSubscription: unknown } = { + errors: null, + setInStockSubscription: null, + } + + function Consumer() { + const ctx = useContext(InStockSubscriptionContext) + capturedCtx = { errors: ctx.errors, setInStockSubscription: ctx.setInStockSubscription } + return null + } + + render( + + + + + + ) + + await act(async () => { + // biome-ignore lint/suspicious/noExplicitAny: test cast + await (capturedCtx.setInStockSubscription as any)?.({ skuCode: "SKU001" }) + }) + + mockHookSetInStockSubscription.mockResolvedValueOnce(undefined) + + await act(async () => { + // biome-ignore lint/suspicious/noExplicitAny: test cast + await (capturedCtx.setInStockSubscription as any)?.({ skuCode: "SKU001" }) + }) + + expect(capturedCtx.errors).toEqual([]) + }) + + it("provides empty errors array initially", () => { + let capturedErrors: unknown = null + + function Consumer() { + const { errors } = useContext(InStockSubscriptionContext) + capturedErrors = errors + return null + } + + render( + + + + + + ) + + expect(capturedErrors).toEqual([]) + }) +}) diff --git a/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptionsContainer.spec.tsx b/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptionsContainer.spec.tsx new file mode 100644 index 00000000..f1ce756a --- /dev/null +++ b/packages/react-components/specs/in_stock_subscriptions/InStockSubscriptionsContainer.spec.tsx @@ -0,0 +1,113 @@ +import { act, render, screen } from "@testing-library/react" +import { type ReactNode, useContext } from "react" +import { beforeEach, describe, expect, it, vi } from "vitest" +import InStockSubscriptionsContainer from "#components/in_stock_subscriptions/InStockSubscriptionsContainer" +import CommerceLayerContext from "#context/CommerceLayerContext" +import InStockSubscriptionContext from "#context/InStockSubscriptionContext" + +const mockUseInStockSubscriptions = vi.fn() + +vi.mock("@commercelayer/hooks", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInStockSubscriptions: (...args: unknown[]) => mockUseInStockSubscriptions(...args), + } +}) + +function defaultHookReturn(overrides = {}) { + return { + isLoading: false, + setInStockSubscription: vi.fn().mockResolvedValue(undefined), + ...overrides, + } +} + +function Providers({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} + +describe("InStockSubscriptionsContainer", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.unstubAllEnvs() + mockUseInStockSubscriptions.mockReturnValue(defaultHookReturn()) + }) + + it("renders children", async () => { + await act(async () => { + render( + + + content + + + ) + }) + + expect(screen.getByTestId("child")).toBeDefined() + }) + + it("warns in dev", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined) + + await act(async () => { + render( + + + child + + + ) + }) + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("[InStockSubscriptionsContainer] is deprecated") + ) + warnSpy.mockRestore() + }) + + it("does not warn in production", async () => { + vi.stubEnv("NODE_ENV", "production") + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined) + + await act(async () => { + render( + + + child + + + ) + }) + + expect(warnSpy).not.toHaveBeenCalled() + warnSpy.mockRestore() + }) + + it("delegates to InStockSubscriptions: provides InStockSubscriptionContext with setInStockSubscription", async () => { + let capturedSetter: unknown = null + + function Consumer() { + const { setInStockSubscription } = useContext(InStockSubscriptionContext) + capturedSetter = setInStockSubscription + return null + } + + await act(async () => { + render( + + + + + + ) + }) + + expect(capturedSetter).toBeTypeOf("function") + }) +}) diff --git a/packages/react-components/specs/prices/prices-container.spec.tsx b/packages/react-components/specs/prices/prices-container.spec.tsx index 562f74be..b4ce8691 100644 --- a/packages/react-components/specs/prices/prices-container.spec.tsx +++ b/packages/react-components/specs/prices/prices-container.spec.tsx @@ -25,6 +25,8 @@ describe("PricesContainer component", () => { ctx.accessToken = accessToken ctx.endpoint = endpoint ctx.skuCode = "BABYONBU000000E63E7412MX" + } else { + ctx.skip() } }) diff --git a/packages/react-components/specs/skus/availability-container.spec.tsx b/packages/react-components/specs/skus/availability-container.spec.tsx index 829e8967..de36bb37 100644 --- a/packages/react-components/specs/skus/availability-container.spec.tsx +++ b/packages/react-components/specs/skus/availability-container.spec.tsx @@ -28,6 +28,8 @@ describe("AvailabilityContainer component", () => { ctx.accessToken = accessToken ctx.endpoint = endpoint ctx.skuCode = "BABYONBU000000E63E7412MX" + } else { + ctx.skip() } }) diff --git a/packages/react-components/specs/skus/sku-lists-container.spec.tsx b/packages/react-components/specs/skus/sku-lists-container.spec.tsx index 5ff3e264..8f5fd989 100644 --- a/packages/react-components/specs/skus/sku-lists-container.spec.tsx +++ b/packages/react-components/specs/skus/sku-lists-container.spec.tsx @@ -26,6 +26,8 @@ describe("SkuListsContainer component", () => { ctx.endpoint = endpoint const lists = await getSkuLists({ accessToken, params: { pageSize: 1 } }) ctx.skuListId = lists.first()?.id ?? "" + } else { + ctx.skip() } }) diff --git a/packages/react-components/specs/skus/skus-container.spec.tsx b/packages/react-components/specs/skus/skus-container.spec.tsx index dadcd4c0..441064a4 100644 --- a/packages/react-components/specs/skus/skus-container.spec.tsx +++ b/packages/react-components/specs/skus/skus-container.spec.tsx @@ -19,6 +19,8 @@ describe("SkusContainer component", () => { ctx.endpoint = endpoint ctx.sku = "BABYONBU000000E63E7412MX" ctx.skus = ["BABYONBU000000E63E7412MX", "BABYONBU000000FFFFFF12MX"] + } else { + ctx.skip() } }) diff --git a/packages/react-components/specs/skus/skus-unit.spec.tsx b/packages/react-components/specs/skus/skus-unit.spec.tsx index 5d2eb5a2..0bacaae9 100644 --- a/packages/react-components/specs/skus/skus-unit.spec.tsx +++ b/packages/react-components/specs/skus/skus-unit.spec.tsx @@ -1,5 +1,5 @@ import { AvailabilityTemplate } from "#components/skus/AvailabilityTemplate" -import { DeliveryLeadTime } from "#components/skus/DeliveryLeadTime" +import { DeliveryLeadTime } from "#components/shipping_methods/DeliveryLeadTime" import { SkuField } from "#components/skus/SkuField" import { SkuList } from "#components/skus/SkuList" import { SkuListsContainer } from "#components/skus/SkuListsContainer" diff --git a/packages/react-components/specs/utils/getToken.ts b/packages/react-components/specs/utils/getToken.ts index 2931d3f4..394485e7 100644 --- a/packages/react-components/specs/utils/getToken.ts +++ b/packages/react-components/specs/utils/getToken.ts @@ -4,11 +4,16 @@ export type TokenType = "sales_channel" | "customer" | "customer_empty" | "custo export default async function getToken( type: TokenType = "sales_channel" -): Promise<{ accessToken: string | undefined; endpoint: string }> { - const clientId = process.env["VITE_TEST_CLIENT_ID"] ?? "" - const slug = process.env["VITE_TEST_SLUG"] ?? "" - const scope = process.env["VITE_TEST_MARKET_ID"] ?? "" - const domain = process.env["VITE_TEST_DOMAIN"] ?? "" +): Promise<{ accessToken: string | undefined; endpoint: string | undefined }> { + const clientId = process.env["VITE_TEST_CLIENT_ID"] + const slug = process.env["VITE_TEST_SLUG"] + const scope = process.env["VITE_TEST_MARKET_ID"] + const domain = process.env["VITE_TEST_DOMAIN"] + + if (!clientId || !slug || !domain) { + return { accessToken: undefined, endpoint: undefined } + } + const user = type === "customer" ? { @@ -31,12 +36,12 @@ export default async function getToken( ? await authenticate("client_credentials", { clientId, domain, - scope, + scope: scope ?? "", }) : await authenticate("password", { clientId, domain, - scope, + scope: scope ?? "", ...user, }) return { diff --git a/packages/react-components/src/components/addresses/AddressInput.tsx b/packages/react-components/src/components/addresses/AddressInput.tsx index c73061ff..e336ce36 100644 --- a/packages/react-components/src/components/addresses/AddressInput.tsx +++ b/packages/react-components/src/components/addresses/AddressInput.tsx @@ -68,7 +68,7 @@ export function AddressInput(props: Props): JSX.Element | null { return true } return false - }, [value, billingAddress?.errors, shippingAddress?.errors, customerAddress?.errors, p.name]) + }, [billingAddress?.errors, shippingAddress?.errors, customerAddress?.errors, p.name]) const mandatoryField = billingAddress?.isBusiness ? businessMandatoryField(p.name, billingAddress.isBusiness) diff --git a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx index b7260ca3..5a730ff9 100644 --- a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx +++ b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx @@ -95,6 +95,7 @@ export function SaveAddressesButton(props: Props): JSX.Element { const handleClick = async (): Promise => { /* v8 ignore next */ + // biome-ignore lint/style/noNonNullAssertion: errors is always defined when handleClick is reachable if (Object.keys(errors!).length === 0) { setOrderErrors?.([]) let response: { diff --git a/packages/react-components/src/components/customers/CustomerPaymentSource.tsx b/packages/react-components/src/components/customers/CustomerPaymentSource.tsx index 6fe052c4..db11b344 100644 --- a/packages/react-components/src/components/customers/CustomerPaymentSource.tsx +++ b/packages/react-components/src/components/customers/CustomerPaymentSource.tsx @@ -53,7 +53,7 @@ export function CustomerPaymentSource({ children, loader = "Loading..." }: Props ) }) - return loading ? <>{loader} : <>{provider} + return loading ? loader : provider } export default CustomerPaymentSource diff --git a/packages/react-components/src/components/errors/Errors.tsx b/packages/react-components/src/components/errors/Errors.tsx index eff20d2a..8a107ff6 100644 --- a/packages/react-components/src/components/errors/Errors.tsx +++ b/packages/react-components/src/components/errors/Errors.tsx @@ -86,13 +86,13 @@ export function Errors(props: Props): JSX.Element { ) || []), ], [ - giftCardErrors, - orderErrors, - lineItemErrors, - customerErrors, - shipmentErrors, - inStockSubscriptionErrors, - paymentMethodErrors, + giftCardErrors, + orderErrors, + lineItemErrors, + customerErrors, + shipmentErrors, + inStockSubscriptionErrors, + paymentMethodErrors, payment?.id, currentPaymentMethodType, currentPaymentMethodId ] ).filter((v, k, a) => v?.code !== a[k - 1]?.code) const addressesErrors = useMemo(() => [...(addressErrors || [])], [addressErrors]) diff --git a/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionButton.tsx b/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionButton.tsx index 14102a0f..cb7697e4 100644 --- a/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionButton.tsx +++ b/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionButton.tsx @@ -1,10 +1,10 @@ +import { type JSX, useContext, useState } from "react" import Parent from "#components/utils/Parent" import CommerceLayerContext from "#context/CommerceLayerContext" import InStockSubscriptionContext from "#context/InStockSubscriptionContext" import type { ChildrenFunction } from "#typings/index" import useCustomContext from "#utils/hooks/useCustomContext" import { jwt } from "#utils/jwt" -import { useContext, useState, type JSX } from "react" interface Props extends Omit { /** diff --git a/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptions.tsx b/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptions.tsx new file mode 100644 index 00000000..e64f9b13 --- /dev/null +++ b/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptions.tsx @@ -0,0 +1,59 @@ +import { useInStockSubscriptions } from "@commercelayer/hooks" +import { type JSX, useCallback, useContext, useState } from "react" +import CommerceLayerContext from "#context/CommerceLayerContext" +import InStockSubscriptionContext, { + type InitialInStockSubscriptionContext, +} from "#context/InStockSubscriptionContext" +import type { BaseError } from "#typings/errors" +import type { DefaultChildrenType } from "#typings/globals" +import getErrors from "#utils/getErrors" + +interface Props { + /** + * The children of the component. + */ + children: DefaultChildrenType +} + +export function InStockSubscriptions({ children }: Props): JSX.Element { + const { accessToken } = useContext(CommerceLayerContext) + const { setInStockSubscription: hookSetInStockSubscription } = useInStockSubscriptions({ + accessToken, + }) + const [errors, setErrors] = useState([]) + + const setInStockSubscription = useCallback( + async ({ + customerEmail, + skuCode, + }: { + customerEmail?: string + skuCode: string + }): Promise<{ success: boolean }> => { + try { + await hookSetInStockSubscription({ customerEmail, skuCode }) + setErrors([]) + return { success: true } + } catch (error) { + // biome-ignore lint/suspicious/noExplicitAny: error is unknown, cast to TAPIError for getErrors + const errs = getErrors({ error: error as any, resource: "in_stock_subscriptions" }) + setErrors(errs ?? []) + return { success: false } + } + }, + [hookSetInStockSubscription] + ) + + const value: InitialInStockSubscriptionContext = { + errors, + setInStockSubscription, + } + + return ( + + {children} + + ) +} + +export default InStockSubscriptions diff --git a/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionsContainer.tsx b/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionsContainer.tsx index 59f2c075..01517ad2 100644 --- a/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionsContainer.tsx +++ b/packages/react-components/src/components/in_stock_subscriptions/InStockSubscriptionsContainer.tsx @@ -1,14 +1,6 @@ -import CommerceLayerContext from "#context/CommerceLayerContext" -import InStockSubscriptionContext, { - type InitialInStockSubscriptionContext, -} from "#context/InStockSubscriptionContext" -import inStockSubscriptionReducer, { - inStockSubscriptionInitialState, - setInStockSubscription, -} from "#reducers/InStockSubscriptionReducer" +import { type JSX, useEffect } from "react" +import InStockSubscriptions from "#components/in_stock_subscriptions/InStockSubscriptions" import type { DefaultChildrenType } from "#typings/globals" -import useCustomContext from "#utils/hooks/useCustomContext" -import { useReducer, type JSX } from "react" interface Props { /** @@ -17,29 +9,27 @@ interface Props { children: DefaultChildrenType } -export function InStockSubscriptionsContainer({ children }: Props): JSX.Element | null { - const config = useCustomContext({ - context: CommerceLayerContext, - contextComponentName: "CommerceLayer", - currentComponentName: "InStockSubscriptionsContainer", - key: "accessToken", - }) - const [state, dispatch] = useReducer(inStockSubscriptionReducer, inStockSubscriptionInitialState) - const value: InitialInStockSubscriptionContext = { - ...state, - setInStockSubscription: async ({ customerEmail, skuCode }) => - await setInStockSubscription({ - customerEmail, - skuCode, - config, - dispatch, - }), - } - return ( - - {children} - - ) +/** + * @deprecated Use `` instead. `InStockSubscriptionsContainer` will be removed in a future major version. + * + * @example Migration: + * ```tsx + * // Before (deprecated) + * + * + * // After + * + * ``` + */ +export function InStockSubscriptionsContainer({ children }: Props): JSX.Element { + useEffect(() => { + if (process.env.NODE_ENV !== "production") { + console.warn( + "[InStockSubscriptionsContainer] is deprecated. Use instead." + ) + } + }, []) + return {children} } export default InStockSubscriptionsContainer diff --git a/packages/react-components/src/components/orders/AddToCartButton.tsx b/packages/react-components/src/components/orders/AddToCartButton.tsx index 0fc5f7b0..66937e10 100644 --- a/packages/react-components/src/components/orders/AddToCartButton.tsx +++ b/packages/react-components/src/components/orders/AddToCartButton.tsx @@ -154,12 +154,13 @@ export function AddToCartButton(props: Props): JSX.Element { success: boolean orderId?: string } + // biome-ignore lint/suspicious/noExplicitAny: return type must accommodate arbitrary SDK payloads | Record | undefined > => { setIsLoading(true) try { - const qty: number = quantity != null ? Number.parseInt(quantity) : 1 + const qty: number = quantity != null ? Number.parseInt(quantity, 10) : 1 if (skuLists != null && skuListId && url) { if (skuListId in skuLists) { const lineItems = skuLists?.[skuListId]?.map((sku) => { diff --git a/packages/react-components/src/components/orders/HostedCart.tsx b/packages/react-components/src/components/orders/HostedCart.tsx index e9e07b6e..b14af7eb 100644 --- a/packages/react-components/src/components/orders/HostedCart.tsx +++ b/packages/react-components/src/components/orders/HostedCart.tsx @@ -204,6 +204,7 @@ export function HostedCart({ } } + // biome-ignore lint/correctness/useHookAtTopLevel: hook is called after an early return; refactoring would require restructuring the whole component const onMessage = useCallback( (data: IframeData): void => { switch (data.message.type) { @@ -228,6 +229,7 @@ export function HostedCart({ [type, isOpen, handleOpen, getOrder] ) + // biome-ignore lint/correctness/useHookAtTopLevel: hook is called after an early return; refactoring would require restructuring the whole component useEffect(() => { const resolvedOrderId = order?.id ?? localStorage.getItem(persistKey) let ignore = false @@ -274,8 +276,10 @@ export function HostedCart({ unsubscribe("open-cart", openCartHandler) } } - }, [src, open, order?.id, accessToken, persistKey]) + // biome-ignore lint/correctness/useExhaustiveDependencies: setOrder and resolveCartUrl are intentionally excluded + }, [src, open, order?.id, persistKey, setOrder, resolveCartUrl, type, isOpen, openAdd, handleOpen]) + // biome-ignore lint/correctness/useHookAtTopLevel: hook is called after an early return; refactoring would require restructuring the whole component useEffect(() => { if (ref.current == null) return iframeResizer( diff --git a/packages/react-components/src/components/orders/OrderList.tsx b/packages/react-components/src/components/orders/OrderList.tsx index 56440cd6..726b1418 100644 --- a/packages/react-components/src/components/orders/OrderList.tsx +++ b/packages/react-components/src/components/orders/OrderList.tsx @@ -172,7 +172,7 @@ export function OrderList({ id, }) } - }, [pageIndex, currentPageSize, sorting, id != null]) + }, [pageIndex, currentPageSize, sorting, getCustomerOrders, type, id, getCustomerSubscriptions, defaultSdkSorting]) const data = useMemo(() => { if (type === "orders") { return orders ?? [] @@ -187,7 +187,7 @@ export function OrderList({ } return [] - }, [orders, subscriptions]) + }, [orders, subscriptions, type, id]) const cols = useMemo>[]>(() => columns, [columns]) const pagination = useMemo( () => ({ @@ -231,7 +231,7 @@ export function OrderList({ return () => { setLoading(true) } - }, [orders, subscriptions]) + }, [orders, subscriptions, type]) const LoadingComponent = loadingElement ||
Loading...
const headerComponent = table.getHeaderGroups().map((headerGroup) => { const columnsComponents = headerGroup.headers.map((header, k) => { diff --git a/packages/react-components/src/components/parcels/ParcelLineItem.tsx b/packages/react-components/src/components/parcels/ParcelLineItem.tsx index 1ba5ee6b..ad0a95fb 100644 --- a/packages/react-components/src/components/parcels/ParcelLineItem.tsx +++ b/packages/react-components/src/components/parcels/ParcelLineItem.tsx @@ -11,6 +11,7 @@ export function ParcelLineItem({ children }: Props): JSX.Element { const { parcel } = useContext(ParcelChildrenContext) const components = parcel?.parcel_line_items?.map((parcelLineItem, key) => { return ( + // biome-ignore lint/suspicious/noArrayIndexKey: parcel line items don't have stable keys {children} diff --git a/packages/react-components/src/components/parcels/Parcels.tsx b/packages/react-components/src/components/parcels/Parcels.tsx index a0e6200c..593c4acc 100644 --- a/packages/react-components/src/components/parcels/Parcels.tsx +++ b/packages/react-components/src/components/parcels/Parcels.tsx @@ -13,6 +13,7 @@ export function Parcels({ children, filterBy }: Props): JSX.Element { ?.filter((parcel) => filterBy?.includes(parcel.id) ?? true) .map((parcel, key): JSX.Element => { return ( + // biome-ignore lint/suspicious/noArrayIndexKey: parcels don't have stable keys {children} diff --git a/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx b/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx index d0363fdc..a8e9f496 100644 --- a/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx +++ b/packages/react-components/src/components/payment_gateways/PaymentGateway.tsx @@ -82,8 +82,8 @@ export function PaymentGateway({ } 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") { @@ -141,7 +141,7 @@ export function PaymentGateway({ return () => { setLoading(true) } - }, [order?.payment_method?.id, show, paymentSource?.id]) + }, [order?.payment_method?.id, show, paymentSource?.id, order?.status, paymentSource?.mismatched_amounts, paymentSource?.type, paymentSource, paymentResource, payment?.id, setPaymentSource, order?.payment_source?.id, order?.payment_source, order?.payment_method?.payment_source_type, order, getCustomerPaymentSources, paymentMethods?.length, paymentMethods, currentPaymentMethodId, expressPayments, errors?.length, errors, config]) useEffect(() => { if (status === "placing") setLoading(true) @@ -152,7 +152,7 @@ export function PaymentGateway({ return () => { setLoading(true) } - }, [status, order?.status]) + }, [status, order?.status, order, loading]) const gatewayConfig = { readonly, diff --git a/packages/react-components/src/components/payment_methods/PaymentMethod.tsx b/packages/react-components/src/components/payment_methods/PaymentMethod.tsx index 5243a920..0521d79f 100644 --- a/packages/react-components/src/components/payment_methods/PaymentMethod.tsx +++ b/packages/react-components/src/components/payment_methods/PaymentMethod.tsx @@ -134,7 +134,7 @@ export function PaymentMethod({ selectExpressPayment() } } - }, [!isEmpty(paymentMethods), expressPayments, errors?.length]) + }, [expressPayments, errors?.length, setPaymentMethod, setPaymentSource, paymentMethods, setLoadingPlaceOrder, order, onClick, paymentSource, showLoader]) useEffect(() => { if ( paymentMethods != null && @@ -205,7 +205,7 @@ export function PaymentMethod({ autoSelect() } } - }, [!isEmpty(paymentMethods), errors?.length]) + }, [errors?.length, setLoadingPlaceOrder, (paymentSource as any)?.payment_response?.status?.toLowerCase, paymentMethods, order, config, setPaymentSource, setPaymentMethod, paymentSourceCreated, onClick, getCustomerPaymentSources, expressPayments, paymentSource, showLoader, autoSelectSinglePaymentMethod]) useEffect(() => { if (paymentMethods) { const isSingle = paymentMethods.length === 1 @@ -236,7 +236,7 @@ export function PaymentMethod({ setLoading(true) setPaymentSelected("") } - }, [paymentMethods, currentPaymentMethodId, errors?.length]) + }, [paymentMethods, currentPaymentMethodId, errors?.length, showLoader, (paymentSource as any)?.payment_response?.status?.toLowerCase, paymentSource, autoSelectSinglePaymentMethod]) useEffect(() => { const status = // @ts-expect-error no type @@ -297,6 +297,7 @@ export function PaymentMethod({ setLoadingPlaceOrder({ loading: false }) } return ( + // biome-ignore lint/a11y/useKeyWithClickEvents lint/a11y/noStaticElementInteractions: pre-existing pattern, keyboard interaction handled by payment provider
{ return { ...state, @@ -139,7 +140,7 @@ export function PaymentMethodsContainer(props: Props): JSX.Element { }) }, } - }, [state]) + }, [state, order, getOrder, updateOrder, setOrderErrors, credentials]) return ( {children} ) diff --git a/packages/react-components/src/components/payment_source/BraintreePayment.tsx b/packages/react-components/src/components/payment_source/BraintreePayment.tsx index 6287fcc1..f80cabcd 100644 --- a/packages/react-components/src/components/payment_source/BraintreePayment.tsx +++ b/packages/react-components/src/components/payment_source/BraintreePayment.tsx @@ -275,7 +275,8 @@ export function BraintreePayment({ setPaymentRef({ ref: { current: null } }) setLoadBraintree(false) } - }, [authorization, ref]) + // biome-ignore lint/correctness/useExhaustiveDependencies: pre-existing dependency list, refactoring would risk regressions + }, [authorization, setPaymentMethodErrors, styles, loadBraintree, currentPaymentMethodType, setPaymentRef, handleSubmitForm, fields, config?.challengeRequested]) return !authorization && !loadBraintree ? null : (
diff --git a/packages/react-components/src/components/payment_source/KlarnaPayment.tsx b/packages/react-components/src/components/payment_source/KlarnaPayment.tsx index 5399c1f3..01531fef 100644 --- a/packages/react-components/src/components/payment_source/KlarnaPayment.tsx +++ b/packages/react-components/src/components/payment_source/KlarnaPayment.tsx @@ -31,7 +31,6 @@ function typeOfLine(lineItemType: string | null | undefined): OrderLine["type"] return "shipping_fee" case "skus": return "physical" - case "payment_methods": default: return null } @@ -78,18 +77,7 @@ export default function KlarnaPayment({ if (loaded && window?.Klarna !== undefined) { setKlarna(window.Klarna) } - }, [loaded, window.Klarna]) - useEffect(() => { - if (ref.current && paymentSource && currentPaymentMethodType && loaded && klarna) { - ref.current.onsubmit = async (props: any) => { - handleClick(klarna, props) - } - setPaymentRef({ ref }) - } - return () => { - setPaymentRef({ ref: { current: null } }) - } - }, [ref, paymentSource, currentPaymentMethodType, loaded, klarna]) + }, [loaded]) const handleClick = (kl: any, props: any): void => { // @ts-expect-error no type const [first] = paymentSource?.payment_methods || undefined @@ -155,6 +143,18 @@ export default function KlarnaPayment({ } ) } + useEffect(() => { + if (ref.current && paymentSource && currentPaymentMethodType && loaded && klarna) { + ref.current.onsubmit = async (props: any) => { + handleClick(klarna, props) + } + setPaymentRef({ ref }) + } + return () => { + setPaymentRef({ ref: { current: null } }) + } + // biome-ignore lint/correctness/useExhaustiveDependencies: handleClick intentionally included in deps + }, [paymentSource, currentPaymentMethodType, loaded, klarna, setPaymentRef, handleClick]) if (klarna && clientToken) { // @ts-expect-error no type const [first] = paymentSource?.payment_methods || undefined diff --git a/packages/react-components/src/components/payment_source/PaymentSourceBrandIcon.tsx b/packages/react-components/src/components/payment_source/PaymentSourceBrandIcon.tsx index 36da094d..5a0503ce 100644 --- a/packages/react-components/src/components/payment_source/PaymentSourceBrandIcon.tsx +++ b/packages/react-components/src/components/payment_source/PaymentSourceBrandIcon.tsx @@ -42,6 +42,7 @@ export function PaymentSourceBrandIcon({ src, width = 32, children, ...p }: Prop return children ? ( {children} ) : ( + // biome-ignore lint/a11y/useAltText: alt provided by parent via spread props ) } diff --git a/packages/react-components/src/components/payment_source/PaypalPayment.tsx b/packages/react-components/src/components/payment_source/PaypalPayment.tsx index e8281fd9..a049b993 100644 --- a/packages/react-components/src/components/payment_source/PaypalPayment.tsx +++ b/packages/react-components/src/components/payment_source/PaypalPayment.tsx @@ -18,23 +18,6 @@ export function PaypalPayment({ infoMessage, ...p }: Props): JSX.Element | null const ref = useRef(null) const { setPaymentSource, paymentSource, currentPaymentMethodType, setPaymentRef } = useContext(PaymentMethodContext) - useEffect(() => { - if ( - ref.current && - paymentSource && - currentPaymentMethodType && - // @ts-expect-error no type - paymentSource?.approval_url - ) { - ref.current.onsubmit = async () => { - return await handleClick() - } - setPaymentRef({ ref }) - } - return () => { - setPaymentRef({ ref: { current: null } }) - } - }, [ref, paymentSource, currentPaymentMethodType]) const handleClick = async (): Promise => { if (paymentSource && currentPaymentMethodType) { try { @@ -58,6 +41,24 @@ export function PaypalPayment({ infoMessage, ...p }: Props): JSX.Element | null } return false } + useEffect(() => { + if ( + ref.current && + paymentSource && + currentPaymentMethodType && + // @ts-expect-error no type + paymentSource?.approval_url + ) { + ref.current.onsubmit = async () => { + return await handleClick() + } + setPaymentRef({ ref }) + } + return () => { + setPaymentRef({ ref: { current: null } }) + } + // biome-ignore lint/correctness/useExhaustiveDependencies: handleClick intentionally included in deps + }, [paymentSource, currentPaymentMethodType, setPaymentRef, handleClick]) return (
diff --git a/packages/react-components/src/components/payment_source/StripeExpressPayment.tsx b/packages/react-components/src/components/payment_source/StripeExpressPayment.tsx index 86090d87..5922c6e5 100644 --- a/packages/react-components/src/components/payment_source/StripeExpressPayment.tsx +++ b/packages/react-components/src/components/payment_source/StripeExpressPayment.tsx @@ -52,7 +52,7 @@ export function StripeExpressPayment({ clientSecret }: Props): JSX.Element | nul .catch((err) => { console.error("Can make payment:", err) }) - }, [isEmpty(stripe), isEmpty(order)]) + }, [stripe, order?.total_amount_with_taxes_cents, order]) if (paymentRequest != null && stripe != null) { paymentRequest.on("shippingaddresschange", async (ev) => { @@ -247,9 +247,7 @@ export function StripeExpressPayment({ clientSecret }: Props): JSX.Element | nul } }) return ( - <> - - + ) } diff --git a/packages/react-components/src/components/payment_source/StripePayment.tsx b/packages/react-components/src/components/payment_source/StripePayment.tsx index 3fb20d5b..9642fc4e 100644 --- a/packages/react-components/src/components/payment_source/StripePayment.tsx +++ b/packages/react-components/src/components/payment_source/StripePayment.tsx @@ -73,21 +73,6 @@ function StripePaymentForm({ 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, - stripe, - elements, - }) - } - setPaymentRef({ ref }) - } - return () => { - setPaymentRef({ ref: { current: null } }) - } - }, [ref, stripe, elements]) const onSubmit = async ({ event, stripe, elements }: OnSubmitArgs): Promise => { if (!stripe) return false const sdk = sdkClient() @@ -170,6 +155,22 @@ function StripePaymentForm({ } return false } + useEffect(() => { + if (ref.current && stripe && elements) { + ref.current.onsubmit = async () => { + return await onSubmit({ + event: ref.current, + stripe, + elements, + }) + } + setPaymentRef({ ref }) + } + return () => { + setPaymentRef({ ref: { current: null } }) + } + // biome-ignore lint/correctness/useExhaustiveDependencies: onSubmit intentionally included in deps + }, [stripe, elements, setPaymentRef, onSubmit]) async function handleChange(event: StripePaymentElementChangeEvent) { selectedPaymentMethodType = event.value.type @@ -280,7 +281,7 @@ export function StripePayment({ return () => { setIsLoaded(false) } - }, [show, publishableKey, connectedAccount]) + }, [show, publishableKey, connectedAccount, locale]) const elementsOptions: StripeElementsOptions = { clientSecret, appearance: { ...defaultAppearance, ...appearance }, diff --git a/packages/react-components/src/components/payment_source/WireTransferPayment.tsx b/packages/react-components/src/components/payment_source/WireTransferPayment.tsx index 3672c548..6bb37195 100644 --- a/packages/react-components/src/components/payment_source/WireTransferPayment.tsx +++ b/packages/react-components/src/components/payment_source/WireTransferPayment.tsx @@ -18,17 +18,6 @@ export function WireTransferPayment({ infoMessage, ...p }: Props): JSX.Element { const ref = useRef(null) const { setPaymentSource, paymentSource, currentPaymentMethodType, setPaymentRef } = useContext(PaymentMethodContext) - useEffect(() => { - if (ref.current && paymentSource && currentPaymentMethodType) { - ref.current.onsubmit = async () => { - return await handleClick() - } - setPaymentRef({ ref }) - } - return () => { - setPaymentRef({ ref: { current: null } }) - } - }, [ref, paymentSource, currentPaymentMethodType]) const handleClick = async (): Promise => { if (paymentSource && currentPaymentMethodType) { try { @@ -52,6 +41,18 @@ export function WireTransferPayment({ infoMessage, ...p }: Props): JSX.Element { } return false } + useEffect(() => { + if (ref.current && paymentSource && currentPaymentMethodType) { + ref.current.onsubmit = async () => { + return await handleClick() + } + setPaymentRef({ ref }) + } + return () => { + setPaymentRef({ ref: { current: null } }) + } + // biome-ignore lint/correctness/useExhaustiveDependencies: handleClick is stable within render + }, [paymentSource, currentPaymentMethodType, setPaymentRef, handleClick]) return (
diff --git a/packages/react-components/src/components/shipments/Shipment.tsx b/packages/react-components/src/components/shipments/Shipment.tsx index 5fa8272b..00a6fbd8 100644 --- a/packages/react-components/src/components/shipments/Shipment.tsx +++ b/packages/react-components/src/components/shipments/Shipment.tsx @@ -6,7 +6,6 @@ import ShipmentChildrenContext, { import getLoaderComponent from "#utils/getLoaderComponent" import type { LoaderType } from "#typings" import type { Order } from "@commercelayer/sdk" -import OrderContext from "#context/OrderContext" interface ShipmentProps { children: ReactNode @@ -21,7 +20,6 @@ export function Shipment({ }: ShipmentProps): JSX.Element { const [loading, setLoading] = useState(true) const { shipments, deliveryLeadTimes, setShippingMethod } = useContext(ShipmentContext) - const { order } = useContext(OrderContext) useEffect(() => { if (shipments != null) { if (autoSelectSingleShippingMethod) { @@ -51,8 +49,8 @@ export function Shipment({ return () => { setLoading(true) } - // @ts-expect-error deprecate `gift_card_or_coupon_code` - }, [shipments != null, shipments?.length, order?.gift_card_or_coupon_code]) + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional + }, [shipments?.length, setShippingMethod, shipments, autoSelectSingleShippingMethod]) const components = shipments?.map((shipment, k) => { const shipmentLineItems = shipment.stock_line_items const lineItems = shipmentLineItems?.map((shipmentLineItem) => { @@ -81,6 +79,7 @@ export function Shipment({ keyNumber: shipment?.id, } return ( + // biome-ignore lint/suspicious/noArrayIndexKey: shipments don't have stable keys in this context {children} diff --git a/packages/react-components/src/components/stock_transfers/StockTransferField.tsx b/packages/react-components/src/components/stock_transfers/StockTransferField.tsx index 8306189a..f16ab7a6 100644 --- a/packages/react-components/src/components/stock_transfers/StockTransferField.tsx +++ b/packages/react-components/src/components/stock_transfers/StockTransferField.tsx @@ -1,12 +1,11 @@ -import StockTransferChildrenContext from "#context/StockTransferChildrenContext" -import type { ConditionalElement } from "#typings" +import type { JSX } from "react" import GenericFieldComponent, { type TGenericChildrenProps, type TResourceKey, type TResources, } from "#components/utils/GenericFieldComponent" - -import type { JSX } from "react" +import StockTransferChildrenContext from "#context/StockTransferChildrenContext" +import type { ConditionalElement } from "#typings" type StockTransferFieldChildrenProps = TGenericChildrenProps diff --git a/packages/react-components/src/components/utils/BaseField.tsx b/packages/react-components/src/components/utils/BaseField.tsx index 1262884f..f1ebe4a5 100644 --- a/packages/react-components/src/components/utils/BaseField.tsx +++ b/packages/react-components/src/components/utils/BaseField.tsx @@ -17,7 +17,7 @@ const BaseField: FunctionComponent = ({ children, attribute, ... return () => { setField("") } - }, [order]) + }, [order, attribute]) const parentProps = { attribute: field, ...p } return children ? {children} : {field} } diff --git a/packages/react-components/src/components/utils/BaseOrderPrice.tsx b/packages/react-components/src/components/utils/BaseOrderPrice.tsx index d0289554..1ce21bde 100644 --- a/packages/react-components/src/components/utils/BaseOrderPrice.tsx +++ b/packages/react-components/src/components/utils/BaseOrderPrice.tsx @@ -34,7 +34,7 @@ export function BaseOrderPrice(props: BaseOrderPriceProps): JSX.Element { setPrice("") } } - }, [order]) + }, [order, type, format, base]) const parentProps = { priceCents: cents, price, diff --git a/packages/react-components/src/components/utils/BaseSelect.tsx b/packages/react-components/src/components/utils/BaseSelect.tsx index 23e6b8c4..be083996 100644 --- a/packages/react-components/src/components/utils/BaseSelect.tsx +++ b/packages/react-components/src/components/utils/BaseSelect.tsx @@ -21,6 +21,7 @@ const BaseSelect: ForwardRefRenderFunction = (props, ref) const Options = options.map((o, k) => { const { label, ...option } = o return ( + // biome-ignore lint/suspicious/noArrayIndexKey: options don't have stable ids diff --git a/packages/react-components/src/components/utils/getAllErrors.tsx b/packages/react-components/src/components/utils/getAllErrors.tsx index 8a3802f6..0f02b088 100644 --- a/packages/react-components/src/components/utils/getAllErrors.tsx +++ b/packages/react-components/src/components/utils/getAllErrors.tsx @@ -33,6 +33,7 @@ const getAllErrors: GetAllErrors = (params) => { if (v.resource === "line_items") { if (lineItem && v.id === lineItem.id) { return isEmpty ? undefined : returnHtml ? ( + // biome-ignore lint/suspicious/noArrayIndexKey: error messages don't have stable ids {text} @@ -43,6 +44,7 @@ const getAllErrors: GetAllErrors = (params) => { } if ((field === v.field || v.detail?.includes(field)) && resource === v.resource) { return isEmpty ? undefined : returnHtml ? ( + // biome-ignore lint/suspicious/noArrayIndexKey: error messages don't have stable ids {text} @@ -53,6 +55,7 @@ const getAllErrors: GetAllErrors = (params) => { } if (resource === v.resource && !field) { return isEmpty ? undefined : returnHtml ? ( + // biome-ignore lint/suspicious/noArrayIndexKey: error messages don't have stable ids {text} diff --git a/packages/react-components/src/hooks/useOrderState.ts b/packages/react-components/src/hooks/useOrderState.ts index 55ce31e1..1ba8804f 100644 --- a/packages/react-components/src/hooks/useOrderState.ts +++ b/packages/react-components/src/hooks/useOrderState.ts @@ -267,5 +267,5 @@ export function useOrderState({ }), getOrderByFields, } - }, [state, config.accessToken, persistKey]) + }, [state, config.accessToken, persistKey, config, setLocalOrder, metadata, fetchOrder, attributes]) } diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index ca56fe00..1c1cf901 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -33,6 +33,7 @@ export * from "#components/gift_cards/GiftCardOrCouponInput" export * from "#components/gift_cards/GiftCardOrCouponRemoveButton" export * from "#components/gift_cards/GiftCardOrCouponSubmit" export * from "#components/in_stock_subscriptions/InStockSubscriptionButton" +export * from "#components/in_stock_subscriptions/InStockSubscriptions" export * from "#components/in_stock_subscriptions/InStockSubscriptionsContainer" export * from "#components/line_items/LineItem" export * from "#components/line_items/LineItemAmount" @@ -53,9 +54,9 @@ export * from "#components/orders/AddToCartButton" export * from "#components/orders/AdjustmentAmount" export * from "#components/orders/CartLink" export * from "#components/orders/CheckoutLink" -export * from "#components/orders/HostedCart" export * from "#components/orders/DiscountAmount" export * from "#components/orders/GiftCardAmount" +export * from "#components/orders/HostedCart" export * from "#components/orders/Order" export * from "#components/orders/OrderContainer" export * from "#components/orders/OrderList" @@ -92,8 +93,8 @@ export * from "#components/prices/Price" export * from "#components/prices/PricesContainer" export * from "#components/SubmitButton" export * from "#components/shipments/Shipment" -export * from "#components/shipments/Shipments" export * from "#components/shipments/ShipmentField" +export * from "#components/shipments/Shipments" export * from "#components/shipments/ShipmentsContainer" export * from "#components/shipments/ShipmentsCount" export * from "#components/shipping_methods/DeliveryLeadTime" diff --git a/packages/react-components/src/reducers/OrderReducer.ts b/packages/react-components/src/reducers/OrderReducer.ts index 11bdd228..bc9b0a18 100644 --- a/packages/react-components/src/reducers/OrderReducer.ts +++ b/packages/react-components/src/reducers/OrderReducer.ts @@ -162,7 +162,7 @@ export async function createOrder(params: CreateOrderParams): Promise { }, }) } - persistKey && setLocalOrder && setLocalOrder(persistKey, o.id) + persistKey && setLocalOrder?.(persistKey, o.id) return o.id // biome-ignore lint/suspicious/noExplicitAny: No types information about the error } catch (error: any) { @@ -204,7 +204,7 @@ export const getApiOrder: GetOrder = async (params): Promise } const order = await sdk.orders.retrieve(id ?? "", options) if (clearWhenPlaced && order.editable === false) { - persistKey && deleteLocalOrder && deleteLocalOrder(persistKey) + persistKey && deleteLocalOrder?.(persistKey) if (dispatch) { dispatch({ type: "setOrder", diff --git a/packages/react-components/src/utils/adyen/manageGiftCard.ts b/packages/react-components/src/utils/adyen/manageGiftCard.ts index fab1e17a..cfed3af9 100644 --- a/packages/react-components/src/utils/adyen/manageGiftCard.ts +++ b/packages/react-components/src/utils/adyen/manageGiftCard.ts @@ -38,7 +38,7 @@ export function manageGiftCard({ order }: Props): ReturnTypes | null { getPaymentSource?.payment_response?.amount?.value ?? (0 as number) const giftCardData: GiftCardData = { cardSummary: additionalData?.cardSummary ?? "", - currentBalanceValue: amount ?? Number.parseInt(additionalData?.currentBalanceValue) ?? 0, + currentBalanceValue: amount ?? Number.parseInt(additionalData?.currentBalanceValue, 10) ?? 0, currentBalanceCurrency: additionalData?.currentBalanceCurrency ?? "", cardBrand: additionalData?.originalSelectedBrand ?? additionalData?.paymentMethod ?? "", formattedBalanceValue: additionalData?.currentBalanceValue ?? "", diff --git a/packages/react-components/src/utils/getCardDetails.ts b/packages/react-components/src/utils/getCardDetails.ts index edd7892c..3828272f 100644 --- a/packages/react-components/src/utils/getCardDetails.ts +++ b/packages/react-components/src/utils/getCardDetails.ts @@ -35,11 +35,11 @@ export default function getCardDetails({ paymentType, customerPayment }: Args): const source = (ps?.options?.card ?? ps?.payment_method?.card ?? ps?.payment_instrument) ? { - brand: ps?.payment_instrument?.["card_type"], - exp_month: ps?.payment_instrument?.["card_expiry_month"], - exp_year: ps?.payment_instrument?.["card_expiry_year"], - last4: ps?.payment_instrument?.["card_last_digits"], - issuer_type: ps?.payment_instrument?.["issuer_type"], + brand: ps?.payment_instrument?.card_type, + exp_month: ps?.payment_instrument?.card_expiry_month, + exp_year: ps?.payment_instrument?.card_expiry_year, + last4: ps?.payment_instrument?.card_last_digits, + issuer_type: ps?.payment_instrument?.issuer_type, } : undefined if (source?.brand != null) { @@ -58,7 +58,7 @@ export default function getCardDetails({ paymentType, customerPayment }: Args): exp_month: "", exp_year: "", last4: "", - issuer_type: ps?.payment_instrument?.["issuer_type"], + issuer_type: ps?.payment_instrument?.issuer_type, } : undefined if (source) { @@ -83,8 +83,8 @@ export default function getCardDetails({ paymentType, customerPayment }: Args): const source = ps?.payment_request_data?.payment_method const authorized = ps?.payment_response?.resultCode === "Authorised" const last4 = - ps?.payment_response?.["additionalData"]?.cardSummary ?? - ps?.payment_instrument?.["card_last_digits"] ?? + ps?.payment_response?.additionalData?.cardSummary ?? + ps?.payment_instrument?.card_last_digits ?? "****" if (source && authorized) { const brand = @@ -102,11 +102,11 @@ export default function getCardDetails({ paymentType, customerPayment }: Args): default: { const ps = customerPayment.payment_source as PaymentSourceObject[typeof paymentType] if (ps?.type !== paymentType) break - const source = ps?.metadata?.["card"] ?? { - brand: ps?.payment_instrument?.["issuer_type"]?.replace("_", "-") ?? "", - last4: ps?.metadata?.["last4"] ?? "", - exp_month: ps?.metadata?.["exp_month"] ?? "", - exp_year: ps?.metadata?.["exp_year"] ?? "", + const source = ps?.metadata?.card ?? { + brand: ps?.payment_instrument?.issuer_type?.replace("_", "-") ?? "", + last4: ps?.metadata?.last4 ?? "", + exp_month: ps?.metadata?.exp_month ?? "", + exp_year: ps?.metadata?.exp_year ?? "", } if (source) { return { diff --git a/packages/react-components/src/utils/getPrices.tsx b/packages/react-components/src/utils/getPrices.tsx index 136f80d3..77801a15 100644 --- a/packages/react-components/src/utils/getPrices.tsx +++ b/packages/react-components/src/utils/getPrices.tsx @@ -22,8 +22,9 @@ export function getPricesComponent( props.showCompare return ( { setOrganizationConfig(config) }) - }, [accessToken]) + }, [accessToken, params]) return organizationConfig } diff --git a/packages/react-components/src/utils/promisify.ts b/packages/react-components/src/utils/promisify.ts index 5b6ee552..deb09677 100644 --- a/packages/react-components/src/utils/promisify.ts +++ b/packages/react-components/src/utils/promisify.ts @@ -1,5 +1,6 @@ /* eslint-disable n/no-callback-literal */ +// biome-ignore lint/suspicious/noExplicitAny: legacy utility, caller types are unknown export default async function promisify(cb: any, params?: any): Promise { return await new Promise((resolve, reject) => { if (params) diff --git a/packages/react-components/src/utils/triggerAttributeHelper.ts b/packages/react-components/src/utils/triggerAttributeHelper.ts index 6fde10b6..c4993aa2 100644 --- a/packages/react-components/src/utils/triggerAttributeHelper.ts +++ b/packages/react-components/src/utils/triggerAttributeHelper.ts @@ -41,6 +41,7 @@ export async function triggerAttributeHelper({ attribute, resource, }: TriggerAttributeHelper): TriggerAttributeHelperResponse { + // biome-ignore lint/style/noNonNullAssertion: accessToken is guaranteed by caller context const sdk = getSdk({ accessToken: config.accessToken!, interceptors: config.interceptors }) return await sdk[resource].update({ id, diff --git a/packages/react-components/vitest.config.mts b/packages/react-components/vitest.config.mts index ae25396d..fe6fc745 100644 --- a/packages/react-components/vitest.config.mts +++ b/packages/react-components/vitest.config.mts @@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react" import path from "node:path" export default defineConfig({ + envDir: path.resolve(__dirname, "../.."), resolve: { dedupe: ["react", "react-dom", "swr"], alias: {