diff --git a/examples/magento-graphcms/package.json b/examples/magento-graphcms/package.json
index 69cfe46c88f..0d1dab7c024 100644
--- a/examples/magento-graphcms/package.json
+++ b/examples/magento-graphcms/package.json
@@ -52,6 +52,7 @@
"@graphcommerce/magento-customer": "7.1.0-canary.61",
"@graphcommerce/magento-graphql": "7.1.0-canary.61",
"@graphcommerce/magento-newsletter": "7.1.0-canary.61",
+ "@graphcommerce/magento-payment-adyen": "7.1.0-canary.61",
"@graphcommerce/magento-payment-included": "7.1.0-canary.61",
"@graphcommerce/magento-product": "7.1.0-canary.61",
"@graphcommerce/magento-product-bundle": "7.1.0-canary.61",
diff --git a/packages/magento-payment-adyen/README.md b/packages/magento-payment-adyen/README.md
index 3319eec4362..633b790b9e2 100644
--- a/packages/magento-payment-adyen/README.md
+++ b/packages/magento-payment-adyen/README.md
@@ -6,9 +6,6 @@ We currently have 'Alternative Payment Methods' implemented, this means that it
supports all off-site payment methods that Adyen supports. This includes CC,
iDeal, Bancontact, Sofort, etc.
-We do not support on-site credit cards yet. Let us know if you want to have
-this.
-
## Requirements
- Magento Adyen module version 8.5.0 or later
@@ -17,16 +14,12 @@ this.
1. Find current version of your `@graphcommerce/magento-cart-payment-method` in
your package.json.
-2. `yarn add @graphcommerce/magento-payment-adyen@1.2.3` (replace 1.2.3 with the
- version of the step above)
-
+2. `yarn add @marcheygroup/graphcommerce-magento-payment-adyen@^1.2.3` (replace
+ 1.2.3 with the version of the step above)
3. Configure the Adyen module in Magento Admin like you would normally do.
-4. Stores -> Configuration -> Sales -> Payment Methods -> Adyen Payment methods
- -> Headless integration -> Payment Origin URL: `https://www.yourdomain.com`
-5. Stores -> Configuration -> Sales -> Payment Methods -> Adyen Payment methods
- -> Headless integration -> Payment Return URL:
- `https://www.yourdomain.com/checkout/payment?locked=1&adyen=1` (make sure the
- URL's match for your storeview)
+4. Configure the Payment Origin URL Stores -> Configuration -> Sales -> Payment
+ Methods -> Adyen Payment methods -> Headless integration -> Payment Origin
+ URL: `https://www.yourdomain.com`
This package uses GraphCommerce plugin systems, so there is no code modification
required.
@@ -36,6 +29,3 @@ required.
- We don't need to configure the Payment URL's anymore since the 8.3.3 release
https://github.com/Adyen/adyen-magento2/releases/tag/8.3.3, but that isn't
integrated in the frontend yet.
-
-- This package is currently untested inside the GraphCommerce repo, which it
- should, but is used in production for multiple shops.
diff --git a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx
index 7efda0cdeb0..23413e895de 100644
--- a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx
+++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx
@@ -2,30 +2,44 @@ import { Image } from '@graphcommerce/image'
import { PaymentMethodActionCardProps } from '@graphcommerce/magento-cart-payment-method'
import { ActionCard, useIconSvgSize } from '@graphcommerce/next-ui'
import { Trans } from '@lingui/react'
-import { useAdyenPaymentMethod } from '../../hooks/useAdyenPaymentMethod'
+import applepay from './applepay.svg'
+import googlepay from './googlepay.svg'
+import paypal from './paypal.svg'
+import scheme from './scheme.svg'
export function AdyenPaymentActionCard(props: PaymentMethodActionCardProps) {
const { child } = props
const iconSize = useIconSvgSize('large')
- const icon = useAdyenPaymentMethod(child)?.icon
-
+ const icons = {
+ scheme: {
+ image: scheme,
+ },
+ adyen_cc: {
+ image: scheme,
+ },
+ applepay: {
+ image: applepay,
+ },
+ googlepay: {
+ image: googlepay,
+ },
+ paypal: {
+ image: paypal,
+ },
+ }
return (
}
image={
- !!icon?.url &&
- !!icon?.width &&
- !!icon?.height && (
+ !!icons[child]?.image && (
)
}
diff --git a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/applepay.svg b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/applepay.svg
new file mode 100644
index 00000000000..0c6ecafef27
--- /dev/null
+++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/applepay.svg
@@ -0,0 +1,84 @@
+
+
+
diff --git a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/googlepay.svg b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/googlepay.svg
new file mode 100644
index 00000000000..a4212689d77
--- /dev/null
+++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/googlepay.svg
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/paypal.svg b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/paypal.svg
new file mode 100644
index 00000000000..e644f23076a
--- /dev/null
+++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/paypal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/scheme.svg b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/scheme.svg
new file mode 100644
index 00000000000..e5553eee2f8
--- /dev/null
+++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/scheme.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/packages/magento-payment-adyen/graphql/AdyenStoreConfig.graphql b/packages/magento-payment-adyen/graphql/AdyenStoreConfig.graphql
new file mode 100644
index 00000000000..975893af2a9
--- /dev/null
+++ b/packages/magento-payment-adyen/graphql/AdyenStoreConfig.graphql
@@ -0,0 +1,11 @@
+fragment AdyenStoreConfig on StoreConfig @inject(into: ["StoreConfigFragment"]) {
+ adyen_demo_mode
+ adyen_title_renderer
+ adyen_client_key_live
+ adyen_client_key_test
+ adyen_has_holder_name
+ adyen_return_path_error
+ adyen_oneclick_card_mode
+ adyen_holder_name_required
+ adyen_checkout_frontend_region
+}
diff --git a/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts b/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts
deleted file mode 100644
index 671d093b533..00000000000
--- a/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { ExpandPaymentMethods } from '@graphcommerce/magento-cart-payment-method'
-import { UseAdyenPaymentMethodsDocument } from './UseAdyenPaymentMethods.gql'
-
-export const nonNullable = (value: T): value is NonNullable =>
- value !== null && value !== undefined
-
-export const adyenHppExpandMethods: ExpandPaymentMethods = async (available, context) => {
- if (!context.id) return []
-
- const result = await context.client.query({
- query: UseAdyenPaymentMethodsDocument,
- variables: { cartId: context.id },
- })
-
- const methods = result.data.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods ?? []
-
- return methods
- .map((method) => {
- if (!method?.name || !method.type) return null
-
- return { title: method.name, code: available.code, child: method.type }
- })
- .filter(nonNullable)
-}
diff --git a/packages/magento-payment-adyen/hooks/useAdyenCheckoutConfig.ts b/packages/magento-payment-adyen/hooks/useAdyenCheckoutConfig.ts
new file mode 100644
index 00000000000..1aa969461d3
--- /dev/null
+++ b/packages/magento-payment-adyen/hooks/useAdyenCheckoutConfig.ts
@@ -0,0 +1,72 @@
+import { CoreOptions } from '@adyen/adyen-web/dist/types/core/types'
+import { ApolloError, QueryResult, useQuery } from '@graphcommerce/graphql'
+import { useCartQuery } from '@graphcommerce/magento-cart'
+import { StoreConfigDocument } from '@graphcommerce/magento-store'
+import { filterNonNullableKeys, nonNullable, useMemoObject } from '@graphcommerce/next-ui'
+import { useRouter } from 'next/router'
+import {
+ UseAdyenPaymentMethodsDocument,
+ UseAdyenPaymentMethodsQuery,
+ UseAdyenPaymentMethodsQueryVariables,
+} from './UseAdyenPaymentMethods.gql'
+
+export type CoreOptionsPartial = Pick<
+ CoreOptions,
+ 'environment' | 'clientKey' | 'locale' | 'paymentMethodsResponse'
+>
+
+type UseAdyenCheckoutConfigResult = QueryResult<
+ UseAdyenPaymentMethodsQuery,
+ UseAdyenPaymentMethodsQueryVariables
+> & {
+ config?: CoreOptionsPartial
+}
+
+export function useAdyenCheckoutConfig(): UseAdyenCheckoutConfigResult {
+ const storeConfig = useQuery(StoreConfigDocument).data?.storeConfig ?? {}
+ const { locale } = useRouter()
+
+ let { adyen_demo_mode, adyen_client_key_live, adyen_client_key_test } = storeConfig
+ if (!adyen_demo_mode) adyen_demo_mode = true
+ const clientKey = adyen_demo_mode ? adyen_client_key_test : adyen_client_key_live
+
+ const paymentMethodsQuery = useCartQuery(UseAdyenPaymentMethodsDocument, {
+ fetchPolicy: 'network-only',
+ })
+
+ const response = paymentMethodsQuery.data?.adyenPaymentMethods?.paymentMethodsResponse
+ const paymentMethodsResponse = {
+ ...response,
+ paymentMethods: filterNonNullableKeys(
+ response?.paymentMethods,
+ // ['brand', 'brands', 'type', 'name', 'details', 'configuration', 'issuers'],
+ ).map((pm) => ({ ...pm, brands: (pm?.brands ?? []).filter(nonNullable) })),
+ } satisfies CoreOptions['paymentMethodsResponse']
+
+ const config = useMemoObject({
+ environment: adyen_demo_mode ? 'test' : 'prod',
+ clientKey,
+ locale: locale?.split('-', 2).join('-'),
+ paymentMethodsResponse,
+ })
+
+ if (paymentMethodsQuery.loading || paymentMethodsQuery.error) {
+ return paymentMethodsQuery
+ }
+
+ if (!config.paymentMethodsResponse) {
+ return {
+ ...paymentMethodsQuery,
+ error: new ApolloError({ errorMessage: 'No Adyen payment methods response found' }),
+ }
+ }
+
+ if (!config.clientKey) {
+ return {
+ ...paymentMethodsQuery,
+ error: new ApolloError({ errorMessage: 'No Adyen client key found in store config' }),
+ }
+ }
+
+ return { ...paymentMethodsQuery, config: { ...config, clientKey: config.clientKey } }
+}
diff --git a/packages/magento-payment-adyen/hooks/useAdyenHandlePaymentResponse.ts b/packages/magento-payment-adyen/hooks/useAdyenHandlePaymentResponse.ts
index cf5bc2c9c73..a42aee7c745 100644
--- a/packages/magento-payment-adyen/hooks/useAdyenHandlePaymentResponse.ts
+++ b/packages/magento-payment-adyen/hooks/useAdyenHandlePaymentResponse.ts
@@ -20,7 +20,7 @@ export enum ResultCodeEnum {
Success = 'Success',
}
-function isResultCodeEnum(value: string): value is ResultCodeEnum {
+export function isResultCodeEnum(value: string): value is ResultCodeEnum {
return Object.values(ResultCodeEnum).includes(value as ResultCodeEnum)
}
diff --git a/packages/magento-payment-adyen/hooks/useAdyenPaymentMethod.ts b/packages/magento-payment-adyen/hooks/useAdyenPaymentMethod.ts
index 8aabb6527d8..6f4286abeb5 100644
--- a/packages/magento-payment-adyen/hooks/useAdyenPaymentMethod.ts
+++ b/packages/magento-payment-adyen/hooks/useAdyenPaymentMethod.ts
@@ -20,11 +20,12 @@ export function useAdyenPaymentMethod(brandCode: string) {
return {
...methodConf,
...config,
+ paymentMethodsResponse: methods.data?.adyenPaymentMethods?.paymentMethodsResponse,
}
}, [
brandCode,
methods.data?.adyenPaymentMethods?.paymentMethodsExtraDetails,
- methods.data?.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods,
+ methods.data?.adyenPaymentMethods?.paymentMethodsResponse,
])
return result
diff --git a/packages/magento-payment-adyen/index.ts b/packages/magento-payment-adyen/index.ts
index d9954808c6a..642501fa28d 100644
--- a/packages/magento-payment-adyen/index.ts
+++ b/packages/magento-payment-adyen/index.ts
@@ -2,7 +2,7 @@ import { PaymentModule } from '@graphcommerce/magento-cart-payment-method'
import { AdyenPaymentActionCard } from './components/AdyenPaymentActionCard/AdyenPaymentActionCard'
import { AdyenPaymentHandler } from './components/AdyenPaymentHandler/AdyenPaymentHandler'
import { HppOptions } from './components/AdyenPaymentOptionsAndPlaceOrder/AdyenPaymentOptionsAndPlaceOrder'
-import { adyenHppExpandMethods } from './hooks/adyenHppExpandMethods'
+import { adyenHppExpandMethods } from './methods/adyen_hpp/adyenHppExpandMethods'
export const adyen_hpp: PaymentModule = {
PaymentOptions: HppOptions,
diff --git a/packages/magento-payment-adyen/lib/common.ts b/packages/magento-payment-adyen/lib/common.ts
new file mode 100644
index 00000000000..c14e59d3bff
--- /dev/null
+++ b/packages/magento-payment-adyen/lib/common.ts
@@ -0,0 +1,5 @@
+export function refresh() {
+ if (typeof window !== undefined) {
+ window.location.reload();
+ }
+}
\ No newline at end of file
diff --git a/packages/magento-payment-adyen/methods/adyen_cc/AdyenCcPaymentOptionsAndPlaceOrder.graphql b/packages/magento-payment-adyen/methods/adyen_cc/AdyenCcPaymentOptionsAndPlaceOrder.graphql
new file mode 100644
index 00000000000..7826ede7502
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_cc/AdyenCcPaymentOptionsAndPlaceOrder.graphql
@@ -0,0 +1,27 @@
+mutation AdyenCcPaymentOptionsAndPlaceOrder(
+ $cartId: String!
+ $stateData: String!
+ $returnUrl: String!
+) {
+ setPaymentMethodOnCart(
+ input: {
+ cart_id: $cartId
+ payment_method: {
+ code: "adyen_cc"
+ adyen_additional_data_cc: { stateData: $stateData, returnUrl: $returnUrl }
+ }
+ }
+ ) {
+ cart {
+ ...PaymentMethodUpdated
+ }
+ }
+ placeOrder(input: { cart_id: $cartId }) {
+ order {
+ order_number
+ adyen_payment_status {
+ ...AdyenPaymentResponse
+ }
+ }
+ }
+}
diff --git a/packages/magento-payment-adyen/methods/adyen_cc/PaymentButton.tsx b/packages/magento-payment-adyen/methods/adyen_cc/PaymentButton.tsx
new file mode 100644
index 00000000000..668d2d0e960
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_cc/PaymentButton.tsx
@@ -0,0 +1,20 @@
+import { PaymentButtonProps } from '@graphcommerce/magento-cart-payment-method/Api/PaymentMethod'
+import { LinkOrButton } from '@graphcommerce/next-ui'
+
+export function PaymentButton(props: PaymentButtonProps) {
+ const { buttonProps, title } = props
+ const isPlaceOrder = buttonProps?.id === 'place-order'
+ const isValid = true
+
+ return (
+
+ {isPlaceOrder && props?.title && (
+ <>
+ {buttonProps.children} ({title})
+ >
+ )}
+
+ {!isPlaceOrder && <>Pay>}
+
+ )
+}
diff --git a/packages/magento-payment-adyen/methods/adyen_cc/PaymentMethodOptions.tsx b/packages/magento-payment-adyen/methods/adyen_cc/PaymentMethodOptions.tsx
new file mode 100644
index 00000000000..8b89e047ad4
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_cc/PaymentMethodOptions.tsx
@@ -0,0 +1,217 @@
+import AdyenCheckout from '@adyen/adyen-web'
+import { CardElement } from '@adyen/adyen-web/dist/types/components/Card/Card'
+import Core from '@adyen/adyen-web/dist/types/core/core'
+import { CoreOptions } from '@adyen/adyen-web/dist/types/core/types'
+import { PaymentAction } from '@adyen/adyen-web/dist/types/types'
+import { ApolloErrorSnackbar, useFormCompose } from '@graphcommerce/ecommerce-ui'
+import { useLazyQuery, useMutation } from '@graphcommerce/graphql'
+import { useFormGqlMutationCart, useCurrentCartId, useCartQuery } from '@graphcommerce/magento-cart'
+import { BillingPageDocument } from '@graphcommerce/magento-cart-checkout'
+import {
+ PaymentOptionsProps,
+ usePaymentMethodContext,
+} from '@graphcommerce/magento-cart-payment-method'
+import { ErrorSnackbar } from '@graphcommerce/next-ui'
+import { composedFormContext } from '@graphcommerce/react-hook-form/src/ComposedForm/context'
+import { Trans } from '@lingui/react'
+import { Box } from '@mui/material'
+import { useContext, useEffect, useRef, useState } from 'react'
+import { AdyenPaymentDetailsDocument } from '../../components/AdyenPaymentHandler/AdyenPaymentDetails.gql'
+import { AdyenPaymentStatusDocument } from '../../components/AdyenPaymentHandler/AdyenPaymentStatus.gql'
+import { useAdyenCheckoutConfig } from '../../hooks/useAdyenCheckoutConfig'
+import { ResultCodeEnum, isResultCodeEnum } from '../../hooks/useAdyenHandlePaymentResponse'
+import {
+ AdyenCcPaymentOptionsAndPlaceOrderMutation,
+ AdyenCcPaymentOptionsAndPlaceOrderMutationVariables,
+ AdyenCcPaymentOptionsAndPlaceOrderDocument,
+} from './AdyenCcPaymentOptionsAndPlaceOrder.gql'
+import '@adyen/adyen-web/dist/adyen.css'
+
+const getResultCode = (result): ResultCodeEnum =>
+ result?.data?.placeOrder?.order.adyen_payment_status?.resultCode &&
+ isResultCodeEnum(result?.data?.placeOrder?.order.adyen_payment_status?.resultCode as string)
+ ? result?.data?.placeOrder?.order.adyen_payment_status?.resultCode
+ : ResultCodeEnum.Error
+
+type UseAdyenCheckoutOptions = Omit<
+ CoreOptions,
+ 'environment' | 'clientKey' | 'locale' | 'paymentMethodsResponse'
+>
+
+function useAdyenCheckout(options?: UseAdyenCheckoutOptions) {
+ const [checkout, setCheckout] = useState(null)
+ const adyenCheckoutConfig = useAdyenCheckoutConfig()
+
+ const optionsRef = useRef(options)
+ optionsRef.current = options
+
+ useEffect(() => {
+ if (checkout || !adyenCheckoutConfig.config) return
+ ;(async () => {
+ setCheckout(await AdyenCheckout({ ...adyenCheckoutConfig.config, ...optionsRef.current }))
+ })().catch(console.error)
+ }, [checkout, adyenCheckoutConfig.config])
+
+ return checkout
+}
+
+export function PaymentMethodOptions(props: PaymentOptionsProps) {
+ const { step, code, child: brandCode, Container } = props
+ const [showError, setShowError] = useState(false)
+ const action = useRef(undefined)
+ const orderNumber = useRef('')
+ const { currentCartId } = useCurrentCartId()
+ const [getDetails] = useMutation(AdyenPaymentDetailsDocument)
+ const [getStatus] = useLazyQuery(AdyenPaymentStatusDocument, { fetchPolicy: 'network-only' })
+ const { selectedMethod, onSuccess } = usePaymentMethodContext()
+ const paymentContainer = useRef(null)
+ const component = useRef(null)
+ const [, dispatch] = useContext(composedFormContext)
+ const billingPage = useCartQuery(BillingPageDocument, { fetchPolicy: 'cache-and-network' }).data
+ ?.cart?.billing_address
+
+ const checkout = useAdyenCheckout({
+ onSubmit: (state) => {
+ const stateDataWithBillingAddress = {
+ ...state.data,
+ billingAddress: {
+ street: billingPage?.street?.[0],
+ postalCode: billingPage?.postcode,
+ city: billingPage?.city,
+ houseNumberOrName: billingPage?.street?.[1],
+ country: billingPage?.country?.code,
+ },
+ }
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ if (state.isValid) setValue('stateData', JSON.stringify(stateDataWithBillingAddress))
+ },
+ onAdditionalDetails: async (state) => {
+ console.info(`${brandCode}: onAdditionalDetails`)
+ console.info('onAdditionalDetails', state)
+
+ const payload = JSON.stringify({ orderId: orderNumber, details: state.data.details })
+
+ // Attempt 1; We first try and handle the payment for the order.
+ const details = await getDetails({
+ errorPolicy: 'all',
+ variables: { cartId: currentCartId, payload },
+ })
+
+ let paymentStatus = details.data?.adyenPaymentDetails
+
+ // Attempt 2; The adyenPaymentDetails mutation failed, because it was already called previously or no payment had been made.
+ if (details.errors) {
+ const status = await getStatus({
+ errorPolicy: 'all',
+ variables: { cartId: currentCartId, orderNumber: orderNumber.current },
+ })
+ paymentStatus = status.data?.adyenPaymentStatus
+ console.error(`payment failed: ${paymentStatus?.resultCode}`)
+
+ // Restart component and show error message
+ component.current?.remount()
+ setShowError(true)
+ dispatch({ type: 'SUBMITTED', isSubmitSuccessful: false })
+ }
+
+ if (paymentStatus?.resultCode === ResultCodeEnum.Authorised) {
+ console.info(`${brandCode} payment success`)
+ await onSuccess(orderNumber.current)
+ }
+ },
+ onError: (e) => {
+ console.error(e)
+ },
+ })
+
+ useEffect(() => {
+ if (component.current || !checkout || !paymentContainer.current) return
+ component.current = checkout
+ .create('card', {
+ hasHolderName: true,
+ holderNameRequired: true,
+ billingAddressRequired: false,
+ })
+ .mount(paymentContainer.current)
+ }, [checkout])
+
+ // Set Adyen client data on payment and place order
+ const form = useFormGqlMutationCart<
+ AdyenCcPaymentOptionsAndPlaceOrderMutation,
+ AdyenCcPaymentOptionsAndPlaceOrderMutationVariables & { issuer?: string }
+ >(AdyenCcPaymentOptionsAndPlaceOrderDocument, {
+ onBeforeSubmit: (vars) => {
+ if (!component.current) throw Error('Adyen component not mounted yet')
+ component.current.submit()
+
+ // ?locked=1&adyen=1
+ const currentUrl = new URL(window.location.href.replace(window.location.hash, ''))
+ currentUrl.searchParams.set('cart_id', vars.cartId)
+ currentUrl.searchParams.set('locked', '1')
+ currentUrl.searchParams.set('adyen', '1')
+ const returnUrl = currentUrl.toString()
+
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ return { ...vars, stateData: getValues('stateData'), returnUrl }
+ },
+ onComplete: async (result) => {
+ const merchantReference = result.data?.placeOrder?.order.order_number
+ if (merchantReference !== undefined && merchantReference !== null) {
+ orderNumber.current = merchantReference
+ }
+
+ const isAction = result?.data?.placeOrder?.order.adyen_payment_status?.action
+ if (isAction !== undefined && isAction !== null) {
+ action.current = isAction
+ }
+
+ const resultCode = getResultCode(result)
+
+ // Case 1: Non-3DS/Place Order failure -> Restart component and show error message
+ if (result.errors || !merchantReference || !selectedMethod?.code) {
+ component.current?.remount()
+ setShowError(true)
+ dispatch({ type: 'SUBMITTED', isSubmitSuccessful: false })
+ return
+ }
+
+ // Case 2: Non-3DS Authorised successfully
+ if (resultCode === ResultCodeEnum.Authorised) {
+ console.info(`${brandCode} payment success`)
+ await onSuccess(merchantReference)
+ }
+
+ // Case 3: 3DS challenge action -> start 3DS flow
+ if (
+ (resultCode === ResultCodeEnum.IdentifyShopper ||
+ resultCode === ResultCodeEnum.ChallengeShopper) &&
+ action.current
+ ) {
+ component.current?.handleAction({
+ ...JSON.parse(action.current),
+ // url: 'https://test.adyen.com/hpp/3d/validate.shtml', // <-- Remove after development
+ } as PaymentAction)
+ }
+ },
+ })
+
+ const { handleSubmit, setValue, getValues, error } = form
+ const submit = handleSubmit(() => {})
+
+ const key = `PaymentMethodOptions_${code}_${brandCode}`
+
+ /** To use an external Pay button we register the current form to be handled there as well. */
+ useFormCompose({ form, step, submit, key })
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/packages/magento-payment-adyen/methods/adyen_cc/adyenCcExpandMethods.ts b/packages/magento-payment-adyen/methods/adyen_cc/adyenCcExpandMethods.ts
new file mode 100644
index 00000000000..481847f809e
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_cc/adyenCcExpandMethods.ts
@@ -0,0 +1,26 @@
+import { ExpandPaymentMethods } from '@graphcommerce/magento-cart-payment-method'
+import { filterNonNullableKeys } from '@graphcommerce/next-ui'
+import { UseAdyenPaymentMethodsDocument } from '../../hooks/UseAdyenPaymentMethods.gql'
+
+export const nonNullable = (value: T): value is NonNullable =>
+ value !== null && value !== undefined
+
+export const adyenCcExpandMethods: ExpandPaymentMethods = async (available, context) => {
+ if (!context.id) return []
+
+ const methods = (
+ await context.client.query({
+ query: UseAdyenPaymentMethodsDocument,
+ variables: { cartId: context.id },
+ })
+ ).data.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods
+
+ return filterNonNullableKeys(methods, ['name', 'type'])
+ .map((method) => ({
+ title: method.name,
+ code: available.code,
+ child: method.type,
+ valid: true,
+ }))
+ .filter((method) => method.child === 'scheme')
+}
diff --git a/packages/magento-payment-adyen/methods/adyen_cc/index.ts b/packages/magento-payment-adyen/methods/adyen_cc/index.ts
new file mode 100644
index 00000000000..7929a3db3e7
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_cc/index.ts
@@ -0,0 +1,13 @@
+import { PaymentModule } from '@graphcommerce/magento-cart-payment-method'
+import { AdyenPaymentActionCard } from '../../components/AdyenPaymentActionCard/AdyenPaymentActionCard'
+import { PaymentButton } from './PaymentButton'
+import { PaymentMethodOptions } from './PaymentMethodOptions'
+import { adyenCcExpandMethods } from './adyenCcExpandMethods'
+
+export const adyen_cc = {
+ PaymentOptions: PaymentMethodOptions,
+ PaymentPlaceOrder: () => null,
+ PaymentActionCard: AdyenPaymentActionCard,
+ expandMethods: adyenCcExpandMethods,
+ PaymentButton,
+} as PaymentModule
diff --git a/packages/magento-payment-adyen/methods/adyen_hpp/AdyenHppPaymentOptionsAndPlaceOrder.graphql b/packages/magento-payment-adyen/methods/adyen_hpp/AdyenHppPaymentOptionsAndPlaceOrder.graphql
new file mode 100644
index 00000000000..342107c7b40
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_hpp/AdyenHppPaymentOptionsAndPlaceOrder.graphql
@@ -0,0 +1,27 @@
+mutation AdyenHppPaymentOptionsAndPlaceOrder(
+ $cartId: String!
+ $brandCode: String!
+ $stateData: String!
+) {
+ setPaymentMethodOnCart(
+ input: {
+ cart_id: $cartId
+ payment_method: {
+ code: "adyen_hpp"
+ adyen_additional_data_hpp: { brand_code: $brandCode, stateData: $stateData }
+ }
+ }
+ ) {
+ cart {
+ ...PaymentMethodUpdated
+ }
+ }
+ placeOrder(input: { cart_id: $cartId }) {
+ order {
+ order_number
+ adyen_payment_status {
+ ...AdyenPaymentResponse
+ }
+ }
+ }
+}
diff --git a/packages/magento-payment-adyen/methods/adyen_hpp/ApplePay.tsx b/packages/magento-payment-adyen/methods/adyen_hpp/ApplePay.tsx
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/magento-payment-adyen/methods/adyen_hpp/GooglePay.tsx b/packages/magento-payment-adyen/methods/adyen_hpp/GooglePay.tsx
new file mode 100644
index 00000000000..a7939582d19
--- /dev/null
+++ b/packages/magento-payment-adyen/methods/adyen_hpp/GooglePay.tsx
@@ -0,0 +1,245 @@
+import { styled } from '@mui/material'
+import Script from 'next/script'
+import { Trans } from '@lingui/react'
+import { ErrorSnackbar, Button } from '@graphcommerce/next-ui'
+import AdyenCheckout from '@adyen/adyen-web'
+import GooglePayElement from '@adyen/adyen-web/dist/types/components/GooglePay'
+import { usePaymentMethodContext } from '@graphcommerce/magento-cart-payment-method'
+import { useEffect, useRef, useState } from 'react'
+import { useFormCompose } from '@graphcommerce/react-hook-form'
+import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
+import { useAdyenPaymentMethod } from '../../hooks/useAdyenPaymentMethod'
+import { useAdyenCartLock } from '../../hooks/useAdyenCartLock'
+import { ResultCodeEnum, isResultCodeEnum } from '../../hooks/useAdyenHandlePaymentResponse'
+import {
+ AdyenHppPaymentOptionsAndPlaceOrderMutation,
+ AdyenHppPaymentOptionsAndPlaceOrderMutationVariables,
+ AdyenHppPaymentOptionsAndPlaceOrderDocument,
+} from './AdyenHppPaymentOptionsAndPlaceOrder.gql'
+import { refresh } from '../../lib/common'
+
+import '@adyen/adyen-web/dist/adyen.css'
+import { useAdyenCheckoutConfig } from '../../hooks/useAdyenCheckoutConfig'
+
+const getResultCode = (result): ResultCodeEnum => {
+ return result?.data?.placeOrder?.order.adyen_payment_status?.resultCode &&
+ isResultCodeEnum(result?.data?.placeOrder?.order.adyen_payment_status?.resultCode)
+ ? result?.data?.placeOrder?.order.adyen_payment_status?.resultCode
+ : ResultCodeEnum.Error
+}
+
+const getEnvironment = (environment: string): string => {
+ let result = 'PRODUCTION'
+ if (environment.toLowerCase() === 'test') {
+ result = 'TEST'
+ }
+ return result
+}
+
+const GooglePayContainer = styled('div')(({ theme }) => ({
+ margin: '0 auto',
+ display: 'block',
+ width: '40%',
+ [theme.breakpoints.down('md')]: {
+ width: '100%',
+ },
+}))
+
+export default function GooglePay(props) {
+ const { step, code, brandCode, cart } = props
+
+ const paymentContainer = useRef(null)
+ const stateData = useRef(undefined)
+ const action = useRef(undefined)
+ const orderNumber = useRef('')
+ const [googlePay, setGooglePay] = useState(undefined)
+ const [error, setError] = useState(false)
+ const [loaded, setLoaded] = useState(false)
+
+ const conf = useAdyenPaymentMethod(brandCode)
+ const { selectedMethod, onSuccess } = usePaymentMethodContext()
+
+ let ignore = false
+ const adyenCheckoutConfig = useAdyenCheckoutConfig()
+
+ const createCheckout = async () => {
+ console.info('create checkout')
+ const checkout = await AdyenCheckout({
+ ...adyenCheckoutConfig,
+ onSubmit: (state) => {
+ if (state.isValid) {
+ const data = JSON.stringify(state.data)
+ stateData.current = data
+ setValue('stateData', data)
+ submit()
+ console.info('onSubmit set stateData done')
+ } else {
+ setError(true)
+ }
+ },
+ onError: (error: any, _component: any) => {
+ console.error(error)
+ setError(true)
+ },
+ })
+
+ // The 'ignore' flag is used to avoid double re-rendering caused by React 18 StrictMode
+ // More about it here: https://beta.reactjs.org/learn/synchronizing-with-effects#fetching-data
+ if (paymentContainer.current && !ignore) {
+ console.info('creating checkout at the end')
+ const options = {
+ amount: {
+ value: parseFloat(cart?.prices?.grand_total?.value) * 100,
+ currency: cart?.prices?.grand_total?.currency,
+ },
+ configuration: {
+ merchantName: process.env.NEXT_PUBLIC_ADYEN_MERCHANT_NAME,
+ merchantId: process.env.NEXT_PUBLIC_ADYEN_GOOGLE_PAY_MERCHANT_ID,
+ gatewayMerchantId: process.env.NEXT_PUBLIC_ADYEN_MERCHANT_ACCOUNT,
+ },
+ environment: getEnvironment(String(process.env.NEXT_PUBLIC_ADYEN_ENVIRONMENT)),
+ countryCode: process.env.NEXT_PUBLIC_ADYEN_COUNTRY_CODE,
+ buttonType: 'checkout',
+ buttonSizeMode: 'fill',
+ }
+
+ if (googlePay === undefined) {
+ // @ts-ignore
+ const component: GooglePayElement = checkout.create(brandCode, options)
+
+ component
+ .isAvailable()
+ .then(() => {
+ // @ts-ignore
+ setGooglePay(component.mount(paymentContainer.current))
+ })
+ .catch((e) => {
+ //Google Pay is not available
+ console.info(e)
+ })
+ } else {
+ console.info(`remounting ${brandCode}`)
+ googlePay.unmount()
+ // @ts-ignore
+ const component: GooglePayElement = checkout.create(brandCode, options)
+ component
+ .isAvailable()
+ .then(() => {
+ // @ts-ignore
+ setGooglePay(component.mount(paymentContainer.current))
+ })
+ .catch((e) => {
+ //Google Pay is not available
+ console.info(e)
+ })
+ }
+ }
+ }
+
+ useEffect(() => {
+ if (!conf?.paymentMethodsResponse || !paymentContainer.current || loaded === false) {
+ console.info('loaded', loaded)
+ return
+ }
+
+ createCheckout()
+
+ return () => {
+ ignore = true
+ }
+ }, [conf?.paymentMethodsResponse, loaded])
+
+ // Set Adyen client data on payment and place order
+ const [, lock] = useAdyenCartLock()
+ const form = useFormGqlMutationCart<
+ AdyenHppPaymentOptionsAndPlaceOrderMutation,
+ AdyenHppPaymentOptionsAndPlaceOrderMutationVariables & { issuer?: string }
+ >(AdyenHppPaymentOptionsAndPlaceOrderDocument, {
+ onBeforeSubmit: async (vars) => {
+ // @ts-ignore
+ await lock({ method: selectedMethod.code, adyen: '1' })
+
+ const data = await new Promise((resolve) => {
+ ;(function waitFor() {
+ if (stateData.current !== undefined) return resolve(stateData.current)
+ setTimeout(waitFor, 30)
+ })()
+ })
+
+ return {
+ ...vars,
+ stateData: data,
+ brandCode,
+ }
+ },
+ onComplete: async (result) => {
+ console.debug('place order result:', result)
+ const merchantReference = result.data?.placeOrder?.order.order_number
+ if (merchantReference !== undefined && merchantReference !== null) {
+ orderNumber.current = merchantReference
+ }
+
+ const isAction = result?.data?.placeOrder?.order.adyen_payment_status?.action
+ if (isAction !== undefined && isAction !== null) {
+ action.current = isAction
+ }
+
+ const resultCode = getResultCode(result)
+
+ // Case 1: Place Order failure -> Show error message and remount googlePay component
+ if (result.errors || !merchantReference || !selectedMethod?.code) {
+ console.info('recreating component')
+ createCheckout()
+ return
+ }
+
+ // Case 2: Authorised successfully
+ if (resultCode == ResultCodeEnum.Authorised) {
+ console.info(`${brandCode} payment success`)
+ await onSuccess(merchantReference)
+ }
+ },
+ })
+
+ const { register, handleSubmit, setValue } = form
+ const submit = handleSubmit(() => {
+ console.info('handleSubmit')
+ })
+
+ const key = `PaymentMethodOptions_${code}_${brandCode}`
+
+ /** To use an external Pay button we register the current form to be handled there as well. */
+ useFormCompose({ form, step, submit, key })
+
+ // if (error) return Failed to load
+ if (!conf?.paymentMethodsResponse) return Loading...
+
+ return (
+