diff --git a/packages/react-components/specs/payment_source/adyen-payment.spec.tsx b/packages/react-components/specs/payment_source/adyen-payment.spec.tsx
new file mode 100644
index 00000000..0acd5ae4
--- /dev/null
+++ b/packages/react-components/specs/payment_source/adyen-payment.spec.tsx
@@ -0,0 +1,111 @@
+import type { Order } from "@commercelayer/sdk"
+import { render, waitFor } from "@testing-library/react"
+import type { ReactNode } from "react"
+import { afterEach, describe, expect, it, vi } from "vitest"
+import AdyenPayment from "#components/payment_source/AdyenPayment"
+import CommerceLayerContext from "#context/CommerceLayerContext"
+import CustomerContext from "#context/CustomerContext"
+import OrderContext, { defaultOrderContext } from "#context/OrderContext"
+import PaymentMethodContext, {
+ defaultPaymentMethodContext,
+} from "#context/PaymentMethodContext"
+import PlaceOrderContext, {
+ defaultPlaceOrderContext,
+} from "#context/PlaceOrderContext"
+import type { PaymentMethodState } from "#reducers/PaymentMethodReducer"
+
+const removeMock = vi.fn()
+const mountMock = vi.fn()
+const dropinConstructor = vi.fn()
+
+vi.mock("@adyen/adyen-web/auto", () => {
+ return {
+ AdyenCheckout: vi.fn(async () => ({})),
+ Dropin: class {
+ mount = mountMock.mockReturnThis()
+ remove = removeMock
+ submit = vi.fn()
+ handleAction = vi.fn()
+ constructor(...args: unknown[]) {
+ dropinConstructor(...args)
+ }
+ },
+ }
+})
+
+const order = { id: "order_1" } as unknown as Order
+
+function Providers({
+ status,
+ children,
+}: {
+ status: "standby" | "placing"
+ children: ReactNode
+}): ReactNode {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
+
+describe("AdyenPayment", () => {
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it("destroys the Drop-in on unmount, but not on a status-only re-render", async () => {
+ const { rerender, unmount } = render(
+
+
+ ,
+ )
+
+ await waitFor(() => expect(dropinConstructor).toHaveBeenCalledTimes(1))
+
+ // A declined-payment retry flips status standby -> placing -> standby
+ // while AdyenPayment stays mounted; that must not tear down the Drop-in.
+ rerender(
+
+
+ ,
+ )
+ rerender(
+
+
+ ,
+ )
+
+ expect(removeMock).not.toHaveBeenCalled()
+ expect(dropinConstructor).toHaveBeenCalledTimes(1)
+
+ unmount()
+
+ expect(removeMock).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/react-components/src/components/payment_source/AdyenPayment.tsx b/packages/react-components/src/components/payment_source/AdyenPayment.tsx
index 55473ce5..7d42be1e 100644
--- a/packages/react-components/src/components/payment_source/AdyenPayment.tsx
+++ b/packages/react-components/src/components/payment_source/AdyenPayment.tsx
@@ -702,6 +702,24 @@ export function AdyenPayment({
setLoadAdyen(false)
}
}, [clientKey, ref != null, status, setPaymentMethodErrors != null])
+
+ /**
+ * Destroys the Adyen Drop-in only on true unmount (empty deps), independent
+ * of the effect above: that effect's cleanup also runs on every `status`
+ * flip (e.g. standby -> placing -> standby on a declined retry) while the
+ * component stays mounted, and calling remove() there would tear down and
+ * rebuild the Drop-in mid-flow.
+ */
+ useEffect(() => {
+ return () => {
+ try {
+ dropinRef.current?.remove()
+ } catch (error) {
+ console.error("Adyen drop-in teardown error:", error)
+ }
+ dropinRef.current = null
+ }
+ }, [])
return !clientKey && !loadAdyen && !checkout ? null : (