@graphcommerce/magento-payment-adyen 4.14.0-canary.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +41 -0
- package/components/AdyenPaymentActionCard/AdyenPaymentActionCard.tsx +34 -0
- package/components/AdyenPaymentHandler/AdyenPaymentDetails.graphql +5 -0
- package/components/AdyenPaymentHandler/AdyenPaymentHandler.tsx +95 -0
- package/components/AdyenPaymentHandler/AdyenPaymentStatus.graphql +5 -0
- package/components/AdyenPaymentOptionsAndPlaceOrder/AdyenPaymentOptionsAndPlaceOrder.graphql +29 -0
- package/components/AdyenPaymentOptionsAndPlaceOrder/AdyenPaymentOptionsAndPlaceOrder.tsx +106 -0
- package/hooks/AdyenPaymentResponse.graphql +6 -0
- package/hooks/UseAdyenPaymentMethods.graphql +46 -0
- package/hooks/adyenHppExpandMethods.ts +24 -0
- package/hooks/useAdyenCartLock.ts +14 -0
- package/hooks/useAdyenHandlePaymentResponse.ts +122 -0
- package/hooks/useAdyenPaymentMethod.ts +31 -0
- package/index.ts +13 -0
- package/next-env.d.ts +4 -0
- package/package.json +43 -0
- package/plugins/AddAdyenMethods.tsx +27 -0
- package/tsconfig.json +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @graphcommerce/magento-payment-adyen
|
|
2
|
+
|
|
3
|
+
## 4.14.0-canary.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1729](https://github.com/graphcommerce-org/graphcommerce/pull/1729) [`2e68e0560`](https://github.com/graphcommerce-org/graphcommerce/commit/2e68e0560690bbf9bad6dc2b33d6e2ddb16197ce) - Adyen Payment gateway support ([@paales](https://github.com/paales))
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Magento Payment Adyen
|
|
2
|
+
|
|
3
|
+
Integrates https://github.com/Adyen/adyen-magento2 with GraphCommerce.
|
|
4
|
+
|
|
5
|
+
We currently have 'Alternative Payment Methods' implemented, this means that it
|
|
6
|
+
supports all off-site payment methods that Adyen supports. This includes CC,
|
|
7
|
+
iDeal, Bancontact, Sofort, etc.
|
|
8
|
+
|
|
9
|
+
We do not support on-site credit cards yet. Let us know if you want to have
|
|
10
|
+
this.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Magento Adyen module version 8.5.0 or later
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
1. Find current version of your `@graphcommerce/magento-cart-payment-method` in
|
|
19
|
+
your package.json.
|
|
20
|
+
2. `yarn add @graphcommerce/magento-payment-adyen@1.2.3` (replace 1.2.3 with the
|
|
21
|
+
version of the step above)
|
|
22
|
+
|
|
23
|
+
3. Configure the Adyen module in Magento Admin like you would normally do.
|
|
24
|
+
4. Stores -> Configuration -> Sales -> Payment Methods -> Adyen Payment methods
|
|
25
|
+
-> Headless integration -> Payment Origin URL: `https://www.yourdomain.com`
|
|
26
|
+
5. Stores -> Configuration -> Sales -> Payment Methods -> Adyen Payment methods
|
|
27
|
+
-> Headless integration -> Payment Return URL:
|
|
28
|
+
`https://www.yourdomain.com/checkout/payment?locked=1&adyen=1` (make sure the
|
|
29
|
+
URL's match for your storeview)
|
|
30
|
+
|
|
31
|
+
This package uses GraphCommerce plugin systems, so there is no code modification
|
|
32
|
+
required.
|
|
33
|
+
|
|
34
|
+
## Known issues
|
|
35
|
+
|
|
36
|
+
- We don't need to configure the Payment URL's anymore since the 8.3.3 release
|
|
37
|
+
https://github.com/Adyen/adyen-magento2/releases/tag/8.3.3, but that isn't
|
|
38
|
+
integrated in the frontend yet.
|
|
39
|
+
|
|
40
|
+
- This package is currently untested inside the GraphCommerce repo, which it
|
|
41
|
+
should, but is used in production for multiple shops.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Image } from '@graphcommerce/image'
|
|
2
|
+
import { PaymentMethodActionCardProps } from '@graphcommerce/magento-cart-payment-method'
|
|
3
|
+
import { ActionCard, useIconSvgSize } from '@graphcommerce/next-ui'
|
|
4
|
+
import { Trans } from '@lingui/react'
|
|
5
|
+
import { useAdyenPaymentMethod } from '../../hooks/useAdyenPaymentMethod'
|
|
6
|
+
|
|
7
|
+
export function AdyenPaymentActionCard(props: PaymentMethodActionCardProps) {
|
|
8
|
+
const { child } = props
|
|
9
|
+
const iconSize = useIconSvgSize('large')
|
|
10
|
+
|
|
11
|
+
const icon = useAdyenPaymentMethod(child)?.icon
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<ActionCard
|
|
15
|
+
{...props}
|
|
16
|
+
details={child === 'ideal' && <Trans id='Pay with iDEAL' />}
|
|
17
|
+
image={
|
|
18
|
+
!!icon?.url &&
|
|
19
|
+
!!icon?.width &&
|
|
20
|
+
!!icon?.height && (
|
|
21
|
+
<Image
|
|
22
|
+
layout='fixed'
|
|
23
|
+
width={icon.width}
|
|
24
|
+
height={icon.height}
|
|
25
|
+
sx={{ width: `calc(${iconSize} / ${icon.height} * ${icon.width})`, height: iconSize }}
|
|
26
|
+
sizes={iconSize}
|
|
27
|
+
unoptimized
|
|
28
|
+
src={icon.url}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useLazyQuery, useMutation } from '@graphcommerce/graphql'
|
|
2
|
+
import { useCurrentCartId } from '@graphcommerce/magento-cart'
|
|
3
|
+
import { ErrorSnackbar } from '@graphcommerce/next-ui'
|
|
4
|
+
import { Trans } from '@lingui/react'
|
|
5
|
+
import { useEffect } from 'react'
|
|
6
|
+
import { useAdyenCartLock } from '../../hooks/useAdyenCartLock'
|
|
7
|
+
import {
|
|
8
|
+
ResultCodeEnum,
|
|
9
|
+
useAdyenHandlePaymentResponse,
|
|
10
|
+
useAdyenPaymentResponse,
|
|
11
|
+
} from '../../hooks/useAdyenHandlePaymentResponse'
|
|
12
|
+
import { AdyenPaymentDetailsDocument } from './AdyenPaymentDetails.gql'
|
|
13
|
+
import { AdyenPaymentStatusDocument } from './AdyenPaymentStatus.gql'
|
|
14
|
+
|
|
15
|
+
export function AdyenPaymentHandler() {
|
|
16
|
+
const [lockStatus] = useAdyenCartLock()
|
|
17
|
+
|
|
18
|
+
const [getDetails, { called }] = useMutation(AdyenPaymentDetailsDocument)
|
|
19
|
+
const [getStatus] = useLazyQuery(AdyenPaymentStatusDocument, { fetchPolicy: 'network-only' })
|
|
20
|
+
const { currentCartId } = useCurrentCartId()
|
|
21
|
+
|
|
22
|
+
const handleResponse = useAdyenHandlePaymentResponse()
|
|
23
|
+
const response = useAdyenPaymentResponse()
|
|
24
|
+
|
|
25
|
+
// This is actually due to a bug.
|
|
26
|
+
const isAdyen = lockStatus.adyen?.split('?')[0] === '1'
|
|
27
|
+
const orderNumber = lockStatus.merchantReference ?? lockStatus.adyen?.split('?')[1]?.slice(18)
|
|
28
|
+
const { redirectResult } = lockStatus
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const call = async () => {
|
|
32
|
+
if (!orderNumber || !isAdyen || !currentCartId || called) return
|
|
33
|
+
|
|
34
|
+
const payload = JSON.stringify({ orderId: orderNumber, details: { redirectResult } })
|
|
35
|
+
|
|
36
|
+
// Atempt 1; We first try and handle the payment for the order.
|
|
37
|
+
const details = await getDetails({
|
|
38
|
+
errorPolicy: 'all',
|
|
39
|
+
variables: { cartId: currentCartId, payload },
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
let paymentStatus = details.data?.adyenPaymentDetails
|
|
43
|
+
|
|
44
|
+
// Atempt 2; The adyenPaymentDetails mutation failed, because it was already called previously or no payment had been made.
|
|
45
|
+
if (details.errors) {
|
|
46
|
+
const status = await getStatus({
|
|
47
|
+
errorPolicy: 'all',
|
|
48
|
+
variables: { cartId: currentCartId, orderNumber },
|
|
49
|
+
})
|
|
50
|
+
paymentStatus = status.data?.adyenPaymentStatus
|
|
51
|
+
}
|
|
52
|
+
handleResponse(paymentStatus)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
56
|
+
call()
|
|
57
|
+
}, [
|
|
58
|
+
redirectResult,
|
|
59
|
+
getDetails,
|
|
60
|
+
orderNumber,
|
|
61
|
+
isAdyen,
|
|
62
|
+
getStatus,
|
|
63
|
+
called,
|
|
64
|
+
handleResponse,
|
|
65
|
+
currentCartId,
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
if (!response?.resultCode) return null
|
|
69
|
+
|
|
70
|
+
if (response.resultCode === ResultCodeEnum.RedirectShopper) {
|
|
71
|
+
return (
|
|
72
|
+
<ErrorSnackbar open>
|
|
73
|
+
<Trans id="The payment hasn't completed, please try again or select a different payment method." />
|
|
74
|
+
</ErrorSnackbar>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (response.resultCode === ResultCodeEnum.Refused) {
|
|
79
|
+
return (
|
|
80
|
+
<ErrorSnackbar open>
|
|
81
|
+
<Trans id='The payment is refused, please try again or select a different payment method.' />
|
|
82
|
+
</ErrorSnackbar>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<ErrorSnackbar open>
|
|
88
|
+
<Trans
|
|
89
|
+
id='We can not process your payment, we received an unsupported status "<0>{resultCode}</0>". Please try again or select a different payment method.'
|
|
90
|
+
values={{ resultCode: response.resultCode }}
|
|
91
|
+
components={{ 0: <strong /> }}
|
|
92
|
+
/>
|
|
93
|
+
</ErrorSnackbar>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
mutation AdyenPaymentOptionsAndPlaceOrder(
|
|
2
|
+
$cartId: String!
|
|
3
|
+
$brandCode: String!
|
|
4
|
+
$stateData: String!
|
|
5
|
+
) {
|
|
6
|
+
setPaymentMethodOnCart(
|
|
7
|
+
input: {
|
|
8
|
+
cart_id: $cartId
|
|
9
|
+
payment_method: {
|
|
10
|
+
code: "adyen_hpp"
|
|
11
|
+
adyen_additional_data_hpp: { brand_code: $brandCode, stateData: $stateData }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
) {
|
|
15
|
+
cart {
|
|
16
|
+
selected_payment_method {
|
|
17
|
+
...SelectedPaymentMethod
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
placeOrder(input: { cart_id: $cartId }) {
|
|
22
|
+
order {
|
|
23
|
+
order_number
|
|
24
|
+
adyen_payment_status {
|
|
25
|
+
...AdyenPaymentResponse
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useFormCompose, useFormPersist, useFormValidFields } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
|
|
3
|
+
import {
|
|
4
|
+
PaymentOptionsProps,
|
|
5
|
+
usePaymentMethodContext,
|
|
6
|
+
} from '@graphcommerce/magento-cart-payment-method'
|
|
7
|
+
import { FormRow, InputCheckmark } from '@graphcommerce/next-ui'
|
|
8
|
+
import { TextField } from '@mui/material'
|
|
9
|
+
import { useRouter } from 'next/router'
|
|
10
|
+
import { useAdyenCartLock } from '../../hooks/useAdyenCartLock'
|
|
11
|
+
import { useAdyenPaymentMethod } from '../../hooks/useAdyenPaymentMethod'
|
|
12
|
+
import {
|
|
13
|
+
AdyenPaymentOptionsAndPlaceOrderMutation,
|
|
14
|
+
AdyenPaymentOptionsAndPlaceOrderMutationVariables,
|
|
15
|
+
AdyenPaymentOptionsAndPlaceOrderDocument,
|
|
16
|
+
} from './AdyenPaymentOptionsAndPlaceOrder.gql'
|
|
17
|
+
|
|
18
|
+
/** It sets the selected payment method on the cart. */
|
|
19
|
+
export function HppOptions(props: PaymentOptionsProps) {
|
|
20
|
+
const { code, step, child: brandCode } = props
|
|
21
|
+
|
|
22
|
+
const conf = useAdyenPaymentMethod(brandCode)
|
|
23
|
+
|
|
24
|
+
const [, lock] = useAdyenCartLock()
|
|
25
|
+
const { selectedMethod } = usePaymentMethodContext()
|
|
26
|
+
const { push } = useRouter()
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* In the this folder you'll also find a PaymentMethodOptionsNoop.graphql document that is
|
|
30
|
+
* imported here and used as the basis for the form below.
|
|
31
|
+
*/
|
|
32
|
+
const form = useFormGqlMutationCart<
|
|
33
|
+
AdyenPaymentOptionsAndPlaceOrderMutation,
|
|
34
|
+
AdyenPaymentOptionsAndPlaceOrderMutationVariables & { issuer?: string }
|
|
35
|
+
>(AdyenPaymentOptionsAndPlaceOrderDocument, {
|
|
36
|
+
onBeforeSubmit: (vars) => ({
|
|
37
|
+
...vars,
|
|
38
|
+
stateData: JSON.stringify({
|
|
39
|
+
paymentMethod: { type: brandCode, issuer: vars.issuer },
|
|
40
|
+
clientStateDataIndicator: true,
|
|
41
|
+
}),
|
|
42
|
+
brandCode,
|
|
43
|
+
}),
|
|
44
|
+
onComplete: async (result) => {
|
|
45
|
+
const merchantReference = result.data?.placeOrder?.order.order_number
|
|
46
|
+
const action = result?.data?.placeOrder?.order.adyen_payment_status?.action
|
|
47
|
+
|
|
48
|
+
if (result.errors || !merchantReference || !selectedMethod?.code || !action) return
|
|
49
|
+
|
|
50
|
+
const url = JSON.parse(action).url as string
|
|
51
|
+
lock({ method: selectedMethod.code, adyen: '1', merchantReference })
|
|
52
|
+
await push(url)
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const { handleSubmit, muiRegister, formState, required } = form
|
|
57
|
+
|
|
58
|
+
const submit = handleSubmit(() => {})
|
|
59
|
+
|
|
60
|
+
const key = `PaymentMethodOptions_${code}${brandCode}`
|
|
61
|
+
useFormPersist({ form, name: key, persist: ['issuer'], storage: 'localStorage' })
|
|
62
|
+
|
|
63
|
+
const valid = useFormValidFields(form, required)
|
|
64
|
+
|
|
65
|
+
/** To use an external Pay button we register the current form to be handled there as well. */
|
|
66
|
+
useFormCompose({ form, step, submit, key })
|
|
67
|
+
|
|
68
|
+
if (!conf?.issuers?.length) return <form onSubmit={submit} noValidate />
|
|
69
|
+
|
|
70
|
+
/** This is the form that the user can fill in. In this case we don't wat the user to fill in anything. */
|
|
71
|
+
return (
|
|
72
|
+
<form key={key} onSubmit={submit} noValidate>
|
|
73
|
+
{conf?.issuers && (
|
|
74
|
+
<FormRow>
|
|
75
|
+
<TextField
|
|
76
|
+
defaultValue=''
|
|
77
|
+
variant='outlined'
|
|
78
|
+
color='secondary'
|
|
79
|
+
select
|
|
80
|
+
SelectProps={{ native: true, displayEmpty: true }}
|
|
81
|
+
error={formState.isSubmitted && !!formState.errors.issuer}
|
|
82
|
+
helperText={formState.isSubmitted && formState.errors.issuer?.message}
|
|
83
|
+
label={brandCode === 'ideal' ? 'Select your bank' : conf?.name}
|
|
84
|
+
required
|
|
85
|
+
{...muiRegister('issuer', {
|
|
86
|
+
required: { value: true, message: 'Please provide an issuer' },
|
|
87
|
+
})}
|
|
88
|
+
InputProps={{ endAdornment: <InputCheckmark show={valid.issuer} select /> }}
|
|
89
|
+
>
|
|
90
|
+
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
|
|
91
|
+
<option value='' />
|
|
92
|
+
{conf.issuers.map((issuer) => {
|
|
93
|
+
if (!issuer?.id || !issuer.name) return null
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<option key={issuer.id} value={issuer.id}>
|
|
97
|
+
{issuer.name}
|
|
98
|
+
</option>
|
|
99
|
+
)
|
|
100
|
+
})}
|
|
101
|
+
</TextField>
|
|
102
|
+
</FormRow>
|
|
103
|
+
)}
|
|
104
|
+
</form>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
query UseAdyenPaymentMethods($cartId: String!) {
|
|
2
|
+
adyenPaymentMethods(cart_id: $cartId) {
|
|
3
|
+
paymentMethodsExtraDetails {
|
|
4
|
+
type
|
|
5
|
+
isOpenInvoice
|
|
6
|
+
configuration {
|
|
7
|
+
currency
|
|
8
|
+
amount {
|
|
9
|
+
currency
|
|
10
|
+
value
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
icon {
|
|
14
|
+
height
|
|
15
|
+
url
|
|
16
|
+
width
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
paymentMethodsResponse {
|
|
20
|
+
paymentMethods {
|
|
21
|
+
brand
|
|
22
|
+
brands
|
|
23
|
+
configuration {
|
|
24
|
+
merchantId
|
|
25
|
+
merchantName
|
|
26
|
+
}
|
|
27
|
+
details {
|
|
28
|
+
items {
|
|
29
|
+
id
|
|
30
|
+
name
|
|
31
|
+
}
|
|
32
|
+
key
|
|
33
|
+
optional
|
|
34
|
+
type
|
|
35
|
+
value
|
|
36
|
+
}
|
|
37
|
+
issuers {
|
|
38
|
+
id
|
|
39
|
+
name
|
|
40
|
+
}
|
|
41
|
+
name
|
|
42
|
+
type
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ExpandPaymentMethods } from '@graphcommerce/magento-cart-payment-method'
|
|
2
|
+
import { UseAdyenPaymentMethodsDocument } from './UseAdyenPaymentMethods.gql'
|
|
3
|
+
|
|
4
|
+
export const nonNullable = <T>(value: T): value is NonNullable<T> =>
|
|
5
|
+
value !== null && value !== undefined
|
|
6
|
+
|
|
7
|
+
export const adyenHppExpandMethods: ExpandPaymentMethods = async (available, context) => {
|
|
8
|
+
if (!context.id) return []
|
|
9
|
+
|
|
10
|
+
const result = await context.client.query({
|
|
11
|
+
query: UseAdyenPaymentMethodsDocument,
|
|
12
|
+
variables: { cartId: context.id },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const methods = result.data.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods ?? []
|
|
16
|
+
|
|
17
|
+
return methods
|
|
18
|
+
.map((method) => {
|
|
19
|
+
if (!method?.name || !method.type) return null
|
|
20
|
+
|
|
21
|
+
return { title: method.name, code: available.code, child: method.type }
|
|
22
|
+
})
|
|
23
|
+
.filter(nonNullable)
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CartLockState, useCartLock } from '@graphcommerce/magento-cart-payment-method'
|
|
2
|
+
|
|
3
|
+
type AdyenLockState = CartLockState & {
|
|
4
|
+
adyen: string | null
|
|
5
|
+
merchantReference?: string | null
|
|
6
|
+
redirectResult?: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The cart lock situation is a bit odd since are unable to actually influence the return URL we can't safely remember the cart ID.
|
|
11
|
+
*
|
|
12
|
+
* This is a potential bug because when the customer is returning from an icognito session, the cart ID is not available.
|
|
13
|
+
*/
|
|
14
|
+
export const useAdyenCartLock = () => useCartLock<AdyenLockState>()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Types } from '@adyen/api-library'
|
|
2
|
+
import { makeVar, useReactiveVar } from '@graphcommerce/graphql'
|
|
3
|
+
import { useClearCurrentCartId, useCurrentCartId } from '@graphcommerce/magento-cart'
|
|
4
|
+
import { useEventCallback } from '@mui/material'
|
|
5
|
+
import { useRouter } from 'next/router'
|
|
6
|
+
import { AdyenPaymentResponseFragment } from './AdyenPaymentResponse.gql'
|
|
7
|
+
import { useAdyenCartLock } from './useAdyenCartLock'
|
|
8
|
+
|
|
9
|
+
export enum ResultCodeEnum {
|
|
10
|
+
AuthenticationFinished = 'AuthenticationFinished',
|
|
11
|
+
Authorised = 'Authorised',
|
|
12
|
+
Cancelled = 'Cancelled',
|
|
13
|
+
ChallengeShopper = 'ChallengeShopper',
|
|
14
|
+
Error = 'Error',
|
|
15
|
+
IdentifyShopper = 'IdentifyShopper',
|
|
16
|
+
Pending = 'Pending',
|
|
17
|
+
PresentToShopper = 'PresentToShopper',
|
|
18
|
+
Received = 'Received',
|
|
19
|
+
RedirectShopper = 'RedirectShopper',
|
|
20
|
+
Refused = 'Refused',
|
|
21
|
+
Success = 'Success',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isResultCodeEnum(value: string): value is ResultCodeEnum {
|
|
25
|
+
return Object.values(ResultCodeEnum).includes(value as ResultCodeEnum)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type AdyenPaymentResponse = {
|
|
29
|
+
isFinal: boolean
|
|
30
|
+
resultCode: ResultCodeEnum
|
|
31
|
+
action?: Types.checkout.PaymentResponse['action']
|
|
32
|
+
additionalData?: Types.checkout.PaymentResponse['additionalData']
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parsePaymentResponse(
|
|
36
|
+
status?: AdyenPaymentResponseFragment | null,
|
|
37
|
+
): AdyenPaymentResponse {
|
|
38
|
+
if (!status?.resultCode) return { isFinal: false, resultCode: ResultCodeEnum.Error }
|
|
39
|
+
|
|
40
|
+
const { isFinal, resultCode, action, additionalData } = status
|
|
41
|
+
|
|
42
|
+
if (!isResultCodeEnum(resultCode) && process.env.NODE_ENV !== 'production')
|
|
43
|
+
throw Error(
|
|
44
|
+
`Returned resultCode ${resultCode} is not in available resultCodes: ${Object.values(
|
|
45
|
+
ResultCodeEnum,
|
|
46
|
+
)}`,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const response: AdyenPaymentResponse = {
|
|
50
|
+
isFinal: Boolean(isFinal),
|
|
51
|
+
// todo: do we need to check if these values are actually valid?
|
|
52
|
+
resultCode: isResultCodeEnum(resultCode) ? resultCode : ResultCodeEnum.Error,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (action) {
|
|
56
|
+
try {
|
|
57
|
+
response.action = JSON.parse(action)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
60
|
+
throw Error('Could not parse AdyenPaymentStatusResult.action as JSON')
|
|
61
|
+
}
|
|
62
|
+
if (process.env.NODE_ENV === 'production') {
|
|
63
|
+
response.isFinal = false
|
|
64
|
+
response.resultCode = ResultCodeEnum.Error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (additionalData) {
|
|
70
|
+
try {
|
|
71
|
+
response.additionalData = JSON.parse(additionalData)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
74
|
+
throw Error('Could not parse AdyenPaymentStatusResult.additionalData as JSON')
|
|
75
|
+
}
|
|
76
|
+
if (process.env.NODE_ENV === 'production') {
|
|
77
|
+
response.isFinal = false
|
|
78
|
+
response.resultCode = ResultCodeEnum.Error
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return response
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const paymentResult = makeVar<AdyenPaymentResponse | undefined>(undefined)
|
|
87
|
+
|
|
88
|
+
export const useAdyenPaymentResponse = () => useReactiveVar(paymentResult)
|
|
89
|
+
|
|
90
|
+
export function useAdyenHandlePaymentResponse() {
|
|
91
|
+
const clearCurrentCartId = useClearCurrentCartId()
|
|
92
|
+
const { currentCartId } = useCurrentCartId()
|
|
93
|
+
const { push } = useRouter()
|
|
94
|
+
const [, , unlockAdyen] = useAdyenCartLock()
|
|
95
|
+
|
|
96
|
+
const unlock = () => unlockAdyen({ adyen: null, redirectResult: null })
|
|
97
|
+
|
|
98
|
+
return useEventCallback((status?: AdyenPaymentResponseFragment | null) => {
|
|
99
|
+
const parsedResponse = parsePaymentResponse(status)
|
|
100
|
+
const resultCode = parsedResponse?.resultCode
|
|
101
|
+
|
|
102
|
+
// https://github.com/Adyen/adyen-magento2/blob/f814d4c095b04d775faced7bdad8f10866354612/Helper/PaymentResponseHandler.php#L91
|
|
103
|
+
switch (resultCode) {
|
|
104
|
+
case ResultCodeEnum.RedirectShopper:
|
|
105
|
+
case ResultCodeEnum.Refused:
|
|
106
|
+
case ResultCodeEnum.Pending:
|
|
107
|
+
case ResultCodeEnum.Error:
|
|
108
|
+
unlock()
|
|
109
|
+
break
|
|
110
|
+
case ResultCodeEnum.Authorised:
|
|
111
|
+
clearCurrentCartId()
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
113
|
+
push({ pathname: '/checkout/success', query: { cart_id: currentCartId } })
|
|
114
|
+
break
|
|
115
|
+
default:
|
|
116
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
117
|
+
throw Error(`Unknown resultCode: ${resultCode}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
paymentResult(parsedResponse)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCartQuery } from '@graphcommerce/magento-cart'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { UseAdyenPaymentMethodsDocument } from './UseAdyenPaymentMethods.gql'
|
|
4
|
+
|
|
5
|
+
export function useAdyenPaymentMethod(brandCode: string) {
|
|
6
|
+
const methods = useCartQuery(UseAdyenPaymentMethodsDocument)
|
|
7
|
+
|
|
8
|
+
const result = useMemo(() => {
|
|
9
|
+
const config = methods.data?.adyenPaymentMethods?.paymentMethodsExtraDetails?.find(
|
|
10
|
+
(extra) => extra?.type === brandCode,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const methodConf =
|
|
14
|
+
methods.data?.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods?.find(
|
|
15
|
+
(extra) => extra?.type === brandCode,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if (!methodConf || !config) return undefined
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
...methodConf,
|
|
22
|
+
...config,
|
|
23
|
+
}
|
|
24
|
+
}, [
|
|
25
|
+
brandCode,
|
|
26
|
+
methods.data?.adyenPaymentMethods?.paymentMethodsExtraDetails,
|
|
27
|
+
methods.data?.adyenPaymentMethods?.paymentMethodsResponse?.paymentMethods,
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PaymentModule } from '@graphcommerce/magento-cart-payment-method'
|
|
2
|
+
import { AdyenPaymentActionCard } from './components/AdyenPaymentActionCard/AdyenPaymentActionCard'
|
|
3
|
+
import { AdyenPaymentHandler } from './components/AdyenPaymentHandler/AdyenPaymentHandler'
|
|
4
|
+
import { HppOptions } from './components/AdyenPaymentOptionsAndPlaceOrder/AdyenPaymentOptionsAndPlaceOrder'
|
|
5
|
+
import { adyenHppExpandMethods } from './hooks/adyenHppExpandMethods'
|
|
6
|
+
|
|
7
|
+
export const adyen_hpp: PaymentModule = {
|
|
8
|
+
PaymentOptions: HppOptions,
|
|
9
|
+
PaymentPlaceOrder: () => null,
|
|
10
|
+
PaymentHandler: AdyenPaymentHandler,
|
|
11
|
+
PaymentActionCard: AdyenPaymentActionCard,
|
|
12
|
+
expandMethods: adyenHppExpandMethods,
|
|
13
|
+
}
|
package/next-env.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphcommerce/magento-payment-adyen",
|
|
3
|
+
"homepage": "https://www.graphcommerce.org/",
|
|
4
|
+
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
+
"version": "4.14.0-canary.3",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
|
+
"eslintConfig": {
|
|
9
|
+
"extends": "@graphcommerce/eslint-config-pwa",
|
|
10
|
+
"parserOptions": {
|
|
11
|
+
"project": "./tsconfig.json"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@graphcommerce/next-ui": "4.31.0-canary.2",
|
|
16
|
+
"@graphcommerce/eslint-config-pwa": "^4.1.11",
|
|
17
|
+
"@graphcommerce/magento-cart-shipping-address": "4.14.0-canary.3",
|
|
18
|
+
"@graphcommerce/magento-product": "4.14.0-canary.3",
|
|
19
|
+
"@graphcommerce/magento-product-configurable": "4.14.0-canary.3",
|
|
20
|
+
"@graphcommerce/prettier-config-pwa": "^4.0.7",
|
|
21
|
+
"@graphcommerce/typescript-config-pwa": "^4.0.5",
|
|
22
|
+
"@playwright/test": "^1.21.1",
|
|
23
|
+
"type-fest": "^2.12.2"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@graphcommerce/graphql": "4.31.0-canary.2",
|
|
27
|
+
"@graphcommerce/graphql-mesh": "4.31.0-canary.2",
|
|
28
|
+
"@graphcommerce/image": "4.31.0-canary.2",
|
|
29
|
+
"@graphcommerce/magento-cart": "4.14.0-canary.3",
|
|
30
|
+
"@graphcommerce/magento-cart-payment-method": "4.14.0-canary.3",
|
|
31
|
+
"@graphcommerce/magento-store": "4.14.0-canary.3",
|
|
32
|
+
"@graphcommerce/next-ui": "4.31.0-canary.2",
|
|
33
|
+
"@graphcommerce/react-hook-form": "4.31.0-canary.2"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@lingui/react": "^3.13.2",
|
|
37
|
+
"@lingui/core": "^3.13.2",
|
|
38
|
+
"@mui/material": "5.5.3",
|
|
39
|
+
"next": "^12.1.2",
|
|
40
|
+
"react": "^17.0.1",
|
|
41
|
+
"react-dom": "^17.0.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PaymentMethodContextProviderProps,
|
|
3
|
+
PaymentModule,
|
|
4
|
+
} from '@graphcommerce/magento-cart-payment-method'
|
|
5
|
+
import type { PluginProps } from '@graphcommerce/next-config'
|
|
6
|
+
import { AdyenPaymentActionCard } from '../components/AdyenPaymentActionCard/AdyenPaymentActionCard'
|
|
7
|
+
import { AdyenPaymentHandler } from '../components/AdyenPaymentHandler/AdyenPaymentHandler'
|
|
8
|
+
import { HppOptions } from '../components/AdyenPaymentOptionsAndPlaceOrder/AdyenPaymentOptionsAndPlaceOrder'
|
|
9
|
+
import { adyenHppExpandMethods } from '../hooks/adyenHppExpandMethods'
|
|
10
|
+
|
|
11
|
+
export const adyen_hpp: PaymentModule = {
|
|
12
|
+
PaymentOptions: HppOptions,
|
|
13
|
+
PaymentPlaceOrder: () => null,
|
|
14
|
+
PaymentHandler: AdyenPaymentHandler,
|
|
15
|
+
PaymentActionCard: AdyenPaymentActionCard,
|
|
16
|
+
expandMethods: adyenHppExpandMethods,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const component = 'PaymentMethodContextProvider'
|
|
20
|
+
export const exported = '@graphcommerce/magento-cart-payment-method'
|
|
21
|
+
|
|
22
|
+
function AddAdyenMethods(props: PluginProps<PaymentMethodContextProviderProps>) {
|
|
23
|
+
const { modules, Component } = props
|
|
24
|
+
return <Component {...props} modules={{ ...modules, adyen_hpp }} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Plugin = AddAdyenMethods
|