@graphcommerce/magento-payment-braintree 8.1.0-canary.2 → 8.1.0-canary.20
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +103 -1
- package/hooks/useBraintreeHostedFields.ts +49 -39
- package/hooks/useBraintreePaypal.ts +39 -0
- package/methods/braintree/PaymentMethodOptions.tsx +281 -22
- package/methods/braintree/index.ts +4 -6
- package/methods/braintree_local_payments/BraintreeLocalPaymentsCart.graphql +1 -2
- package/methods/braintree_local_payments/PaymentMethodOptions.tsx +4 -5
- package/methods/braintree_local_payments/index.ts +2 -2
- package/methods/braintree_paypal/PaymentMethodOptions.tsx +90 -0
- package/methods/braintree_paypal/index.ts +10 -0
- package/package.json +14 -14
- package/plugins/AddBraintreeMethods.tsx +12 -1
package/CHANGELOG.md
CHANGED
@@ -1,6 +1,108 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## 8.1.0-canary.
|
3
|
+
## 8.1.0-canary.20
|
4
|
+
|
5
|
+
## 8.1.0-canary.19
|
6
|
+
|
7
|
+
## 8.1.0-canary.18
|
8
|
+
|
9
|
+
### Patch Changes
|
10
|
+
|
11
|
+
- [#2277](https://github.com/graphcommerce-org/graphcommerce/pull/2277) [`f9199f7`](https://github.com/graphcommerce-org/graphcommerce/commit/f9199f798583138a68dd641ea6637375c487f29b) - Solve issue where Braintree wouldn't place the order after successfully validating a Credit Card.
|
12
|
+
([@paales](https://github.com/paales))
|
13
|
+
|
14
|
+
## 8.1.0-canary.17
|
15
|
+
|
16
|
+
## 8.1.0-canary.16
|
17
|
+
|
18
|
+
## 8.1.0-canary.15
|
19
|
+
|
20
|
+
## 8.1.0-canary.14
|
21
|
+
|
22
|
+
## 8.1.0-canary.13
|
23
|
+
|
24
|
+
## 8.1.0-canary.12
|
25
|
+
|
26
|
+
## 8.1.0-canary.11
|
27
|
+
|
28
|
+
## 8.1.0-canary.10
|
29
|
+
|
30
|
+
## 8.1.0-canary.9
|
31
|
+
|
32
|
+
## 8.1.0-canary.8
|
33
|
+
|
34
|
+
## 8.1.0-canary.7
|
35
|
+
|
36
|
+
## 8.1.0-canary.6
|
37
|
+
|
38
|
+
## 8.1.0-canary.5
|
39
|
+
|
40
|
+
## 8.0.6-canary.4
|
41
|
+
|
42
|
+
## 8.0.6-canary.3
|
43
|
+
|
44
|
+
## 8.0.6-canary.2
|
45
|
+
|
46
|
+
### Patch Changes
|
47
|
+
|
48
|
+
- [#2234](https://github.com/graphcommerce-org/graphcommerce/pull/2234) [`43bd04a`](https://github.com/graphcommerce-org/graphcommerce/commit/43bd04a777c5800cc7e01bee1e123a5aad82f194) - Prevent BillingPage query from rerunning on each mutation
|
49
|
+
([@FrankHarland](https://github.com/FrankHarland))
|
50
|
+
|
51
|
+
## 8.0.6-canary.1
|
52
|
+
|
53
|
+
## 8.0.6-canary.0
|
54
|
+
|
55
|
+
## 8.0.5
|
56
|
+
|
57
|
+
### Patch Changes
|
58
|
+
|
59
|
+
- [#2188](https://github.com/graphcommerce-org/graphcommerce/pull/2188) [`eec7498`](https://github.com/graphcommerce-org/graphcommerce/commit/eec7498213f34f0f850123b577b77bf678e3c80b) - Braintree Credit Card: Hosted payment fields now have proper styling and all focus/blur and error states are correctly handled.
|
60
|
+
([@paales](https://github.com/paales))
|
61
|
+
|
62
|
+
## 8.0.5-canary.10
|
63
|
+
|
64
|
+
## 8.0.5-canary.9
|
65
|
+
|
66
|
+
## 8.0.5-canary.8
|
67
|
+
|
68
|
+
## 8.0.5-canary.7
|
69
|
+
|
70
|
+
## 8.0.5-canary.6
|
71
|
+
|
72
|
+
## 8.0.5-canary.5
|
73
|
+
|
74
|
+
### Patch Changes
|
75
|
+
|
76
|
+
- [#2188](https://github.com/graphcommerce-org/graphcommerce/pull/2188) [`eec7498`](https://github.com/graphcommerce-org/graphcommerce/commit/eec7498213f34f0f850123b577b77bf678e3c80b) - Braintree Credit Card: Hosted payment fields now have proper styling and all focus/blur and error states are correctly handled.
|
77
|
+
([@paales](https://github.com/paales))
|
78
|
+
|
79
|
+
## 8.0.5-canary.4
|
80
|
+
|
81
|
+
## 8.0.5-canary.3
|
82
|
+
|
83
|
+
## 8.0.5-canary.2
|
84
|
+
|
85
|
+
## 8.0.5-canary.1
|
86
|
+
|
87
|
+
## 8.0.5-canary.0
|
88
|
+
|
89
|
+
## 8.0.4
|
90
|
+
|
91
|
+
## 8.0.4-canary.1
|
92
|
+
|
93
|
+
## 8.0.4-canary.0
|
94
|
+
|
95
|
+
## 8.0.3
|
96
|
+
|
97
|
+
## 8.0.3-canary.6
|
98
|
+
|
99
|
+
## 8.0.3-canary.5
|
100
|
+
|
101
|
+
## 8.0.3-canary.4
|
102
|
+
|
103
|
+
## 8.0.3-canary.3
|
104
|
+
|
105
|
+
## 8.0.3-canary.2
|
4
106
|
|
5
107
|
## 8.0.3-canary.1
|
6
108
|
|
@@ -1,46 +1,56 @@
|
|
1
|
-
import braintree, { HostedFields } from 'braintree-web'
|
2
|
-
import {
|
1
|
+
import braintree, { HostedFields, ThreeDSecure } from 'braintree-web'
|
2
|
+
import { useEffect, useState } from 'react'
|
3
3
|
import { useBraintreeClient } from './useBraintree'
|
4
4
|
|
5
|
-
let
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
let teardownPromise: Promise<void> | undefined | void
|
6
|
+
|
7
|
+
export function useBraintreeHostedFields() {
|
8
|
+
const braintreePromise = useBraintreeClient()
|
9
|
+
const [hostedFields, setHostedFields] = useState<
|
10
|
+
[HostedFields, ThreeDSecure] | [undefined, undefined]
|
11
|
+
>([undefined, undefined])
|
12
|
+
|
13
|
+
useEffect(() => {
|
14
|
+
if (!hostedFields[0] && !teardownPromise) {
|
9
15
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
resolve(
|
15
|
-
await braintree.hostedFields.create({
|
16
|
-
client,
|
17
|
-
fields: {
|
18
|
-
number: {
|
19
|
-
container: '#card-number',
|
20
|
-
// placeholder: '4111 1111 1111 1111',
|
21
|
-
},
|
22
|
-
cvv: {
|
23
|
-
container: '#cvv',
|
24
|
-
// placeholder: '123',
|
25
|
-
},
|
26
|
-
expirationDate: {
|
27
|
-
container: '#expiration-date',
|
28
|
-
// placeholder: '10/2022',
|
29
|
-
},
|
30
|
-
},
|
31
|
-
}),
|
32
|
-
)
|
33
|
-
} catch (e) {
|
34
|
-
reject(e)
|
16
|
+
braintreePromise.then(async (client) => {
|
17
|
+
if (teardownPromise) {
|
18
|
+
await teardownPromise
|
19
|
+
teardownPromise = undefined
|
35
20
|
}
|
36
|
-
})()
|
37
|
-
})
|
38
|
-
}
|
39
21
|
|
40
|
-
|
41
|
-
}
|
22
|
+
const threeDSecure = await braintree.threeDSecure.create({ client, version: 2 })
|
42
23
|
|
43
|
-
|
44
|
-
|
45
|
-
|
24
|
+
const hosted = await braintree.hostedFields.create({
|
25
|
+
client,
|
26
|
+
styles: {
|
27
|
+
input: {
|
28
|
+
// change input styles to match
|
29
|
+
// bootstrap styles
|
30
|
+
'font-size': '1rem',
|
31
|
+
color: '#495057',
|
32
|
+
'padding-left': '16px',
|
33
|
+
'padding-right': '16px',
|
34
|
+
},
|
35
|
+
},
|
36
|
+
fields: {
|
37
|
+
number: { container: '#card-number' },
|
38
|
+
cvv: { container: '#cvv' },
|
39
|
+
expirationDate: { container: '#expiration-date' },
|
40
|
+
// cardholderName: { container: '#cardholder-name' },
|
41
|
+
// postalCode: { container: '#postal-code' },
|
42
|
+
},
|
43
|
+
})
|
44
|
+
|
45
|
+
setHostedFields([hosted, threeDSecure])
|
46
|
+
})
|
47
|
+
}
|
48
|
+
|
49
|
+
return () => {
|
50
|
+
teardownPromise = hostedFields[0]?.teardown()
|
51
|
+
hostedFields[1]?.teardown()
|
52
|
+
}
|
53
|
+
}, [])
|
54
|
+
|
55
|
+
return teardownPromise ? [undefined, undefined] : hostedFields
|
46
56
|
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import braintree, { PayPalCheckout } from 'braintree-web'
|
2
|
+
import { useEffect, useState } from 'react'
|
3
|
+
import { useBraintreeClient } from './useBraintree'
|
4
|
+
|
5
|
+
let teardownPromise: Promise<void> | undefined | void
|
6
|
+
|
7
|
+
export function useBraintreePaypal() {
|
8
|
+
const braintreePromise = useBraintreeClient()
|
9
|
+
const [paypalCheckout, setPaypalCheckout] = useState<PayPalCheckout | undefined>(undefined)
|
10
|
+
|
11
|
+
useEffect(() => {
|
12
|
+
if (!paypalCheckout && !teardownPromise) {
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
14
|
+
braintreePromise.then(async (client) => {
|
15
|
+
if (teardownPromise) {
|
16
|
+
await teardownPromise
|
17
|
+
teardownPromise = undefined
|
18
|
+
}
|
19
|
+
|
20
|
+
const checkout = await braintree.paypalCheckout.create({ client })
|
21
|
+
|
22
|
+
const loaded = await checkout.loadPayPalSDK({
|
23
|
+
debug: true,
|
24
|
+
currency: 'EUR',
|
25
|
+
locale: 'en-US',
|
26
|
+
intent: 'capture',
|
27
|
+
vault: false,
|
28
|
+
})
|
29
|
+
setPaypalCheckout(loaded)
|
30
|
+
})
|
31
|
+
}
|
32
|
+
|
33
|
+
return () => {
|
34
|
+
teardownPromise = paypalCheckout?.teardown()
|
35
|
+
}
|
36
|
+
}, [braintreePromise, paypalCheckout])
|
37
|
+
|
38
|
+
return teardownPromise ? undefined : paypalCheckout
|
39
|
+
}
|
@@ -1,33 +1,252 @@
|
|
1
1
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
2
|
-
import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
|
3
|
-
import { PaymentOptionsProps } from '@graphcommerce/magento-cart-payment-method'
|
4
|
-
import {
|
5
|
-
import {
|
2
|
+
import { useCartQuery, useFormGqlMutationCart } from '@graphcommerce/magento-cart'
|
3
|
+
import { PaymentOptionsProps, useCartLock } from '@graphcommerce/magento-cart-payment-method'
|
4
|
+
import { BillingPageDocument } from '@graphcommerce/magento-cart-checkout'
|
5
|
+
import { ErrorSnackbar, FormRow, FullPageMessage } from '@graphcommerce/next-ui'
|
6
|
+
import {
|
7
|
+
FieldValues,
|
8
|
+
FormProvider,
|
9
|
+
Path,
|
10
|
+
UseControllerProps,
|
11
|
+
useController,
|
12
|
+
useFormCompose,
|
13
|
+
} from '@graphcommerce/react-hook-form'
|
14
|
+
import { i18n } from '@lingui/core'
|
15
|
+
import { Trans } from '@lingui/react'
|
16
|
+
import { Box, CircularProgress, TextField } from '@mui/material'
|
17
|
+
import { HostedFields } from 'braintree-web'
|
18
|
+
import { HostedFieldsEvent, HostedFieldsHostedFieldsFieldName } from 'braintree-web/hosted-fields'
|
19
|
+
import React, { useEffect, useState } from 'react'
|
20
|
+
import {
|
21
|
+
BraintreePaymentMethodOptionsDocument,
|
22
|
+
BraintreePaymentMethodOptionsMutation,
|
23
|
+
BraintreePaymentMethodOptionsMutationVariables,
|
24
|
+
} from '../../BraintreePaymentMethodOptions.gql'
|
6
25
|
import { useBraintreeHostedFields } from '../../hooks/useBraintreeHostedFields'
|
26
|
+
import { isBraintreeError } from '../../utils/isBraintreeError'
|
27
|
+
|
28
|
+
const Field = React.forwardRef<any, { ownerState: unknown; as: string }>((props, ref) => {
|
29
|
+
const { ownerState, as, ...rest } = props
|
30
|
+
return <Box {...rest} ref={ref} sx={{ height: 54, width: '100%' }} />
|
31
|
+
})
|
32
|
+
|
33
|
+
export function BraintreeField<T extends FieldValues>(
|
34
|
+
props: {
|
35
|
+
hostedFields: HostedFields | undefined
|
36
|
+
id: string
|
37
|
+
label: string
|
38
|
+
name: HostedFieldsHostedFieldsFieldName
|
39
|
+
} & Omit<UseControllerProps<T>, 'name' | 'defaultValue'>,
|
40
|
+
) {
|
41
|
+
const { hostedFields, id, label, name, control, disabled } = props
|
42
|
+
const scopedName: HostedFieldsHostedFieldsFieldName = name
|
43
|
+
|
44
|
+
const {
|
45
|
+
field: { ref, ...field },
|
46
|
+
fieldState,
|
47
|
+
} = useController({
|
48
|
+
name: name as Path<T>,
|
49
|
+
control,
|
50
|
+
disabled: Boolean(!hostedFields || disabled),
|
51
|
+
// shouldUnregister: true,
|
52
|
+
rules: {
|
53
|
+
validate: () => {
|
54
|
+
console.log('validate')
|
55
|
+
if (!hostedFields) return false
|
56
|
+
const hostedField = hostedFields.getState().fields[name]
|
57
|
+
|
58
|
+
if (hostedField.isEmpty) return i18n._(/* i18n */ 'This field is required')
|
59
|
+
if (!hostedField.isPotentiallyValid) return i18n._(/* i18n */ 'This field is invalid')
|
60
|
+
if (!hostedField.isValid) return i18n._(/* i18n */ 'This field is invalid')
|
61
|
+
|
62
|
+
return true
|
63
|
+
},
|
64
|
+
},
|
65
|
+
})
|
66
|
+
|
67
|
+
const { invalid, error } = fieldState
|
68
|
+
|
69
|
+
const [focused, setFocused] = useState(false)
|
70
|
+
const [shrink, setShrink] = useState(false)
|
71
|
+
|
72
|
+
// Manual ref handling
|
73
|
+
ref({
|
74
|
+
focus: () => {
|
75
|
+
hostedFields?.focus(scopedName)
|
76
|
+
setFocused(true)
|
77
|
+
},
|
78
|
+
})
|
79
|
+
|
80
|
+
useEffect(() => {
|
81
|
+
if (!hostedFields) return () => {}
|
82
|
+
const handleBlur = (event: HostedFieldsEvent) => {
|
83
|
+
if (event.emittedBy !== name) return
|
84
|
+
setShrink(!event.fields[name].isEmpty)
|
85
|
+
setFocused(false)
|
86
|
+
}
|
87
|
+
const handleFocus = (event: HostedFieldsEvent) => {
|
88
|
+
if (event.emittedBy !== name) return
|
89
|
+
setShrink(true)
|
90
|
+
setFocused(true)
|
91
|
+
}
|
92
|
+
const handleNotEmpty = (event: HostedFieldsEvent) => {
|
93
|
+
if (event.emittedBy !== name) return
|
94
|
+
setShrink(true)
|
95
|
+
}
|
96
|
+
|
97
|
+
try {
|
98
|
+
hostedFields.on('focus', handleFocus)
|
99
|
+
hostedFields.on('blur', handleBlur)
|
100
|
+
hostedFields.on('notEmpty', handleNotEmpty)
|
101
|
+
} catch {
|
102
|
+
// swallow error, sometimes due to timing issue this gets called after the hostedFields.teardown() is called.
|
103
|
+
}
|
104
|
+
|
105
|
+
return () => {
|
106
|
+
hostedFields.off('focus', handleFocus)
|
107
|
+
hostedFields.off('blur', handleBlur)
|
108
|
+
hostedFields.off('blur', handleNotEmpty)
|
109
|
+
}
|
110
|
+
}, [hostedFields, name])
|
111
|
+
|
112
|
+
return (
|
113
|
+
<TextField
|
114
|
+
id={id}
|
115
|
+
label={label}
|
116
|
+
error={invalid}
|
117
|
+
helperText={error?.message}
|
118
|
+
focused={focused}
|
119
|
+
InputProps={{ slots: { input: Field } }}
|
120
|
+
InputLabelProps={{ shrink }}
|
121
|
+
{...field}
|
122
|
+
/>
|
123
|
+
)
|
124
|
+
}
|
7
125
|
|
8
126
|
/** It sets the selected payment method on the cart. */
|
9
127
|
export function PaymentMethodOptions(props: PaymentOptionsProps) {
|
10
128
|
const { code, step, Container } = props
|
11
|
-
const
|
129
|
+
const [hostedFields, threeDSecure] = useBraintreeHostedFields()
|
130
|
+
const cart = useCartQuery(BillingPageDocument)
|
131
|
+
const [lockstate, lock, unlock] = useCartLock()
|
132
|
+
|
133
|
+
useEffect(() => {
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
135
|
+
if (!lockstate.justLocked && lockstate.locked) unlock({})
|
136
|
+
}, [lockstate.justLocked, lockstate.locked, unlock])
|
137
|
+
|
138
|
+
useEffect(() => {
|
139
|
+
if (!threeDSecure) return
|
140
|
+
|
141
|
+
threeDSecure.on('lookup-complete', (data, next) => {
|
142
|
+
console.log('lookup-complete', data)
|
143
|
+
next?.()
|
144
|
+
})
|
145
|
+
threeDSecure.on('customer-canceled', (data, next) => {
|
146
|
+
console.log('customer-canceled', data)
|
147
|
+
next?.()
|
148
|
+
})
|
149
|
+
}, [threeDSecure])
|
12
150
|
|
13
151
|
/**
|
14
152
|
* In the this folder you'll also find a PaymentMethodOptionsNoop.graphql document that is
|
15
153
|
* imported here and used as the basis for the form below.
|
16
154
|
*/
|
17
|
-
const form = useFormGqlMutationCart
|
155
|
+
const form = useFormGqlMutationCart<
|
156
|
+
BraintreePaymentMethodOptionsMutation,
|
157
|
+
BraintreePaymentMethodOptionsMutationVariables & {
|
158
|
+
[K in HostedFieldsHostedFieldsFieldName]?: string
|
159
|
+
}
|
160
|
+
>(BraintreePaymentMethodOptionsDocument, {
|
18
161
|
defaultValues: { code },
|
162
|
+
experimental_useV2: true,
|
19
163
|
onBeforeSubmit: async (variables) => {
|
164
|
+
if (!hostedFields) throw new Error('Hosted fields not available')
|
165
|
+
if (!threeDSecure) throw new Error('3D Secure not available')
|
166
|
+
if (!cart.data?.cart?.prices?.grand_total?.value) throw Error('Cart total not found')
|
167
|
+
|
20
168
|
try {
|
21
|
-
const
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
169
|
+
const tokenResult = await hostedFields.tokenize()
|
170
|
+
|
171
|
+
const verifyResult = await threeDSecure.verifyCard({
|
172
|
+
nonce: tokenResult.nonce,
|
173
|
+
amount: String(cart.data.cart.prices.grand_total.value),
|
174
|
+
bin: tokenResult.details.bin,
|
175
|
+
collectDeviceData: true,
|
176
|
+
billingAddress: {
|
177
|
+
givenName: cart.data.cart.billing_address?.firstname,
|
178
|
+
surname: cart.data.cart.billing_address?.lastname,
|
179
|
+
countryCodeAlpha2: cart.data.cart.billing_address?.country?.code,
|
180
|
+
streetAddress: cart.data.cart.billing_address?.street?.join(' '),
|
181
|
+
postalCode: cart.data.cart.billing_address?.postcode ?? undefined,
|
182
|
+
phoneNumber: cart.data.cart.billing_address?.telephone ?? undefined,
|
183
|
+
region: cart.data.cart.billing_address?.region?.code ?? undefined,
|
184
|
+
locality: cart.data.cart.billing_address?.city,
|
185
|
+
},
|
186
|
+
email: cart.data.cart.email ?? undefined,
|
187
|
+
mobilePhoneNumber: cart.data.cart.billing_address?.telephone ?? undefined,
|
188
|
+
})
|
189
|
+
|
190
|
+
if (!verifyResult.threeDSecureInfo.liabilityShifted) {
|
191
|
+
throw Error('Liability not shifted')
|
28
192
|
}
|
193
|
+
|
194
|
+
await lock({ method: code })
|
195
|
+
return { ...variables, deviceData: '', nonce: verifyResult.nonce, isTokenEnabler: false }
|
29
196
|
} catch (e) {
|
30
|
-
|
197
|
+
if (isBraintreeError(e)) {
|
198
|
+
switch (e.code) {
|
199
|
+
case 'HOSTED_FIELDS_FIELDS_EMPTY':
|
200
|
+
// occurs when none of the fields are filled in
|
201
|
+
console.error('All fields are empty! Please fill out the form.')
|
202
|
+
break
|
203
|
+
case 'HOSTED_FIELDS_FIELDS_INVALID':
|
204
|
+
// occurs when certain fields do not pass client side validation
|
205
|
+
// console.error('Some fields are invalid:', tokenizeErr.details.invalidFieldKeys)
|
206
|
+
|
207
|
+
// you can also programtically access the field containers for the invalid fields
|
208
|
+
// e.details.invalidFields.forEach((fieldContainer) => {
|
209
|
+
// form.setError(fieldContainer.fieldKey, {})
|
210
|
+
// fieldContainer.className = 'invalid'
|
211
|
+
// })
|
212
|
+
break
|
213
|
+
case 'HOSTED_FIELDS_TOKENIZATION_FAIL_ON_DUPLICATE':
|
214
|
+
// occurs when:
|
215
|
+
// * the client token used for client authorization was generated
|
216
|
+
// with a customer ID and the fail on duplicate payment method
|
217
|
+
// option is set to true
|
218
|
+
// * the card being tokenized has previously been vaulted (with any customer)
|
219
|
+
// See: https://developers.braintreepayments.com/reference/request/client-token/generate/#options.fail_on_duplicate_payment_method
|
220
|
+
console.error('This payment method already exists in your vault.')
|
221
|
+
break
|
222
|
+
case 'HOSTED_FIELDS_TOKENIZATION_CVV_VERIFICATION_FAILED':
|
223
|
+
// occurs when:
|
224
|
+
// * the client token used for client authorization was generated
|
225
|
+
// with a customer ID and the verify card option is set to true
|
226
|
+
// and you have credit card verification turned on in the Braintree
|
227
|
+
// control panel
|
228
|
+
// * the cvv does not pass verfication (https://developers.braintreepayments.com/reference/general/testing/#avs-and-cvv/cid-responses)
|
229
|
+
// See: https://developers.braintreepayments.com/reference/request/client-token/generate/#options.verify_card
|
230
|
+
console.error('CVV did not pass verification')
|
231
|
+
break
|
232
|
+
case 'HOSTED_FIELDS_FAILED_TOKENIZATION':
|
233
|
+
// occurs for any other tokenization error on the server
|
234
|
+
console.error('Tokenization failed server side. Is the card valid?')
|
235
|
+
break
|
236
|
+
case 'HOSTED_FIELDS_TOKENIZATION_NETWORK_ERROR':
|
237
|
+
// occurs when the Braintree gateway cannot be contacted
|
238
|
+
console.error('Network error occurred when tokenizing.')
|
239
|
+
break
|
240
|
+
default:
|
241
|
+
console.error('Something bad happened!', e)
|
242
|
+
}
|
243
|
+
} else if (e instanceof Error) {
|
244
|
+
form.setError('nonce', {
|
245
|
+
message:
|
246
|
+
'Could not verify your Credit Card, please check your information and try again.',
|
247
|
+
})
|
248
|
+
}
|
249
|
+
await unlock({})
|
31
250
|
throw e
|
32
251
|
}
|
33
252
|
},
|
@@ -39,20 +258,60 @@ export function PaymentMethodOptions(props: PaymentOptionsProps) {
|
|
39
258
|
/** To use an external Pay button we register the current form to be handled there as well. */
|
40
259
|
useFormCompose({ form, step, submit, key: `PaymentMethodOptions_${code}` })
|
41
260
|
|
261
|
+
const nonce = form.getFieldState('nonce')
|
262
|
+
|
263
|
+
const loading = !hostedFields
|
264
|
+
|
42
265
|
/** This is the form that the user can fill in. In this case we don't wat the user to fill in anything. */
|
43
266
|
return (
|
44
|
-
<
|
267
|
+
<FormProvider {...form}>
|
45
268
|
<form onSubmit={submit}>
|
269
|
+
<ErrorSnackbar open={nonce.invalid} onClose={() => form.clearErrors('nonce')}>
|
270
|
+
<>{nonce.error?.message}</>
|
271
|
+
</ErrorSnackbar>
|
272
|
+
|
46
273
|
<input type='hidden' {...register('code')} />
|
47
|
-
<label htmlFor='card-number'>Card Number</label>
|
48
|
-
<div id='card-number' />
|
49
274
|
|
50
|
-
|
51
|
-
|
275
|
+
{loading && (
|
276
|
+
<FullPageMessage
|
277
|
+
icon={<CircularProgress />}
|
278
|
+
title={<Trans id='Loading' />}
|
279
|
+
disableMargin
|
280
|
+
sx={{ mb: 0 }}
|
281
|
+
/>
|
282
|
+
)}
|
283
|
+
|
284
|
+
<Box sx={[loading && { display: 'none' }]}>
|
285
|
+
<Container>
|
286
|
+
<FormRow sx={[]}>
|
287
|
+
<BraintreeField
|
288
|
+
control={form.control}
|
289
|
+
name='number'
|
290
|
+
hostedFields={hostedFields}
|
291
|
+
id='card-number'
|
292
|
+
label='Card Number'
|
293
|
+
/>
|
294
|
+
</FormRow>
|
52
295
|
|
53
|
-
|
54
|
-
|
296
|
+
<FormRow>
|
297
|
+
<BraintreeField
|
298
|
+
control={form.control}
|
299
|
+
name='expirationDate'
|
300
|
+
hostedFields={hostedFields}
|
301
|
+
id='expiration-date'
|
302
|
+
label='Expiration Date (MM/YYYY)'
|
303
|
+
/>
|
304
|
+
<BraintreeField
|
305
|
+
hostedFields={hostedFields}
|
306
|
+
id='cvv'
|
307
|
+
control={form.control}
|
308
|
+
name='cvv'
|
309
|
+
label='CVV'
|
310
|
+
/>
|
311
|
+
</FormRow>
|
312
|
+
</Container>
|
313
|
+
</Box>
|
55
314
|
</form>
|
56
|
-
</
|
315
|
+
</FormProvider>
|
57
316
|
)
|
58
317
|
}
|
@@ -1,10 +1,8 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
PaymentModule,
|
4
|
-
} from '@graphcommerce/magento-cart-payment-method'
|
1
|
+
import type { PaymentModule } from '@graphcommerce/magento-cart-payment-method'
|
2
|
+
import { PaymentMethodPlaceOrderNoop } from '@graphcommerce/magento-cart-payment-method/PaymentMethodPlaceOrderNoop/PaymentMethodPlaceOrderNoop'
|
5
3
|
import { PaymentMethodOptions } from './PaymentMethodOptions'
|
6
4
|
|
7
|
-
export const braintree = {
|
5
|
+
export const braintree: PaymentModule = {
|
8
6
|
PaymentOptions: PaymentMethodOptions,
|
9
7
|
PaymentPlaceOrder: PaymentMethodPlaceOrderNoop,
|
10
|
-
}
|
8
|
+
}
|
@@ -48,11 +48,10 @@ function validateAndBuildStartPaymentParams(cartData: BraintreeLocalPaymentsCart
|
|
48
48
|
|
49
49
|
return {
|
50
50
|
amount: amount.toString(),
|
51
|
-
|
52
51
|
currencyCode,
|
53
52
|
shippingAddressRequired: false,
|
54
53
|
email: cart?.email ?? '',
|
55
|
-
phone,
|
54
|
+
phone: phone ?? '',
|
56
55
|
givenName,
|
57
56
|
surname,
|
58
57
|
address: { streetAddress, extendedAddress, locality, postalCode, region, countryCode },
|
@@ -98,7 +97,7 @@ export function PaymentMethodOptions(props: PaymentOptionsProps) {
|
|
98
97
|
if (!selectedMethod?.code) throw Error('Selected method not found')
|
99
98
|
const options = validateAndBuildStartPaymentParams(cartData)
|
100
99
|
|
101
|
-
lock({ payment_id: null, method: selectedMethod?.code })
|
100
|
+
await lock({ payment_id: null, method: selectedMethod?.code })
|
102
101
|
|
103
102
|
const localPayment = await localPaymentPromise
|
104
103
|
try {
|
@@ -109,8 +108,8 @@ export function PaymentMethodOptions(props: PaymentOptionsProps) {
|
|
109
108
|
buttonText: 'Return to website',
|
110
109
|
url: window.location.href,
|
111
110
|
},
|
112
|
-
onPaymentStart: ({ paymentId }, next) => {
|
113
|
-
lock({ payment_id: paymentId, method: selectedMethod?.code })
|
111
|
+
onPaymentStart: async ({ paymentId }, next) => {
|
112
|
+
await lock({ payment_id: paymentId, method: selectedMethod?.code })
|
114
113
|
next()
|
115
114
|
},
|
116
115
|
})
|
@@ -6,9 +6,9 @@ import { PaymentHandler } from './PaymentHandler'
|
|
6
6
|
import { PaymentMethodOptions } from './PaymentMethodOptions'
|
7
7
|
import { expandMethods } from './expandMethods'
|
8
8
|
|
9
|
-
export const braintree_local_payment = {
|
9
|
+
export const braintree_local_payment: PaymentModule = {
|
10
10
|
PaymentOptions: PaymentMethodOptions,
|
11
11
|
PaymentPlaceOrder: PaymentMethodPlaceOrderNoop,
|
12
12
|
PaymentHandler,
|
13
13
|
expandMethods,
|
14
|
-
}
|
14
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import { useCartQuery, useFormGqlMutationCart } from '@graphcommerce/magento-cart'
|
2
|
+
import {
|
3
|
+
PaymentOptionsProps,
|
4
|
+
usePaymentMethodContext,
|
5
|
+
} from '@graphcommerce/magento-cart-payment-method'
|
6
|
+
import { useFormCompose } from '@graphcommerce/react-hook-form'
|
7
|
+
// import { PayPalCheckoutCreatePaymentOptions } from 'braintree-web/paypal-checkout'
|
8
|
+
import type { FlowType, Intent } from 'paypal-checkout-components'
|
9
|
+
import { useEffect } from 'react'
|
10
|
+
import { BraintreePaymentMethodOptionsDocument } from '../../BraintreePaymentMethodOptions.gql'
|
11
|
+
import { StartPaymentOptions } from '../../hooks/useBraintree'
|
12
|
+
import { useBraintreeCartLock } from '../../hooks/useBraintreeCartLock'
|
13
|
+
import { useBraintreePaypal } from '../../hooks/useBraintreePaypal'
|
14
|
+
import { isBraintreeError } from '../../utils/isBraintreeError'
|
15
|
+
import { BraintreeLocalPaymentsCartDocument } from '../braintree_local_payments/BraintreeLocalPaymentsCart.gql'
|
16
|
+
|
17
|
+
export function PaymentMethodOptions(props: PaymentOptionsProps) {
|
18
|
+
const paypal = useBraintreePaypal()
|
19
|
+
|
20
|
+
useEffect(() => {})
|
21
|
+
|
22
|
+
const { code, step, child } = props
|
23
|
+
const { data: cartData } = useCartQuery(BraintreeLocalPaymentsCartDocument)
|
24
|
+
const [lockState, lock, unlock] = useBraintreeCartLock()
|
25
|
+
const { selectedMethod } = usePaymentMethodContext()
|
26
|
+
|
27
|
+
useEffect(() => {
|
28
|
+
if (lockState.locked && !lockState.justLocked) {
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
30
|
+
unlock({ payment_id: null })
|
31
|
+
}
|
32
|
+
}, [lockState.justLocked, lockState.locked, unlock])
|
33
|
+
|
34
|
+
const form = useFormGqlMutationCart(BraintreePaymentMethodOptionsDocument, {
|
35
|
+
defaultValues: { code },
|
36
|
+
onBeforeSubmit: async () => {
|
37
|
+
if (!cartData?.cart?.id) throw Error('Cart id is missing')
|
38
|
+
if (!cartData.cart.prices?.grand_total?.value) throw Error("Cart doesn't have a total")
|
39
|
+
if (!cartData.cart.prices.grand_total.currency) throw Error("Cart doesn't have a total")
|
40
|
+
if (!selectedMethod?.code) throw Error('Selected method not found')
|
41
|
+
if (!paypal) throw Error('Paypal not loaded')
|
42
|
+
|
43
|
+
await lock({ payment_id: null, method: selectedMethod?.code })
|
44
|
+
|
45
|
+
const address = cartData.cart.shipping_addresses?.[0]
|
46
|
+
|
47
|
+
console.log('hoi')
|
48
|
+
|
49
|
+
const nonce = await paypal.createPayment({
|
50
|
+
flow: 'checkout' as FlowType, // Required
|
51
|
+
amount: cartData.cart.prices.grand_total.value,
|
52
|
+
currency: cartData.cart.prices.grand_total.currency,
|
53
|
+
intent: 'authorize' as Intent,
|
54
|
+
|
55
|
+
enableShippingAddress: true,
|
56
|
+
shippingAddressEditable: false,
|
57
|
+
shippingAddressOverride: {
|
58
|
+
recipientName: `${address?.firstname} ${address?.lastname}`,
|
59
|
+
line1: address?.street.join(' ') ?? '',
|
60
|
+
// line2: 'Unit 1',
|
61
|
+
city: address?.city ?? '',
|
62
|
+
countryCode: address?.country.code ?? '',
|
63
|
+
postalCode: address?.postcode ?? '',
|
64
|
+
state: address?.region?.code ?? '',
|
65
|
+
phone: address?.telephone ?? '',
|
66
|
+
},
|
67
|
+
})
|
68
|
+
|
69
|
+
try {
|
70
|
+
return { cartId: cartData?.cart?.id, deviceData: '', isTokenEnabler: false, nonce, code }
|
71
|
+
} catch (e) {
|
72
|
+
if (isBraintreeError(e)) await unlock({ payment_id: null })
|
73
|
+
throw e
|
74
|
+
}
|
75
|
+
},
|
76
|
+
})
|
77
|
+
|
78
|
+
const { handleSubmit, register } = form
|
79
|
+
const submit = handleSubmit(() => {})
|
80
|
+
|
81
|
+
/** To use an external Pay button we register the current form to be handled there as well. */
|
82
|
+
useFormCompose({ form, step, submit, key: `PaymentMethodOptions_${code}` })
|
83
|
+
|
84
|
+
/** This is the form that the user can fill in. In this case we don't wat the user to fill in anything. */
|
85
|
+
return (
|
86
|
+
<form onSubmit={submit}>
|
87
|
+
<input type='hidden' {...register('code')} />
|
88
|
+
</form>
|
89
|
+
)
|
90
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import {
|
2
|
+
PaymentMethodPlaceOrderNoop,
|
3
|
+
PaymentModule,
|
4
|
+
} from '@graphcommerce/magento-cart-payment-method'
|
5
|
+
import { PaymentMethodOptions } from './PaymentMethodOptions'
|
6
|
+
|
7
|
+
export const braintree_paypal: PaymentModule = {
|
8
|
+
PaymentOptions: PaymentMethodOptions,
|
9
|
+
PaymentPlaceOrder: PaymentMethodPlaceOrderNoop,
|
10
|
+
}
|
package/package.json
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"name": "@graphcommerce/magento-payment-braintree",
|
3
3
|
"homepage": "https://www.graphcommerce.org/",
|
4
4
|
"repository": "github:graphcommerce-org/graphcommerce",
|
5
|
-
"version": "8.1.0-canary.
|
5
|
+
"version": "8.1.0-canary.20",
|
6
6
|
"sideEffects": false,
|
7
7
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
8
8
|
"eslintConfig": {
|
@@ -18,19 +18,19 @@
|
|
18
18
|
"braintree-web": "^3.99.0"
|
19
19
|
},
|
20
20
|
"peerDependencies": {
|
21
|
-
"@graphcommerce/eslint-config-pwa": "^8.1.0-canary.
|
22
|
-
"@graphcommerce/graphql": "^8.1.0-canary.
|
23
|
-
"@graphcommerce/image": "^8.1.0-canary.
|
24
|
-
"@graphcommerce/magento-cart": "^8.1.0-canary.
|
25
|
-
"@graphcommerce/magento-cart-payment-method": "^8.1.0-canary.
|
26
|
-
"@graphcommerce/magento-cart-shipping-address": "^8.1.0-canary.
|
27
|
-
"@graphcommerce/magento-product": "^8.1.0-canary.
|
28
|
-
"@graphcommerce/magento-product-configurable": "^8.1.0-canary.
|
29
|
-
"@graphcommerce/magento-store": "^8.1.0-canary.
|
30
|
-
"@graphcommerce/next-ui": "^8.1.0-canary.
|
31
|
-
"@graphcommerce/prettier-config-pwa": "^8.1.0-canary.
|
32
|
-
"@graphcommerce/react-hook-form": "^8.1.0-canary.
|
33
|
-
"@graphcommerce/typescript-config-pwa": "^8.1.0-canary.
|
21
|
+
"@graphcommerce/eslint-config-pwa": "^8.1.0-canary.20",
|
22
|
+
"@graphcommerce/graphql": "^8.1.0-canary.20",
|
23
|
+
"@graphcommerce/image": "^8.1.0-canary.20",
|
24
|
+
"@graphcommerce/magento-cart": "^8.1.0-canary.20",
|
25
|
+
"@graphcommerce/magento-cart-payment-method": "^8.1.0-canary.20",
|
26
|
+
"@graphcommerce/magento-cart-shipping-address": "^8.1.0-canary.20",
|
27
|
+
"@graphcommerce/magento-product": "^8.1.0-canary.20",
|
28
|
+
"@graphcommerce/magento-product-configurable": "^8.1.0-canary.20",
|
29
|
+
"@graphcommerce/magento-store": "^8.1.0-canary.20",
|
30
|
+
"@graphcommerce/next-ui": "^8.1.0-canary.20",
|
31
|
+
"@graphcommerce/prettier-config-pwa": "^8.1.0-canary.20",
|
32
|
+
"@graphcommerce/react-hook-form": "^8.1.0-canary.20",
|
33
|
+
"@graphcommerce/typescript-config-pwa": "^8.1.0-canary.20",
|
34
34
|
"@lingui/core": "^4.2.1",
|
35
35
|
"@lingui/macro": "^4.2.1",
|
36
36
|
"@lingui/react": "^4.2.1",
|
@@ -2,13 +2,24 @@ import { PaymentMethodContextProviderProps } from '@graphcommerce/magento-cart-p
|
|
2
2
|
import type { PluginProps } from '@graphcommerce/next-config'
|
3
3
|
import { braintree } from '../methods/braintree'
|
4
4
|
import { braintree_local_payment } from '../methods/braintree_local_payments'
|
5
|
+
import { braintree_paypal } from '../methods/braintree_paypal'
|
5
6
|
|
6
7
|
export const component = 'PaymentMethodContextProvider'
|
7
8
|
export const exported = '@graphcommerce/magento-cart-payment-method'
|
8
9
|
|
9
10
|
function AddBrainTreeMethods(props: PluginProps<PaymentMethodContextProviderProps>) {
|
10
11
|
const { modules, Prev, ...rest } = props
|
11
|
-
return
|
12
|
+
return (
|
13
|
+
<Prev
|
14
|
+
{...rest}
|
15
|
+
modules={{
|
16
|
+
...modules,
|
17
|
+
braintree,
|
18
|
+
braintree_local_payment,
|
19
|
+
// braintree_paypal,
|
20
|
+
}}
|
21
|
+
/>
|
22
|
+
)
|
12
23
|
}
|
13
24
|
|
14
25
|
export const Plugin = AddBrainTreeMethods
|