@graphcommerce/magento-payment-braintree 8.0.5-canary.4 → 8.0.5-canary.6
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 +9 -0
- package/hooks/useBraintreeHostedFields.ts +49 -39
- package/hooks/useBraintreePaypal.ts +39 -0
- package/methods/braintree/PaymentMethodOptions.tsx +281 -22
- package/methods/braintree/index.ts +2 -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,5 +1,14 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 8.0.5-canary.6
|
4
|
+
|
5
|
+
## 8.0.5-canary.5
|
6
|
+
|
7
|
+
### Patch Changes
|
8
|
+
|
9
|
+
- [#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.
|
10
|
+
([@paales](https://github.com/paales))
|
11
|
+
|
3
12
|
## 8.0.5-canary.4
|
4
13
|
|
5
14
|
## 8.0.5-canary.3
|
@@ -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
|
}
|
@@ -4,7 +4,7 @@ import {
|
|
4
4
|
} from '@graphcommerce/magento-cart-payment-method'
|
5
5
|
import { PaymentMethodOptions } from './PaymentMethodOptions'
|
6
6
|
|
7
|
-
export const braintree = {
|
7
|
+
export const braintree: PaymentModule = {
|
8
8
|
PaymentOptions: PaymentMethodOptions,
|
9
9
|
PaymentPlaceOrder: PaymentMethodPlaceOrderNoop,
|
10
|
-
}
|
10
|
+
}
|
@@ -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.0.5-canary.
|
5
|
+
"version": "8.0.5-canary.6",
|
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.0.5-canary.
|
22
|
-
"@graphcommerce/graphql": "^8.0.5-canary.
|
23
|
-
"@graphcommerce/image": "^8.0.5-canary.
|
24
|
-
"@graphcommerce/magento-cart": "^8.0.5-canary.
|
25
|
-
"@graphcommerce/magento-cart-payment-method": "^8.0.5-canary.
|
26
|
-
"@graphcommerce/magento-cart-shipping-address": "^8.0.5-canary.
|
27
|
-
"@graphcommerce/magento-product": "^8.0.5-canary.
|
28
|
-
"@graphcommerce/magento-product-configurable": "^8.0.5-canary.
|
29
|
-
"@graphcommerce/magento-store": "^8.0.5-canary.
|
30
|
-
"@graphcommerce/next-ui": "^8.0.5-canary.
|
31
|
-
"@graphcommerce/prettier-config-pwa": "^8.0.5-canary.
|
32
|
-
"@graphcommerce/react-hook-form": "^8.0.5-canary.
|
33
|
-
"@graphcommerce/typescript-config-pwa": "^8.0.5-canary.
|
21
|
+
"@graphcommerce/eslint-config-pwa": "^8.0.5-canary.6",
|
22
|
+
"@graphcommerce/graphql": "^8.0.5-canary.6",
|
23
|
+
"@graphcommerce/image": "^8.0.5-canary.6",
|
24
|
+
"@graphcommerce/magento-cart": "^8.0.5-canary.6",
|
25
|
+
"@graphcommerce/magento-cart-payment-method": "^8.0.5-canary.6",
|
26
|
+
"@graphcommerce/magento-cart-shipping-address": "^8.0.5-canary.6",
|
27
|
+
"@graphcommerce/magento-product": "^8.0.5-canary.6",
|
28
|
+
"@graphcommerce/magento-product-configurable": "^8.0.5-canary.6",
|
29
|
+
"@graphcommerce/magento-store": "^8.0.5-canary.6",
|
30
|
+
"@graphcommerce/next-ui": "^8.0.5-canary.6",
|
31
|
+
"@graphcommerce/prettier-config-pwa": "^8.0.5-canary.6",
|
32
|
+
"@graphcommerce/react-hook-form": "^8.0.5-canary.6",
|
33
|
+
"@graphcommerce/typescript-config-pwa": "^8.0.5-canary.6",
|
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
|