@riosst100/pwa-marketplace 2.5.1 → 2.5.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@riosst100/pwa-marketplace",
3
3
  "author": "riosst100@gmail.com",
4
- "version": "2.5.1",
4
+ "version": "2.5.2",
5
5
  "main": "src/index.js",
6
6
  "pwa-studio": {
7
7
  "targets": {
@@ -134,7 +134,7 @@ const liveChat = (props) => {
134
134
  />
135
135
  </div>
136
136
  </div>
137
- <style jsx>
137
+ <style jsx="true">
138
138
  {`
139
139
  .chat-container::-webkit-scrollbar {
140
140
  width: 8px;
@@ -31,7 +31,7 @@ const ProductItem = () => {
31
31
  </div>
32
32
  </div>
33
33
  </div>
34
- <style jsx>
34
+ <style jsx="true">
35
35
  {`
36
36
  .line-clamp-2 {
37
37
  display: -webkit-box;
@@ -230,7 +230,7 @@ const RFQModalForm = (props) => {
230
230
  </div>
231
231
  </div>
232
232
  </Modal>
233
- <style jsx>
233
+ <style jsx="true">
234
234
  {`
235
235
  .react-datepicker__input-container>input:focus-visible,
236
236
  .react-datepicker__input-container>input:focus,
@@ -316,7 +316,7 @@ const quoteDetail = () => {
316
316
  </div>
317
317
  </div>
318
318
  </div>
319
- <style jsx>
319
+ <style jsx="true">
320
320
  {`
321
321
  .chat-container::-webkit-scrollbar {
322
322
  width: 8px;
@@ -212,7 +212,7 @@ const RMACreate = () => {
212
212
  </div>
213
213
  </div>
214
214
  </div>
215
- <style jsx>
215
+ <style jsx="true">
216
216
  {`
217
217
  .chat-container::-webkit-scrollbar {
218
218
  width: 8px;
@@ -292,7 +292,7 @@ const RMADetail = () => {
292
292
  </div>
293
293
  </div>
294
294
  </div>
295
- <style jsx>
295
+ <style jsx="true">
296
296
  {`
297
297
  .chat-container::-webkit-scrollbar {
298
298
  width: 8px;
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { FormattedMessage } from 'react-intl';
3
+ import { shape, string, func } from 'prop-types';
4
+
5
+ import { useSummary } from '@magento/peregrine/lib/talons/CheckoutPage/PaymentInformation/useSummary';
6
+ import { useStyle } from '@magento/venia-ui/lib/classify';
7
+
8
+ import defaultClasses from './summary.module.css';
9
+ import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator';
10
+ import summaryPayments from '@magento/venia-ui/lib/components/CheckoutPage/PaymentInformation/summaryPaymentCollection';
11
+
12
+ const Summary = props => {
13
+ const { classes: propClasses, onEdit } = props;
14
+ const classes = useStyle(defaultClasses, propClasses);
15
+
16
+ const talonProps = useSummary();
17
+
18
+ const { isLoading, selectedPaymentMethod } = talonProps;
19
+
20
+ if (isLoading && !selectedPaymentMethod) {
21
+ return (
22
+ <LoadingIndicator classes={{ root: classes.loading }}>
23
+ <FormattedMessage
24
+ id={'checkoutPage.loadingPaymentInformation'}
25
+ defaultMessage={'Fetching Payment Information'}
26
+ />
27
+ </LoadingIndicator>
28
+ );
29
+ }
30
+
31
+ const hasCustomSummaryComp = Object.keys(summaryPayments).includes(
32
+ selectedPaymentMethod.code
33
+ );
34
+
35
+ if (hasCustomSummaryComp) {
36
+ const SummaryPaymentMethodComponent =
37
+ summaryPayments[selectedPaymentMethod.code];
38
+ return <SummaryPaymentMethodComponent onEdit={onEdit} />;
39
+ } else {
40
+ return (
41
+ <div className={classes.root}>
42
+ <div className={classes.heading_container}>
43
+ <h5 className={classes.heading}>
44
+ <FormattedMessage
45
+ id={'checkoutPage.paymentInformation'}
46
+ defaultMessage={'Payment Information'}
47
+ />
48
+ </h5>
49
+ </div>
50
+ <div className={classes.card_details_container}>
51
+ <span className={classes.payment_details}>
52
+ {selectedPaymentMethod.title}
53
+ </span>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
58
+ };
59
+
60
+ export default Summary;
61
+
62
+ Summary.propTypes = {
63
+ classes: shape({
64
+ root: string,
65
+ heading_container: string,
66
+ heading: string,
67
+ card_details_container: string,
68
+ payment_details: string
69
+ }),
70
+ onEdit: func.isRequired
71
+ };
@@ -0,0 +1,23 @@
1
+ .root {
2
+ composes: gap-xs from global;
3
+ composes: grid from global;
4
+ composes: p-md from global;
5
+ }
6
+
7
+ .heading_container {
8
+ composes: grid from global;
9
+ composes: grid-cols-1 from global;
10
+ composes: grid-flow-col from global;
11
+ }
12
+
13
+ .heading {
14
+ composes: font-semibold from global;
15
+ }
16
+
17
+ .card_details_container {
18
+ composes: gap-2xs from global;
19
+ composes: grid from global;
20
+ }
21
+
22
+ .payment_details {
23
+ }
@@ -0,0 +1,348 @@
1
+ import React, { useMemo, useCallback } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import { bool, func, shape, string } from 'prop-types';
4
+ import { useXendit } from '@riosst100/src/talons/Xendit/useXendit';
5
+
6
+ import { isRequired } from '@magento/venia-ui/lib/util/formValidators';
7
+ import Country from '@magento/venia-ui/lib/components/Country';
8
+ import Region from '@magento/venia-ui/lib/components/Region';
9
+ import Postcode from '@magento/venia-ui/lib/components/Postcode';
10
+ import Checkbox from '@magento/venia-ui/lib/components/Checkbox';
11
+ import Field from '@magento/venia-ui/lib/components/Field';
12
+ import TextInput from '@magento/venia-ui/lib/components/TextInput';
13
+ import BrainTreeDropin from './brainTreeDropIn';
14
+ import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator';
15
+ import { useStyle } from '@magento/venia-ui/lib/classify';
16
+
17
+ import defaultClasses from './xendit.module.css';
18
+ import FormError from '@magento/venia-ui/lib/components/FormError';
19
+ import GoogleReCaptcha from '@magento/venia-ui/lib/components/GoogleReCaptcha';
20
+
21
+ const STEP_DESCRIPTIONS = [
22
+ { defaultMessage: 'Loading Payment', id: 'checkoutPage.step0' },
23
+ {
24
+ defaultMessage: 'Checking Credit Card Information',
25
+ id: 'checkoutPage.step1'
26
+ },
27
+ {
28
+ defaultMessage: 'Checking Credit Card Information',
29
+ id: 'checkoutPage.step2'
30
+ },
31
+ {
32
+ defaultMessage: 'Checking Credit Card Information',
33
+ id: 'checkoutPage.step3'
34
+ },
35
+ {
36
+ defaultMessage: 'Saved Credit Card Information Successfully',
37
+ id: 'checkoutPage.step4'
38
+ }
39
+ ];
40
+
41
+ /**
42
+ * The initial view for the Braintree payment method.
43
+ */
44
+ const Xendit = props => {
45
+ const {
46
+ classes: propClasses,
47
+ onPaymentSuccess: onSuccess,
48
+ onPaymentReady: onReady,
49
+ onPaymentError: onError,
50
+ resetShouldSubmit,
51
+ shouldSubmit
52
+ } = props;
53
+ const { formatMessage } = useIntl();
54
+
55
+ const classes = useStyle(defaultClasses, propClasses);
56
+
57
+ const talonProps = useXendit({
58
+ onSuccess,
59
+ onReady,
60
+ onError,
61
+ shouldSubmit,
62
+ resetShouldSubmit
63
+ });
64
+
65
+ const {
66
+ errors,
67
+ shouldRequestPaymentNonce,
68
+ onPaymentError,
69
+ onPaymentSuccess,
70
+ onPaymentReady,
71
+ isBillingAddressSame,
72
+ isLoading,
73
+ /**
74
+ * `stepNumber` depicts the state of the process flow in credit card
75
+ * payment flow.
76
+ *
77
+ * `0` No call made yet
78
+ * `1` Billing address mutation intiated
79
+ * `2` Braintree nonce requsted
80
+ * `3` Payment information mutation intiated
81
+ * `4` All mutations done
82
+ */
83
+ stepNumber,
84
+ initialValues,
85
+ shippingAddressCountry,
86
+ shouldTeardownDropin,
87
+ resetShouldTeardownDropin,
88
+ recaptchaWidgetProps
89
+ } = talonProps;
90
+
91
+ const creditCardComponentClassName = isLoading
92
+ ? classes.credit_card_root_hidden
93
+ : classes.credit_card_root;
94
+
95
+ const billingAddressFieldsClassName = isBillingAddressSame
96
+ ? classes.billing_address_fields_root_hidden
97
+ : classes.billing_address_fields_root;
98
+
99
+ /**
100
+ * Instead of defining classes={root: classes.FIELD_NAME}
101
+ * we are using useMemo to only do it once (hopefully).
102
+ */
103
+ const fieldClasses = useMemo(() => {
104
+ return [
105
+ 'first_name',
106
+ 'last_name',
107
+ 'country',
108
+ 'street1',
109
+ 'street2',
110
+ 'city',
111
+ 'region',
112
+ 'postal_code',
113
+ 'phone_number'
114
+ ].reduce((acc, fieldName) => {
115
+ acc[fieldName] = { root: classes[fieldName] };
116
+
117
+ return acc;
118
+ }, {});
119
+ }, [classes]);
120
+
121
+ /**
122
+ * These 2 functions are wrappers around the `isRequired` function
123
+ * of `formValidators`. They perform validations only if the
124
+ * billing address is different from shipping address.
125
+ *
126
+ * We write this function in `venia-ui` and not in the `peregrine` talon
127
+ * because it references `isRequired` which is a `venia-ui` function.
128
+ */
129
+ const isFieldRequired = useCallback((value, { isBillingAddressSame }) => {
130
+ if (isBillingAddressSame) {
131
+ /**
132
+ * Informed validator functions return `undefined` if
133
+ * validation is `true`
134
+ */
135
+ return undefined;
136
+ } else {
137
+ return isRequired(value);
138
+ }
139
+ }, []);
140
+
141
+ const stepTitle = STEP_DESCRIPTIONS[stepNumber].defaultMessage
142
+ ? formatMessage({
143
+ id: STEP_DESCRIPTIONS[stepNumber].id,
144
+ defaultMessage: STEP_DESCRIPTIONS[stepNumber].defaultMessage
145
+ })
146
+ : formatMessage({
147
+ id: 'checkoutPage.loadingPayment',
148
+ defaultMessage: 'Loading Payment'
149
+ });
150
+
151
+ const loadingIndicator = isLoading ? (
152
+ <LoadingIndicator>{stepTitle}</LoadingIndicator>
153
+ ) : null;
154
+
155
+ return (
156
+ <div className={classes.root} data-cy="Xendit-root">
157
+ <div className={creditCardComponentClassName}>
158
+ <FormError
159
+ allowErrorMessages
160
+ classes={{ root: classes.formErrorContainer }}
161
+ errors={Array.from(errors.values())}
162
+ />
163
+ <div className={classes.dropin_root}>
164
+ <BrainTreeDropin
165
+ onError={onPaymentError}
166
+ onReady={onPaymentReady}
167
+ onSuccess={onPaymentSuccess}
168
+ shouldRequestPaymentNonce={shouldRequestPaymentNonce}
169
+ shouldTeardownDropin={shouldTeardownDropin}
170
+ resetShouldTeardownDropin={resetShouldTeardownDropin}
171
+ />
172
+ </div>
173
+ <div
174
+ data-cy="Xendit-AddressCheck-root"
175
+ className={classes.address_check}
176
+ >
177
+ <Checkbox
178
+ data-cy="PaymentInformation-billingAddressSame"
179
+ field="isBillingAddressSame"
180
+ label={formatMessage({
181
+ id: 'checkoutPage.billingAddressSame',
182
+ defaultMessage:
183
+ 'Billing address same as shipping address'
184
+ })}
185
+ initialValue={initialValues.isBillingAddressSame}
186
+ />
187
+ </div>
188
+ <div
189
+ data-cy="Xendit-billingAddressFields"
190
+ className={billingAddressFieldsClassName}
191
+ >
192
+ <Field
193
+ id="firstName"
194
+ classes={fieldClasses.first_name}
195
+ label={formatMessage({
196
+ id: 'global.firstName',
197
+ defaultMessage: 'First Name'
198
+ })}
199
+ >
200
+ <TextInput
201
+ data-cy="Xendit-billingAddress-firstname"
202
+ id="firstName"
203
+ field="firstName"
204
+ validate={isFieldRequired}
205
+ initialValue={initialValues.firstName}
206
+ />
207
+ </Field>
208
+ <Field
209
+ id="lastName"
210
+ classes={fieldClasses.last_name}
211
+ label={formatMessage({
212
+ id: 'global.lastName',
213
+ defaultMessage: 'Last Name'
214
+ })}
215
+ >
216
+ <TextInput
217
+ data-cy="Xendit-billingAddress-lastname"
218
+ id="lastName"
219
+ field="lastName"
220
+ validate={isFieldRequired}
221
+ initialValue={initialValues.lastName}
222
+ />
223
+ </Field>
224
+ <Country
225
+ data-cy="Xendit-billingAddress-country"
226
+ classes={fieldClasses.country}
227
+ validate={isFieldRequired}
228
+ initialValue={
229
+ /**
230
+ * If there is no initial value to start with
231
+ * use the country from shipping address.
232
+ */
233
+ initialValues.country || shippingAddressCountry
234
+ }
235
+ />
236
+ <Field
237
+ id="street1"
238
+ classes={fieldClasses.street1}
239
+ label={formatMessage({
240
+ id: 'global.streetAddress',
241
+ defaultMessage: 'Street Address'
242
+ })}
243
+ >
244
+ <TextInput
245
+ data-cy="Xendit-billingAddress-street1"
246
+ id="street1"
247
+ field="street1"
248
+ validate={isFieldRequired}
249
+ initialValue={initialValues.street1}
250
+ />
251
+ </Field>
252
+ <Field
253
+ id="street2"
254
+ classes={fieldClasses.street2}
255
+ label={formatMessage({
256
+ id: 'global.streetAddress2',
257
+ defaultMessage: 'Street Address 2'
258
+ })}
259
+ optional={true}
260
+ >
261
+ <TextInput
262
+ data-cy="Xendit-billingAddress-street2"
263
+ id="street2"
264
+ field="street2"
265
+ initialValue={initialValues.street2}
266
+ />
267
+ </Field>
268
+ <Field
269
+ id="city"
270
+ classes={fieldClasses.city}
271
+ label={formatMessage({
272
+ id: 'global.city',
273
+ defaultMessage: 'City'
274
+ })}
275
+ >
276
+ <TextInput
277
+ data-cy="Xendit-billingAddress-city"
278
+ id="city"
279
+ field="city"
280
+ validate={isFieldRequired}
281
+ initialValue={initialValues.city}
282
+ />
283
+ </Field>
284
+ <Region
285
+ data-cy="Xendit-billingAddress-region"
286
+ classes={fieldClasses.region}
287
+ initialValue={initialValues.region}
288
+ validate={isFieldRequired}
289
+ fieldInput={'region[label]'}
290
+ fieldSelect={'region[region_id]'}
291
+ optionValueKey={'id'}
292
+ />
293
+ <Postcode
294
+ data-cy="Xendit-billingAddress-postcode"
295
+ classes={fieldClasses.postal_code}
296
+ validate={isFieldRequired}
297
+ initialValue={initialValues.postcode}
298
+ />
299
+ <Field
300
+ id="phoneNumber"
301
+ classes={fieldClasses.phone_number}
302
+ label={formatMessage({
303
+ id: 'global.phoneNumber',
304
+ defaultMessage: 'Phone Number'
305
+ })}
306
+ >
307
+ <TextInput
308
+ data-cy="Xendit-billingAddress-phoneNumber"
309
+ id="phoneNumber"
310
+ field="phoneNumber"
311
+ validate={isFieldRequired}
312
+ initialValue={initialValues.phoneNumber}
313
+ />
314
+ </Field>
315
+ </div>
316
+ <GoogleReCaptcha {...recaptchaWidgetProps} />
317
+ </div>
318
+ {loadingIndicator}
319
+ </div>
320
+ );
321
+ };
322
+
323
+ export default Xendit;
324
+
325
+ Xendit.propTypes = {
326
+ classes: shape({
327
+ root: string,
328
+ dropin_root: string,
329
+ billing_address_fields_root: string,
330
+ first_name: string,
331
+ last_name: string,
332
+ city: string,
333
+ region: string,
334
+ postal_code: string,
335
+ phone_number: string,
336
+ country: string,
337
+ street1: string,
338
+ street2: string,
339
+ address_check: string,
340
+ credit_card_root: string,
341
+ credit_card_root_hidden: string
342
+ }),
343
+ shouldSubmit: bool.isRequired,
344
+ onPaymentSuccess: func,
345
+ onPaymentReady: func,
346
+ onPaymentError: func,
347
+ resetShouldSubmit: func.isRequired
348
+ };
@@ -0,0 +1,58 @@
1
+ .root {
2
+ }
3
+
4
+ .credit_card_root {
5
+ composes: visible from global;
6
+ composes: opacity-100 from global;
7
+ transition-delay: 64ms;
8
+ transition-duration: 384ms;
9
+ transition-property: opacity, visbility;
10
+ transition-timing-function: var(--venia-global-anim-standard);
11
+ }
12
+
13
+ .credit_card_root_hidden {
14
+ composes: h-0 from global;
15
+ composes: invisible from global;
16
+ composes: opacity-0 from global;
17
+ composes: overflow-hidden from global;
18
+ }
19
+
20
+ .dropin_root {
21
+ }
22
+
23
+ .billing_address_fields_root {
24
+ composes: gap-x-xs from global;
25
+ composes: gap-y-sm from global;
26
+ composes: grid from global;
27
+ composes: px-0 from global;
28
+ composes: py-xs from global;
29
+ }
30
+
31
+ .billing_address_fields_root_hidden {
32
+ composes: h-0 from global;
33
+ composes: invisible from global;
34
+ composes: opacity-0 from global;
35
+ composes: overflow-hidden from global;
36
+ }
37
+
38
+ .formErrorContainer {
39
+ composes: pt-sm from global;
40
+ }
41
+
42
+ .first_name,
43
+ .last_name {
44
+ composes: col-end-span2 from global;
45
+
46
+ composes: lg_col-end-span1 from global;
47
+ }
48
+
49
+ .country,
50
+ .street1,
51
+ .street2,
52
+ .address_check,
53
+ .city,
54
+ .region,
55
+ .postal_code,
56
+ .phone_number {
57
+ composes: col-end-span2 from global;
58
+ }
package/src/intercept.js CHANGED
@@ -220,4 +220,18 @@ module.exports = targets => {
220
220
  routesArray.push(...routes);
221
221
  return routesArray;
222
222
  });
223
+
224
+ targets.of('@magento/venia-ui').checkoutPagePaymentTypes.tap(checkoutPagePaymentTypes =>
225
+ checkoutPagePaymentTypes.add({
226
+ paymentCode: 'xendit',
227
+ importPath: '@riosst100/pwa-marketplace/src/components/Xendit/xendit.js'
228
+ })
229
+ );
230
+
231
+ // targets.of('@magento/venia-ui').summaryPagePaymentTypes.tap(summaryPagePaymentTypes =>
232
+ // summaryPagePaymentTypes.add({
233
+ // paymentCode: 'xendit',
234
+ // importPath: '@riosst100/pwa-marketplace/src/components/Xendit/summary.js'
235
+ // })
236
+ // );
223
237
  };
@@ -16,7 +16,7 @@ const OrderSummary = props => {
16
16
  />
17
17
  </h2>
18
18
  <PriceSummary isUpdating={props.isUpdating} />
19
- <style jsx>
19
+ <style jsx="true">
20
20
  {`
21
21
  li[class*="priceSummary-lineItems"] span:first-child {
22
22
  margin-top: 0;
@@ -37,6 +37,12 @@ const PaymentMethods = props => {
37
37
  return null;
38
38
  }
39
39
 
40
+ console.log('payments')
41
+ console.log(payments)
42
+
43
+ console.log('availablePaymentMethods')
44
+ console.log(availablePaymentMethods)
45
+
40
46
  const radios = availablePaymentMethods
41
47
  .map(({ code, title }) => {
42
48
  // If we don't have an implementation for a method type, ignore it.
@@ -74,6 +80,9 @@ const PaymentMethods = props => {
74
80
  })
75
81
  .filter(paymentMethod => !!paymentMethod);
76
82
 
83
+ console.log('radios != [] harusny')
84
+ console.log(radios)
85
+
77
86
  const noPaymentMethodMessage = !radios.length ? (
78
87
  <div className={classes.payment_errors}>
79
88
  <span>
@@ -0,0 +1,574 @@
1
+ import { useCallback, useEffect, useState, useMemo } from 'react';
2
+ import { useFormState, useFormApi } from 'informed';
3
+ import { useQuery, useApolloClient, useMutation } from '@apollo/client';
4
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
5
+
6
+ import { useCartContext } from '@magento/peregrine/lib/context/cart';
7
+
8
+ import DEFAULT_OPERATIONS from './xendit.gql';
9
+ import { useGoogleReCaptcha } from '@magento/peregrine/lib/hooks/useGoogleReCaptcha';
10
+
11
+ const getRegion = region => {
12
+ return region.region_id || region.label || region.code;
13
+ };
14
+
15
+ /**
16
+ * Maps address response data from GET_BILLING_ADDRESS and GET_SHIPPING_ADDRESS
17
+ * queries to input names in the billing address form.
18
+ * {@link creditCard.gql.js}.
19
+ *
20
+ * @param {ShippingCartAddress|BillingCartAddress} rawAddressData query data
21
+ */
22
+ export const mapAddressData = rawAddressData => {
23
+ if (rawAddressData) {
24
+ const {
25
+ firstName,
26
+ lastName,
27
+ city,
28
+ postcode,
29
+ phoneNumber,
30
+ street,
31
+ country,
32
+ region
33
+ } = rawAddressData;
34
+
35
+ return {
36
+ firstName,
37
+ lastName,
38
+ city,
39
+ postcode,
40
+ phoneNumber,
41
+ street1: street[0],
42
+ street2: street[1] || '',
43
+ country: country.code,
44
+ region: getRegion(region)
45
+ };
46
+ } else {
47
+ return {};
48
+ }
49
+ };
50
+
51
+ /**
52
+ * Talon to handle Credit Card payment method.
53
+ *
54
+ * @param {Boolean} props.shouldSubmit boolean value which represents if a payment nonce request has been submitted
55
+ * @param {Function} props.onSuccess callback to invoke when the a payment nonce has been generated
56
+ * @param {Function} props.onReady callback to invoke when the braintree dropin component is ready
57
+ * @param {Function} props.onError callback to invoke when the braintree dropin component throws an error
58
+ * @param {Function} props.resetShouldSubmit callback to reset the shouldSubmit flag
59
+ * @param {DocumentNode} props.operations.getBillingAddressQuery query to fetch billing address from cache
60
+ * @param {DocumentNode} props.operations.getIsBillingAddressSameQuery query to fetch is billing address same checkbox value from cache
61
+ * @param {DocumentNode} props.operations.getPaymentNonceQuery query to fetch payment nonce saved in cache
62
+ * @param {DocumentNode} props.operations.setBillingAddressMutation mutation to update billing address on the cart
63
+ * @param {DocumentNode} props.operations.setCreditCardDetailsOnCartMutation mutation to update payment method and payment nonce on the cart
64
+ *
65
+ * @returns {
66
+ * errors: Map<String, Error>,
67
+ * shouldRequestPaymentNonce: Boolean,
68
+ * onPaymentError: Function,
69
+ * onPaymentSuccess: Function,
70
+ * onPaymentReady: Function,
71
+ * isBillingAddressSame: Boolean,
72
+ * isLoading: Boolean,
73
+ * stepNumber: Number,
74
+ * initialValues: {
75
+ * firstName: String,
76
+ * lastName: String,
77
+ * city: String,
78
+ * postcode: String,
79
+ * phoneNumber: String,
80
+ * street1: String,
81
+ * street2: String,
82
+ * country: String,
83
+ * state: String,
84
+ * isBillingAddressSame: Boolean
85
+ * },
86
+ * shippingAddressCountry: String,
87
+ * shouldTeardownDropin: Boolean,
88
+ * resetShouldTeardownDropin: Function
89
+ * }
90
+ */
91
+ export const useXendit = props => {
92
+ const {
93
+ onSuccess,
94
+ onReady,
95
+ onError,
96
+ shouldSubmit,
97
+ resetShouldSubmit
98
+ } = props;
99
+
100
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
101
+
102
+ const {
103
+ getBillingAddressQuery,
104
+ getIsBillingAddressSameQuery,
105
+ getPaymentNonceQuery,
106
+ getShippingAddressQuery,
107
+ setBillingAddressMutation,
108
+ setCreditCardDetailsOnCartMutation
109
+ } = operations;
110
+
111
+ const {
112
+ recaptchaLoading,
113
+ generateReCaptchaData,
114
+ recaptchaWidgetProps
115
+ } = useGoogleReCaptcha({
116
+ currentForm: 'BRAINTREE',
117
+ formAction: 'braintree'
118
+ });
119
+
120
+ /**
121
+ * Definitions
122
+ */
123
+
124
+ const [isDropinLoading, setDropinLoading] = useState(true);
125
+ const [shouldRequestPaymentNonce, setShouldRequestPaymentNonce] = useState(
126
+ false
127
+ );
128
+ const [shouldTeardownDropin, setShouldTeardownDropin] = useState(false);
129
+ /**
130
+ * `stepNumber` depicts the state of the process flow in credit card
131
+ * payment flow.
132
+ *
133
+ * `0` No call made yet
134
+ * `1` Billing address mutation initiated
135
+ * `2` Braintree nonce requested
136
+ * `3` Payment information mutation initiated
137
+ * `4` All mutations done
138
+ */
139
+ const [stepNumber, setStepNumber] = useState(0);
140
+
141
+ const client = useApolloClient();
142
+ const formState = useFormState();
143
+ const { validate: validateBillingAddressForm } = useFormApi();
144
+ const [{ cartId }] = useCartContext();
145
+
146
+ const isLoading =
147
+ isDropinLoading ||
148
+ recaptchaLoading ||
149
+ (stepNumber >= 1 && stepNumber <= 3);
150
+
151
+ const { data: billingAddressData } = useQuery(getBillingAddressQuery, {
152
+ skip: !cartId,
153
+ variables: { cartId }
154
+ });
155
+ const { data: shippingAddressData } = useQuery(getShippingAddressQuery, {
156
+ skip: !cartId,
157
+ variables: { cartId }
158
+ });
159
+ const { data: isBillingAddressSameData } = useQuery(
160
+ getIsBillingAddressSameQuery,
161
+ { skip: !cartId, variables: { cartId } }
162
+ );
163
+ const [
164
+ updateBillingAddress,
165
+ {
166
+ error: billingAddressMutationError,
167
+ called: billingAddressMutationCalled,
168
+ loading: billingAddressMutationLoading
169
+ }
170
+ ] = useMutation(setBillingAddressMutation);
171
+
172
+ const [
173
+ updateCCDetails,
174
+ {
175
+ error: ccMutationError,
176
+ called: ccMutationCalled,
177
+ loading: ccMutationLoading
178
+ }
179
+ ] = useMutation(setCreditCardDetailsOnCartMutation);
180
+
181
+ const shippingAddressCountry = shippingAddressData
182
+ ? shippingAddressData.cart.shippingAddresses[0].country.code
183
+ : DEFAULT_COUNTRY_CODE;
184
+ const isBillingAddressSame = formState.values.isBillingAddressSame;
185
+
186
+ const initialValues = useMemo(() => {
187
+ const isBillingAddressSame = isBillingAddressSameData
188
+ ? isBillingAddressSameData.cart.isBillingAddressSame
189
+ : true;
190
+
191
+ let billingAddress = {};
192
+ /**
193
+ * If billing address is same as shipping address, do
194
+ * not auto fill the fields.
195
+ */
196
+ if (billingAddressData && !isBillingAddressSame) {
197
+ if (billingAddressData.cart.billingAddress) {
198
+ const {
199
+ // eslint-disable-next-line no-unused-vars
200
+ __typename,
201
+ ...rawBillingAddress
202
+ } = billingAddressData.cart.billingAddress;
203
+ billingAddress = mapAddressData(rawBillingAddress);
204
+ }
205
+ }
206
+
207
+ return { isBillingAddressSame, ...billingAddress };
208
+ }, [isBillingAddressSameData, billingAddressData]);
209
+
210
+ /**
211
+ * Helpers
212
+ */
213
+
214
+ /**
215
+ * This function sets the boolean isBillingAddressSame
216
+ * in cache for future use. We use cache because there
217
+ * is no way to save this on the cart in remote.
218
+ */
219
+ const setIsBillingAddressSameInCache = useCallback(() => {
220
+ client.writeQuery({
221
+ query: getIsBillingAddressSameQuery,
222
+ data: {
223
+ cart: {
224
+ __typename: 'Cart',
225
+ id: cartId,
226
+ isBillingAddressSame
227
+ }
228
+ }
229
+ });
230
+ }, [client, cartId, getIsBillingAddressSameQuery, isBillingAddressSame]);
231
+
232
+ /**
233
+ * This function sets the billing address on the cart using the
234
+ * shipping address.
235
+ */
236
+ const setShippingAddressAsBillingAddress = useCallback(() => {
237
+ var shippingAddress = shippingAddressData
238
+ ? mapAddressData(shippingAddressData.cart.shippingAddresses[0])
239
+ : {};
240
+
241
+ shippingAddress.region =
242
+ shippingAddress.region == null ? '' : shippingAddress.region;
243
+
244
+ updateBillingAddress({
245
+ variables: {
246
+ cartId,
247
+ ...shippingAddress,
248
+ sameAsShipping: true
249
+ }
250
+ });
251
+ }, [updateBillingAddress, shippingAddressData, cartId]);
252
+
253
+ /**
254
+ * This function sets the billing address on the cart using the
255
+ * information from the form.
256
+ */
257
+ const setBillingAddress = useCallback(() => {
258
+ const {
259
+ firstName,
260
+ lastName,
261
+ country,
262
+ street1,
263
+ street2,
264
+ city,
265
+ region,
266
+ postcode,
267
+ phoneNumber
268
+ } = formState.values;
269
+
270
+ updateBillingAddress({
271
+ variables: {
272
+ cartId,
273
+ firstName,
274
+ lastName,
275
+ country,
276
+ street1,
277
+ street2: street2 || '',
278
+ city,
279
+ region: getRegion(region),
280
+ postcode,
281
+ phoneNumber,
282
+ sameAsShipping: false
283
+ }
284
+ });
285
+ }, [formState.values, updateBillingAddress, cartId]);
286
+
287
+ /**
288
+ * This function sets the payment nonce details in the cache.
289
+ * We use cache because there is no way to save this information
290
+ * on the cart in the remote.
291
+ *
292
+ * We do not save the nonce code because it is a PII.
293
+ */
294
+ const setPaymentDetailsInCache = useCallback(
295
+ braintreeNonce => {
296
+ /**
297
+ * We dont save the nonce code due to PII,
298
+ * we only save the subset of details.
299
+ */
300
+ const { details, description, type } = braintreeNonce;
301
+ client.writeQuery({
302
+ query: getPaymentNonceQuery,
303
+ data: {
304
+ cart: {
305
+ __typename: 'Cart',
306
+ id: cartId,
307
+ paymentNonce: {
308
+ details,
309
+ description,
310
+ type
311
+ }
312
+ }
313
+ }
314
+ });
315
+ },
316
+ [cartId, client, getPaymentNonceQuery]
317
+ );
318
+
319
+ /**
320
+ * This function saves the nonce code from braintree
321
+ * on the cart along with the payment method used in
322
+ * this case `braintree`.
323
+ */
324
+ const updateCCDetailsOnCart = useCallback(
325
+ async braintreeNonce => {
326
+ try {
327
+ const { nonce } = braintreeNonce;
328
+ const reCaptchaData = await generateReCaptchaData();
329
+
330
+ await updateCCDetails({
331
+ variables: {
332
+ cartId,
333
+ paymentMethod: 'braintree',
334
+ paymentNonce: nonce
335
+ },
336
+ ...reCaptchaData
337
+ });
338
+ } catch (error) {
339
+ // Error is logged by apollo link - no need to double log.
340
+ }
341
+ },
342
+ [updateCCDetails, cartId, generateReCaptchaData]
343
+ );
344
+
345
+ /**
346
+ * Function to be called by the braintree dropin when the
347
+ * nonce generation is successful.
348
+ */
349
+ const onPaymentSuccess = useCallback(
350
+ braintreeNonce => {
351
+ setPaymentDetailsInCache(braintreeNonce);
352
+ /**
353
+ * Updating payment braintreeNonce and selected payment method on cart.
354
+ */
355
+ updateCCDetailsOnCart(braintreeNonce);
356
+ setStepNumber(3);
357
+ },
358
+ [setPaymentDetailsInCache, updateCCDetailsOnCart]
359
+ );
360
+
361
+ /**
362
+ * Function to be called by the braintree dropin when the
363
+ * nonce generation is not successful.
364
+ */
365
+ const onPaymentError = useCallback(
366
+ error => {
367
+ setStepNumber(0);
368
+ setShouldRequestPaymentNonce(false);
369
+ resetShouldSubmit();
370
+ if (onError) {
371
+ onError(error);
372
+ }
373
+ },
374
+ [onError, resetShouldSubmit]
375
+ );
376
+
377
+ /**
378
+ * Function to be called by the braintree dropin when the
379
+ * credit card component has loaded successfully.
380
+ */
381
+ const onPaymentReady = useCallback(() => {
382
+ setDropinLoading(false);
383
+ setStepNumber(0);
384
+ if (onReady) {
385
+ onReady();
386
+ }
387
+ }, [onReady]);
388
+
389
+ /**
390
+ * Function to be called by braintree dropin when the payment
391
+ * teardown is done successfully before re creating the new dropin.
392
+ */
393
+ const resetShouldTeardownDropin = useCallback(() => {
394
+ setShouldTeardownDropin(false);
395
+ }, []);
396
+
397
+ /**
398
+ * Effects
399
+ */
400
+
401
+ /**
402
+ * Step 1 effect
403
+ *
404
+ * User has clicked the update button
405
+ */
406
+ useEffect(() => {
407
+ try {
408
+ if (shouldSubmit) {
409
+ /**
410
+ * Validate billing address fields and only process with
411
+ * submit if there are no errors.
412
+ *
413
+ * We do this because the user can click Review Order button
414
+ * without fillig in all fields and the form submission
415
+ * happens manually. The informed Form component validates
416
+ * on submission but that only happens when we use the onSubmit
417
+ * prop. In this case we are using manually submission because
418
+ * of the nature of the credit card submission process.
419
+ */
420
+ validateBillingAddressForm();
421
+
422
+ const hasErrors = Object.keys(formState.errors).length;
423
+
424
+ if (!hasErrors) {
425
+ setStepNumber(1);
426
+ if (isBillingAddressSame) {
427
+ setShippingAddressAsBillingAddress();
428
+ } else {
429
+ setBillingAddress();
430
+ }
431
+ setIsBillingAddressSameInCache();
432
+ } else {
433
+ throw new Error('Errors in the billing address form');
434
+ }
435
+ }
436
+ } catch (err) {
437
+ if (process.env.NODE_ENV !== 'production') {
438
+ console.error(err);
439
+ }
440
+ setStepNumber(0);
441
+ resetShouldSubmit();
442
+ setShouldRequestPaymentNonce(false);
443
+ }
444
+ }, [
445
+ shouldSubmit,
446
+ isBillingAddressSame,
447
+ setShippingAddressAsBillingAddress,
448
+ setBillingAddress,
449
+ setIsBillingAddressSameInCache,
450
+ resetShouldSubmit,
451
+ validateBillingAddressForm,
452
+ formState.errors
453
+ ]);
454
+
455
+ /**
456
+ * Step 2 effect
457
+ *
458
+ * Billing address mutation has completed
459
+ */
460
+ useEffect(() => {
461
+ try {
462
+ const billingAddressMutationCompleted =
463
+ billingAddressMutationCalled && !billingAddressMutationLoading;
464
+
465
+ if (
466
+ billingAddressMutationCompleted &&
467
+ !billingAddressMutationError
468
+ ) {
469
+ /**
470
+ * Billing address save mutation is successful
471
+ * we can initiate the braintree nonce request
472
+ */
473
+ setStepNumber(2);
474
+ setShouldRequestPaymentNonce(true);
475
+ }
476
+
477
+ if (
478
+ billingAddressMutationCompleted &&
479
+ billingAddressMutationError
480
+ ) {
481
+ /**
482
+ * Billing address save mutation is not successful.
483
+ * Reset update button clicked flag.
484
+ */
485
+ throw new Error('Billing address mutation failed');
486
+ }
487
+ } catch (err) {
488
+ if (process.env.NODE_ENV !== 'production') {
489
+ console.error(err);
490
+ }
491
+ setStepNumber(0);
492
+ resetShouldSubmit();
493
+ setShouldRequestPaymentNonce(false);
494
+ }
495
+ }, [
496
+ billingAddressMutationError,
497
+ billingAddressMutationCalled,
498
+ billingAddressMutationLoading,
499
+ resetShouldSubmit
500
+ ]);
501
+
502
+ /**
503
+ * Step 3 effect
504
+ *
505
+ * Credit card save mutation has completed
506
+ */
507
+ useEffect(() => {
508
+ /**
509
+ * Saved billing address, payment method and payment nonce on cart.
510
+ *
511
+ * Time to call onSuccess.
512
+ */
513
+
514
+ try {
515
+ const ccMutationCompleted = ccMutationCalled && !ccMutationLoading;
516
+
517
+ if (ccMutationCompleted && !ccMutationError) {
518
+ if (onSuccess) {
519
+ onSuccess();
520
+ }
521
+ resetShouldSubmit();
522
+ setStepNumber(4);
523
+ }
524
+
525
+ if (ccMutationCompleted && ccMutationError) {
526
+ /**
527
+ * If credit card mutation failed, reset update button clicked so the
528
+ * user can click again and set `stepNumber` to 0.
529
+ */
530
+ throw new Error('Credit card nonce save mutation failed.');
531
+ }
532
+ } catch (err) {
533
+ if (process.env.NODE_ENV !== 'production') {
534
+ console.error(err);
535
+ }
536
+ setStepNumber(0);
537
+ resetShouldSubmit();
538
+ setShouldRequestPaymentNonce(false);
539
+ setShouldTeardownDropin(true);
540
+ }
541
+ }, [
542
+ ccMutationCalled,
543
+ ccMutationLoading,
544
+ onSuccess,
545
+ setShouldRequestPaymentNonce,
546
+ resetShouldSubmit,
547
+ ccMutationError
548
+ ]);
549
+
550
+ const errors = useMemo(
551
+ () =>
552
+ new Map([
553
+ ['setBillingAddressMutation', billingAddressMutationError],
554
+ ['setCreditCardDetailsOnCartMutation', ccMutationError]
555
+ ]),
556
+ [billingAddressMutationError, ccMutationError]
557
+ );
558
+
559
+ return {
560
+ errors,
561
+ onPaymentError,
562
+ onPaymentSuccess,
563
+ onPaymentReady,
564
+ isBillingAddressSame,
565
+ isLoading,
566
+ shouldRequestPaymentNonce,
567
+ stepNumber,
568
+ initialValues,
569
+ shippingAddressCountry,
570
+ shouldTeardownDropin,
571
+ resetShouldTeardownDropin,
572
+ recaptchaWidgetProps
573
+ };
574
+ };
@@ -0,0 +1,165 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ import { PriceSummaryFragment } from '@magento/peregrine/lib/talons/CartPage/PriceSummary/priceSummaryFragments.gql';
4
+ import { AvailablePaymentMethodsFragment } from '@magento/peregrine/lib/talons/CheckoutPage/PaymentInformation/paymentInformation.gql';
5
+
6
+ export const GET_IS_BILLING_ADDRESS_SAME = gql`
7
+ query getIsBillingAddressSame($cartId: String!) {
8
+ cart(cart_id: $cartId) @client {
9
+ id
10
+ isBillingAddressSame
11
+ }
12
+ }
13
+ `;
14
+
15
+ export const GET_PAYMENT_NONCE = gql`
16
+ query getPaymentNonce($cartId: String!) {
17
+ cart(cart_id: $cartId) @client {
18
+ id
19
+ paymentNonce
20
+ }
21
+ }
22
+ `;
23
+
24
+ export const GET_BILLING_ADDRESS = gql`
25
+ query getBillingAddress($cartId: String!) {
26
+ cart(cart_id: $cartId) {
27
+ id
28
+ billingAddress: billing_address {
29
+ firstName: firstname
30
+ lastName: lastname
31
+ country {
32
+ code
33
+ }
34
+ street
35
+ city
36
+ region {
37
+ code
38
+ label
39
+ region_id
40
+ }
41
+ postcode
42
+ phoneNumber: telephone
43
+ }
44
+ }
45
+ }
46
+ `;
47
+
48
+ export const GET_SHIPPING_ADDRESS = gql`
49
+ query getSelectedShippingAddress($cartId: String!) {
50
+ cart(cart_id: $cartId) {
51
+ id
52
+ shippingAddresses: shipping_addresses {
53
+ firstName: firstname
54
+ lastName: lastname
55
+ country {
56
+ code
57
+ }
58
+ street
59
+ city
60
+ region {
61
+ code
62
+ label
63
+ region_id
64
+ }
65
+ postcode
66
+ phoneNumber: telephone
67
+ }
68
+ }
69
+ }
70
+ `;
71
+
72
+ export const SET_BILLING_ADDRESS = gql`
73
+ mutation setBillingAddress(
74
+ $cartId: String!
75
+ $firstName: String!
76
+ $lastName: String!
77
+ $street1: String!
78
+ $street2: String
79
+ $city: String!
80
+ $region: String!
81
+ $postcode: String!
82
+ $country: String!
83
+ $phoneNumber: String!
84
+ ) {
85
+ setBillingAddressOnCart(
86
+ input: {
87
+ cart_id: $cartId
88
+ billing_address: {
89
+ address: {
90
+ firstname: $firstName
91
+ lastname: $lastName
92
+ street: [$street1, $street2]
93
+ city: $city
94
+ region: $region
95
+ postcode: $postcode
96
+ country_code: $country
97
+ telephone: $phoneNumber
98
+ save_in_address_book: false
99
+ }
100
+ }
101
+ }
102
+ ) {
103
+ cart {
104
+ id
105
+ billing_address {
106
+ firstname
107
+ lastname
108
+ country {
109
+ code
110
+ }
111
+ street
112
+ city
113
+ region {
114
+ code
115
+ label
116
+ region_id
117
+ }
118
+ postcode
119
+ telephone
120
+ }
121
+ ...PriceSummaryFragment
122
+ ...AvailablePaymentMethodsFragment
123
+ }
124
+ }
125
+ }
126
+ ${PriceSummaryFragment}
127
+ ${AvailablePaymentMethodsFragment}
128
+ `;
129
+
130
+ export const SET_CC_DETAILS_ON_CART = gql`
131
+ mutation setSelectedPaymentMethod(
132
+ $cartId: String!
133
+ $paymentNonce: String!
134
+ ) {
135
+ setPaymentMethodOnCart(
136
+ input: {
137
+ cart_id: $cartId
138
+ payment_method: {
139
+ code: "braintree"
140
+ braintree: {
141
+ payment_method_nonce: $paymentNonce
142
+ is_active_payment_token_enabler: false
143
+ }
144
+ }
145
+ }
146
+ ) {
147
+ cart {
148
+ id
149
+ selected_payment_method {
150
+ code
151
+ title
152
+ }
153
+ }
154
+ }
155
+ }
156
+ `;
157
+
158
+ export default {
159
+ getBillingAddressQuery: GET_BILLING_ADDRESS,
160
+ getIsBillingAddressSameQuery: GET_IS_BILLING_ADDRESS_SAME,
161
+ getPaymentNonceQuery: GET_PAYMENT_NONCE,
162
+ getShippingAddressQuery: GET_SHIPPING_ADDRESS,
163
+ setBillingAddressMutation: SET_BILLING_ADDRESS,
164
+ setCreditCardDetailsOnCartMutation: SET_CC_DETAILS_ON_CART
165
+ };