@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 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 { useRef } from 'react'
1
+ import braintree, { HostedFields, ThreeDSecure } from 'braintree-web'
2
+ import { useEffect, useState } from 'react'
3
3
  import { useBraintreeClient } from './useBraintree'
4
4
 
5
- let hostedFieldsPromise: Promise<HostedFields> | undefined
6
- function getLocalPaymentPromise(braintreePromise: ReturnType<typeof useBraintreeClient>) {
7
- if (!hostedFieldsPromise) {
8
- hostedFieldsPromise = new Promise((resolve, reject) => {
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
- ;(async () => {
11
- try {
12
- const client = await braintreePromise
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
- return hostedFieldsPromise
41
- }
22
+ const threeDSecure = await braintree.threeDSecure.create({ client, version: 2 })
42
23
 
43
- export function useBraintreeHostedFields() {
44
- const braintreePromise = useBraintreeClient()
45
- return useRef<Promise<HostedFields>>(getLocalPaymentPromise(braintreePromise)).current
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 { useFormCompose } from '@graphcommerce/react-hook-form'
5
- import { BraintreePaymentMethodOptionsDocument } from '../../BraintreePaymentMethodOptions.gql'
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 hosted = useBraintreeHostedFields()
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(BraintreePaymentMethodOptionsDocument, {
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 bla = await hosted
22
- const { nonce } = await bla.tokenize()
23
- return {
24
- ...variables,
25
- deviceData: '',
26
- nonce,
27
- isTokenEnabler: false,
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
- console.error(e)
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
- <Container>
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
- <label htmlFor='cvv'>CVV</label>
51
- <div id='cvv' />
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
- <label htmlFor='expiration-date'>Expiration Date</label>
54
- <div id='expiration-date' />
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
- </Container>
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
- } as PaymentModule
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
- } as PaymentModule
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.4",
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.4",
22
- "@graphcommerce/graphql": "^8.0.5-canary.4",
23
- "@graphcommerce/image": "^8.0.5-canary.4",
24
- "@graphcommerce/magento-cart": "^8.0.5-canary.4",
25
- "@graphcommerce/magento-cart-payment-method": "^8.0.5-canary.4",
26
- "@graphcommerce/magento-cart-shipping-address": "^8.0.5-canary.4",
27
- "@graphcommerce/magento-product": "^8.0.5-canary.4",
28
- "@graphcommerce/magento-product-configurable": "^8.0.5-canary.4",
29
- "@graphcommerce/magento-store": "^8.0.5-canary.4",
30
- "@graphcommerce/next-ui": "^8.0.5-canary.4",
31
- "@graphcommerce/prettier-config-pwa": "^8.0.5-canary.4",
32
- "@graphcommerce/react-hook-form": "^8.0.5-canary.4",
33
- "@graphcommerce/typescript-config-pwa": "^8.0.5-canary.4",
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 <Prev {...rest} modules={{ ...modules, braintree, braintree_local_payment }} />
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