From 82f5fe56c85fc3c0614a8ab073e52d12a1d5024b Mon Sep 17 00:00:00 2001 From: Joshua Bolk Date: Wed, 29 Nov 2023 13:19:16 +0100 Subject: [PATCH 01/13] Copy files from external repo --- .../PaymentMethodActionCardListForm.tsx | 2 + packages/magento-payment-adyen/README.md | 22 +- .../AdyenPaymentActionCard.tsx | 38 ++- .../AdyenPaymentActionCard/applepay.svg | 84 ++++++ .../AdyenPaymentActionCard/googlepay.svg | 21 ++ .../AdyenPaymentActionCard/paypal.svg | 1 + .../AdyenPaymentActionCard/scheme.svg | 26 ++ .../hooks/adyenCcExpandMethods.ts | 25 ++ .../hooks/adyenHppExpandMethods.ts | 8 +- .../hooks/adyenRedirectExpandMethods.ts | 25 ++ .../hooks/useAdyenHandlePaymentResponse.ts | 2 +- .../hooks/useAdyenPaymentMethod.ts | 3 +- packages/magento-payment-adyen/lib/common.ts | 5 + ...AdyenCcPaymentOptionsAndPlaceOrder.graphql | 26 ++ .../methods/adyen_cc/PaymentButton.tsx | 24 ++ .../methods/adyen_cc/PaymentMethodOptions.tsx | 277 ++++++++++++++++++ .../methods/adyen_cc/index.ts | 13 + ...dyenHppPaymentOptionsAndPlaceOrder.graphql | 30 ++ .../methods/adyen_hpp/ApplePay.tsx | 220 ++++++++++++++ .../methods/adyen_hpp/GooglePay.tsx | 247 ++++++++++++++++ .../methods/adyen_hpp/PaymentButton.tsx | 23 ++ .../adyen_hpp/PaymentMethodOptions.tsx | 37 +++ .../methods/adyen_hpp/index.ts | 15 + .../methods/adyen_redirect/index.ts | 13 + .../plugins/AddAdyenMethods.tsx | 18 +- yarn.lock | 117 ++++++-- 26 files changed, 1270 insertions(+), 52 deletions(-) create mode 100644 packages/magento-payment-adyen/components/AdyenPaymentActionCard/applepay.svg create mode 100644 packages/magento-payment-adyen/components/AdyenPaymentActionCard/googlepay.svg create mode 100644 packages/magento-payment-adyen/components/AdyenPaymentActionCard/paypal.svg create mode 100644 packages/magento-payment-adyen/components/AdyenPaymentActionCard/scheme.svg create mode 100644 packages/magento-payment-adyen/hooks/adyenCcExpandMethods.ts create mode 100644 packages/magento-payment-adyen/hooks/adyenRedirectExpandMethods.ts create mode 100644 packages/magento-payment-adyen/lib/common.ts create mode 100644 packages/magento-payment-adyen/methods/adyen_cc/AdyenCcPaymentOptionsAndPlaceOrder.graphql create mode 100644 packages/magento-payment-adyen/methods/adyen_cc/PaymentButton.tsx create mode 100644 packages/magento-payment-adyen/methods/adyen_cc/PaymentMethodOptions.tsx create mode 100644 packages/magento-payment-adyen/methods/adyen_cc/index.ts create mode 100644 packages/magento-payment-adyen/methods/adyen_hpp/AdyenHppPaymentOptionsAndPlaceOrder.graphql create mode 100644 packages/magento-payment-adyen/methods/adyen_hpp/ApplePay.tsx create mode 100644 packages/magento-payment-adyen/methods/adyen_hpp/GooglePay.tsx create mode 100644 packages/magento-payment-adyen/methods/adyen_hpp/PaymentButton.tsx create mode 100644 packages/magento-payment-adyen/methods/adyen_hpp/PaymentMethodOptions.tsx create mode 100644 packages/magento-payment-adyen/methods/adyen_hpp/index.ts create mode 100644 packages/magento-payment-adyen/methods/adyen_redirect/index.ts diff --git a/packages/magento-cart-payment-method/PaymentMethodActionCardList/PaymentMethodActionCardListForm.tsx b/packages/magento-cart-payment-method/PaymentMethodActionCardList/PaymentMethodActionCardListForm.tsx index a1c3681f34..1a95f60826 100644 --- a/packages/magento-cart-payment-method/PaymentMethodActionCardList/PaymentMethodActionCardListForm.tsx +++ b/packages/magento-cart-payment-method/PaymentMethodActionCardList/PaymentMethodActionCardListForm.tsx @@ -72,6 +72,8 @@ export function PaymentMethodActionCardListForm(props: PaymentMethodActionCardLi const { methods, selectedMethod, setSelectedMethod, setSelectedModule, modules } = usePaymentMethodContext() + console.log(10, modules, methods) + const [lockState] = useCartLock() type FormFields = { code: string | null; paymentMethod?: string } diff --git a/packages/magento-payment-adyen/README.md b/packages/magento-payment-adyen/README.md index 3319eec436..a57a1a2e58 100644 --- a/packages/magento-payment-adyen/README.md +++ b/packages/magento-payment-adyen/README.md @@ -17,13 +17,25 @@ 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 +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` +3. Add Adyen config variables in `.env` files as below: +```bash + +NEXT_PUBLIC_ADYEN_LOCALE=en_US +NEXT_PUBLIC_ADYEN_ENVIRONMENT=TEST +NEXT_PUBLIC_ADYEN_CLIENT_KEY=test_AOLSIF3JWVCM5KBL2J6QHZUP2MMAD5YM +NEXT_PUBLIC_ADYEN_COUNTRY_CODE=US +NEXT_PUBLIC_ADYEN_MERCHANT_NAME=Supplyz.com +NEXT_PUBLIC_ADYEN_MERCHANT_ACCOUNT=MarcheyGroupECOM +NEXT_PUBLIC_ADYEN_GOOGLE_PAY_MERCHANT_ID=BXX2XX4XX3X3XXXX +NEXT_PUBLIC_ADYEN_APPLE_PAY_MERCHANT_ID=merchant.yourdomain.com + +``` +4. Configure the Adyen module in Magento Admin like you would normally do. 5. Stores -> Configuration -> Sales -> Payment Methods -> Adyen Payment methods + -> Headless integration -> Payment Origin URL: `https://www.yourdomain.com` +6. 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) diff --git a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx index 7efda0cdeb..80f8aad6d2 100644 --- a/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx +++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx @@ -1,31 +1,45 @@ -import { Image } from '@graphcommerce/image' import { PaymentMethodActionCardProps } from '@graphcommerce/magento-cart-payment-method' import { ActionCard, useIconSvgSize } from '@graphcommerce/next-ui' +import { Image } from '@graphcommerce/image' import { Trans } from '@lingui/react' -import { useAdyenPaymentMethod } from '../../hooks/useAdyenPaymentMethod' +import googlepay from './googlepay.svg' +import applepay from './applepay.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 0000000000..0c6ecafef2 --- /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 0000000000..a4212689d7 --- /dev/null +++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/googlepay.svg @@ -0,0 +1,21 @@ + + + + Layer 1 + + + + + + + + + + + + + + + + + \ 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 0000000000..e644f23076 --- /dev/null +++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/paypal.svg @@ -0,0 +1 @@ +paypal-seeklogo.com \ 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 0000000000..e5553eee2f --- /dev/null +++ b/packages/magento-payment-adyen/components/AdyenPaymentActionCard/scheme.svg @@ -0,0 +1,26 @@ + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/magento-payment-adyen/hooks/adyenCcExpandMethods.ts b/packages/magento-payment-adyen/hooks/adyenCcExpandMethods.ts new file mode 100644 index 0000000000..ffb8e0e489 --- /dev/null +++ b/packages/magento-payment-adyen/hooks/adyenCcExpandMethods.ts @@ -0,0 +1,25 @@ +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 adyenCcExpandMethods: ExpandPaymentMethods = async (available, context) => { + if (!context.id) return [] + + const result = await context.client.query({ + query: UseAdyenPaymentMethodsDocument, + variables: { cartId: { input: context.id, output: 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, valid: true } + }) + .filter(nonNullable) + .filter((method) => method.child === 'scheme') +} diff --git a/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts b/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts index 671d093b53..59c46f8520 100644 --- a/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts +++ b/packages/magento-payment-adyen/hooks/adyenHppExpandMethods.ts @@ -9,7 +9,7 @@ export const adyenHppExpandMethods: ExpandPaymentMethods = async (available, con const result = await context.client.query({ query: UseAdyenPaymentMethodsDocument, - variables: { cartId: context.id }, + variables: { cartId: { input: context.id, output: context.id } }, }) const methods = result.data.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods ?? [] @@ -21,4 +21,10 @@ export const adyenHppExpandMethods: ExpandPaymentMethods = async (available, con return { title: method.name, code: available.code, child: method.type } }) .filter(nonNullable) + .filter((method) => { + if (method.child === 'applepay' && typeof window !== undefined && !window.ApplePaySession) { + return false + } + return method.child !== 'scheme' + }) } diff --git a/packages/magento-payment-adyen/hooks/adyenRedirectExpandMethods.ts b/packages/magento-payment-adyen/hooks/adyenRedirectExpandMethods.ts new file mode 100644 index 0000000000..bff8c79005 --- /dev/null +++ b/packages/magento-payment-adyen/hooks/adyenRedirectExpandMethods.ts @@ -0,0 +1,25 @@ +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 adyenRedirectExpandMethods: ExpandPaymentMethods = async (available, context) => { + if (!context.id) return [] + + const result = await context.client.query({ + query: UseAdyenPaymentMethodsDocument, + variables: { cartId: { input: context.id, output: 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) + .filter((method) => !['applepay', 'googlepay', 'scheme'].includes(method.child)) +} diff --git a/packages/magento-payment-adyen/hooks/useAdyenHandlePaymentResponse.ts b/packages/magento-payment-adyen/hooks/useAdyenHandlePaymentResponse.ts index cf5bc2c9c7..a42aee7c74 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 8aabb6527d..6f4286abeb 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/lib/common.ts b/packages/magento-payment-adyen/lib/common.ts new file mode 100644 index 0000000000..c14e59d3bf --- /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 0000000000..1c4aa735f6 --- /dev/null +++ b/packages/magento-payment-adyen/methods/adyen_cc/AdyenCcPaymentOptionsAndPlaceOrder.graphql @@ -0,0 +1,26 @@ +mutation AdyenCcPaymentOptionsAndPlaceOrder( + $cartId: String! + $stateData: String! +) { + setPaymentMethodOnCart( + input: { + cart_id: $cartId + payment_method: { + code: "adyen_cc" + adyen_additional_data_cc: { 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_cc/PaymentButton.tsx b/packages/magento-payment-adyen/methods/adyen_cc/PaymentButton.tsx new file mode 100644 index 0000000000..26cee9a9d8 --- /dev/null +++ b/packages/magento-payment-adyen/methods/adyen_cc/PaymentButton.tsx @@ -0,0 +1,24 @@ +import { LinkOrButton } from '@graphcommerce/next-ui' +import { PaymentButtonProps } from '@graphcommerce/magento-cart-payment-method/Api/PaymentMethod' + +export function PaymentButton(props: PaymentButtonProps) { + const isPlaceOrder = props.buttonProps?.id === 'place-order' ? true : false + const isValid = true + + return ( + {isPlaceOrder && props?.title && ( + <> + {props.buttonProps.children} + {' '} + ({props?.title}) + + )} + + {!isPlaceOrder && ( + <>Pay + )} + ) +} \ No newline at end of file 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 0000000000..60a4ee94c6 --- /dev/null +++ b/packages/magento-payment-adyen/methods/adyen_cc/PaymentMethodOptions.tsx @@ -0,0 +1,277 @@ +import { styled } from '@mui/material' +import { Trans } from '@lingui/react' +import { ErrorSnackbar, Button } from '@graphcommerce/next-ui' +import AdyenCheckout from '@adyen/adyen-web' +import { useLazyQuery, useMutation } from '@graphcommerce/graphql' +import { CardElement } from '@adyen/adyen-web/dist/types/components/Card/Card' +import { PaymentOptionsProps, 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 { useCurrentCartId } from '@graphcommerce/magento-cart' +import { useAdyenPaymentMethod } from '../../hooks/useAdyenPaymentMethod' +import { useAdyenCartLock } from '../../hooks/useAdyenCartLock' +import { ResultCodeEnum, isResultCodeEnum } from '../../hooks/useAdyenHandlePaymentResponse' +import { + AdyenCcPaymentOptionsAndPlaceOrderMutation, + AdyenCcPaymentOptionsAndPlaceOrderMutationVariables, + AdyenCcPaymentOptionsAndPlaceOrderDocument, +} from './AdyenCcPaymentOptionsAndPlaceOrder.gql' +import { AdyenPaymentDetailsDocument } from '../../components/AdyenPaymentHandler/AdyenPaymentDetails.gql' +import { AdyenPaymentStatusDocument } from '../../components/AdyenPaymentHandler/AdyenPaymentStatus.gql' +import { refresh } from '../../lib/common' + +import '@adyen/adyen-web/dist/adyen.css' + +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 CardContainer = styled('div')(({ theme }) => ({ + height: '100%', + width: 'max-content', + display: 'flex', + alignItems: 'center', + margin: '0 auto', + justifyContent: 'center', + pointerEvents: 'all' +})) + +const Checkout = (props) => { + const { step, code, brandCode } = props + + const paymentContainer = useRef(null) + const stateData = useRef(undefined); + const action = useRef(undefined); + const orderNumber = useRef(""); + const [card, setCard] = useState(undefined); + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = useState("Unable to complete the payment, please try again or select a different payment method. "); + // @ts-ignore + const { paymentMethodsResponse } = useAdyenPaymentMethod(brandCode) + const { currentCartId } = useCurrentCartId() + const [getDetails, { called }] = useMutation(AdyenPaymentDetailsDocument) + const [getStatus] = useLazyQuery(AdyenPaymentStatusDocument, { fetchPolicy: 'network-only' }) + const { selectedMethod, onSuccess } = usePaymentMethodContext() + + let ignore = false + const config = { + environment: process.env.NEXT_PUBLIC_ADYEN_ENVIRONMENT, + clientKey: process.env.NEXT_PUBLIC_ADYEN_CLIENT_KEY, + locale: process.env.NEXT_PUBLIC_ADYEN_LOCALE, + } + + const createCheckout = async () => { + console.info('create checkout') + const checkout = await AdyenCheckout({ + ...config, + paymentMethodsResponse, + onSubmit: (state) => { + console.info(`${brandCode}: onSubmit`) + + if (state.isValid) { + const data = JSON.stringify(state.data) + stateData.current = data + setValue('stateData', data) + console.info("onSubmit set stateData done") + } else { + setErrorMessage("Unable to complete the payment, please try again or select a different payment method. ") + setError(true) + } + }, + onAdditionalDetails: async (state) => { + console.info(`${brandCode}: onAdditionalDetails`) + console.info('onAdditionalDetails', state) + + const payload = JSON.stringify({ orderId: orderNumber, details: state.data.details }) + + // Atempt 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 + + // Atempt 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 + } + + if (paymentStatus?.resultCode == ResultCodeEnum.Authorised) { + console.info(`${brandCode} payment success`) + await onSuccess(orderNumber.current) + } + }, + onError: (error: any, _component: any) => { + console.error(error) + setErrorMessage("Unable to complete the payment, please try again or select a different payment method. "); + 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') + if (card === undefined) { + setCard(checkout.create('card').mount(paymentContainer.current)) + } else { + console.info(`remounting ${brandCode}`) + card.unmount() + setCard(checkout.create('card').mount(paymentContainer.current).setStatus('ready')) + } + } + } + + useEffect(() => { + if (!paymentMethodsResponse || !paymentContainer.current) { + return + } + + createCheckout() + + return () => { + ignore = true + } + }, [paymentMethodsResponse]) + + + // Set Adyen client data on payment and place order + const [, lock] = useAdyenCartLock() + const form = useFormGqlMutationCart< + AdyenCcPaymentOptionsAndPlaceOrderMutation, + AdyenCcPaymentOptionsAndPlaceOrderMutationVariables & { issuer?: string } + >(AdyenCcPaymentOptionsAndPlaceOrderDocument, { + onBeforeSubmit: async (vars) => { + new Promise((resolve) => { + (function waitFor() { + if (card !== undefined) return resolve(card); + setTimeout(waitFor, 30); + })(); + }).then((card) => { + console.info(`${brandCode} submit() done`) + card.submit() + }); + + const data = await new Promise((resolve) => { + (function waitFor() { + if (stateData.current !== undefined) return resolve(stateData.current); + setTimeout(waitFor, 30); + })(); + }); + + return { + ...vars, + stateData: data, + } + }, + 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: Non-3DS/Place Order failure -> Show error message and remount card component + if (result.errors || !merchantReference || !selectedMethod?.code) { + console.info("recreating component") + createCheckout() + return + } + + + // Case 2: Non-3DS Authorised successfully + if (resultCode == ResultCodeEnum.Authorised) { + console.info(`${brandCode} payment success`) + await onSuccess(merchantReference) + } + + // Case 3: 3DS challenge action -> show popup + if (action.current !== undefined) { + new Promise((resolve) => { + (function waitFor() { + if (card !== undefined) return resolve(card); + setTimeout(waitFor, 30); + })(); + }).then(async (card) => { + const data = JSON.parse(String(action.current)); + if (data?.type === 'redirect') { + await lock({ method: selectedMethod.code, adyen: '1', merchantReference }) + } + }); + } + + // Case 4: 3DS failure -> show retry message + // Case 5: 3DS challenge action success response -> show success + } + }); + + + 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 (!paymentMethodsResponse) return
Loading...
+ + return ( +
+ +
+ + { + if (card?.isValid == false) { + setErrorMessage("Please fill all the required card details and try again. "); + setError(true); + + } + return card?.isValid === true + }})}/> + + + + + } + > + + ) +} + +/** It sets the selected payment method on the cart. */ +export function PaymentMethodOptions(props: PaymentOptionsProps) { + const { code, step, child: brandCode, Container } = props + + /** This is the form that the user can fill in. */ + return ( + + + + ) +} 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 0000000000..9d2b78183a --- /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 { PaymentMethodOptions } from './PaymentMethodOptions' +import { AdyenPaymentActionCard } from '../../components/AdyenPaymentActionCard/AdyenPaymentActionCard' +import { adyenCcExpandMethods } from '../../hooks/adyenCcExpandMethods' +import { PaymentButton } from './PaymentButton' + +export const adyen_cc = { + PaymentOptions: PaymentMethodOptions, + PaymentPlaceOrder: () => null, + PaymentActionCard: AdyenPaymentActionCard, + expandMethods: adyenCcExpandMethods, + PaymentButton: 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 0000000000..6a832e0ed2 --- /dev/null +++ b/packages/magento-payment-adyen/methods/adyen_hpp/AdyenHppPaymentOptionsAndPlaceOrder.graphql @@ -0,0 +1,30 @@ +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 0000000000..79f4460451 --- /dev/null +++ b/packages/magento-payment-adyen/methods/adyen_hpp/ApplePay.tsx @@ -0,0 +1,220 @@ +import { styled } from '@mui/material' +import { Trans } from '@lingui/react' +import { ErrorSnackbar, Button } from '@graphcommerce/next-ui' +import AdyenCheckout from '@adyen/adyen-web' +import ApplePayElement from '@adyen/adyen-web/dist/types/components/ApplePay' +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' + +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 ApplePayContainer = styled('div')(({ theme }) => ({ + margin: '0 auto', + display: 'block', + width: '40%', + [theme.breakpoints.down('md')]: { + width: '100%' + }, +})) + +export default function ApplePay (props) { + const { step, code, brandCode, cart } = props + + const paymentContainer = useRef(null) + const stateData = useRef(undefined); + const action = useRef(undefined); + const orderNumber = useRef(""); + const [applePay, setApplePay] = useState(undefined); + const [error, setError] = useState(false); + // @ts-ignore + const { paymentMethodsResponse } = useAdyenPaymentMethod(brandCode) + const { selectedMethod, onSuccess } = usePaymentMethodContext() + + let ignore = false + const config = { + environment: process.env.NEXT_PUBLIC_ADYEN_ENVIRONMENT, + clientKey: process.env.NEXT_PUBLIC_ADYEN_CLIENT_KEY, + locale: process.env.NEXT_PUBLIC_ADYEN_LOCALE, + } + + const createCheckout = async () => { + console.info('create checkout') + const checkout = await AdyenCheckout({ + ...config, + paymentMethodsResponse, + 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, + }, + countryCode: process.env.NEXT_PUBLIC_ADYEN_COUNTRY_CODE, + buttonType: 'check-out' + } + + if (applePay === undefined) { + // @ts-ignore + const component: ApplePayElement = checkout.create(brandCode, options); + + component.isAvailable().then(() => { + // @ts-ignore + setApplePay(component.mount(paymentContainer.current)); + }) + .catch(e => { + //Apple Pay is not available + console.info(e) + }); + } else { + console.info(`remounting ${brandCode}`) + applePay.unmount() + // @ts-ignore + const component: ApplePayElement = checkout.create(brandCode, options); + component.isAvailable().then(() => { + // @ts-ignore + setApplePay(component.mount(paymentContainer.current)); + }) + .catch(e => { + //Apple Pay is not available + console.info(e) + }); + } + } + } + + useEffect(() => { + if (!paymentMethodsResponse || !paymentContainer.current) { + return + } + + createCheckout() + + return () => { + ignore = true + } + }, [paymentMethodsResponse]) + + + // 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 applePay 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 (!paymentMethodsResponse) return
Loading...
+ + return ( +
+ +
+ + + + + + + } + > + + ) +} \ No newline at end of file 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 0000000000..ed69d3db34 --- /dev/null +++ b/packages/magento-payment-adyen/methods/adyen_hpp/GooglePay.tsx @@ -0,0 +1,247 @@ +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' + +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); + // @ts-ignore + const { paymentMethodsResponse } = useAdyenPaymentMethod(brandCode) + const { selectedMethod, onSuccess } = usePaymentMethodContext() + + let ignore = false + const config = { + environment: process.env.NEXT_PUBLIC_ADYEN_ENVIRONMENT, + clientKey: process.env.NEXT_PUBLIC_ADYEN_CLIENT_KEY, + locale: process.env.NEXT_PUBLIC_ADYEN_LOCALE, + } + + const createCheckout = async () => { + console.info('create checkout') + const checkout = await AdyenCheckout({ + ...config, + paymentMethodsResponse, + 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 (!paymentMethodsResponse || !paymentContainer.current || loaded === false) { + console.info('loaded', loaded) + return + } + + createCheckout() + + return () => { + ignore = true + } + }, [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 (!paymentMethodsResponse) return
Loading...
+ + return ( +
+