@graphcommerce/magento-payment-braintree 8.0.5-canary.4 → 8.0.5-canary.5

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 8.0.5-canary.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+ ([@paales](https://github.com/paales))
9
+
3
10
  ## 8.0.5-canary.4
4
11
 
5
12
  ## 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.5",
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.5",
22
+ "@graphcommerce/graphql": "^8.0.5-canary.5",
23
+ "@graphcommerce/image": "^8.0.5-canary.5",
24
+ "@graphcommerce/magento-cart": "^8.0.5-canary.5",
25
+ "@graphcommerce/magento-cart-payment-method": "^8.0.5-canary.5",
26
+ "@graphcommerce/magento-cart-shipping-address": "^8.0.5-canary.5",
27
+ "@graphcommerce/magento-product": "^8.0.5-canary.5",
28
+ "@graphcommerce/magento-product-configurable": "^8.0.5-canary.5",
29
+ "@graphcommerce/magento-store": "^8.0.5-canary.5",
30
+ "@graphcommerce/next-ui": "^8.0.5-canary.5",
31
+ "@graphcommerce/prettier-config-pwa": "^8.0.5-canary.5",
32
+ "@graphcommerce/react-hook-form": "^8.0.5-canary.5",
33
+ "@graphcommerce/typescript-config-pwa": "^8.0.5-canary.5",
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