Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit 14aa5eb

Browse files
mikejolleyoprsenadir
authored
Experimental newsletter subscription checkbox block POC and Store API supporting changes (#4607)
* remove todo from sample block * Add newsletter block * Block registration * Move provider/processor so separate them from context providers * customData implementation for setting customData for requests * Make data and schema callbacks optional in extendrestapi class * schema_type should be data_type * Allow checkout endpoint to be extended * Support validation, sanitization, and defaults on nested REST properties * Experimental endpoint data for newsletter field * Add extension data to requests * SET_EXTENSION_DATA * Update types * Add todo * move check within hook function * Remove newsletter block This is because we're testing with the integration being done in a separate extension * Delete newsletter subscription block * Pass the result of hooks down to the children blocks We need to do this to allow extension blocks to modify the extensionData (so they can send custom input to the REST api when submitting the checkout form). * Remove newsletter signup block * remove checkoutSubmitData Co-authored-by: Thomas Roberts <[email protected]> Co-authored-by: Nadir Seghir <[email protected]>
1 parent 0e3bc00 commit 14aa5eb

File tree

25 files changed

+472
-274
lines changed

25 files changed

+472
-274
lines changed

assets/js/base/context/hooks/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './use-checkout-address';
1212
export * from './use-checkout-notices';
1313
export * from './use-checkout-submit';
1414
export * from './use-emit-response';
15+
export * from './use-checkout-extension-data';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { useCallback, useEffect, useRef } from '@wordpress/element';
5+
import isShallowEqual from '@wordpress/is-shallow-equal';
6+
7+
/**
8+
* Internal dependencies
9+
*/
10+
import { useCheckoutContext } from '../providers/cart-checkout/checkout-state';
11+
import type { CheckoutStateContextState } from '../providers/cart-checkout/checkout-state/types';
12+
13+
/**
14+
* Custom hook for setting custom checkout data which is passed to the wc/store/checkout endpoint when processing orders.
15+
*/
16+
export const useCheckoutExtensionData = (): {
17+
extensionData: CheckoutStateContextState[ 'extensionData' ];
18+
setExtensionData: (
19+
namespace: string,
20+
key: string,
21+
value: unknown
22+
) => void;
23+
} => {
24+
const { dispatchActions, extensionData } = useCheckoutContext();
25+
const extensionDataRef = useRef( extensionData );
26+
27+
useEffect( () => {
28+
if ( ! isShallowEqual( extensionData, extensionDataRef.current ) ) {
29+
extensionDataRef.current = extensionData;
30+
}
31+
}, [ extensionData ] );
32+
33+
const setExtensionDataWithNamespace = useCallback(
34+
( namespace, key, value ) => {
35+
const currentData = extensionDataRef.current[ namespace ] || {};
36+
dispatchActions.setExtensionData( {
37+
...extensionDataRef.current,
38+
[ namespace ]: {
39+
...currentData,
40+
[ key ]: value,
41+
},
42+
} );
43+
},
44+
[ dispatchActions ]
45+
);
46+
47+
return {
48+
extensionData: extensionDataRef.current,
49+
setExtensionData: setExtensionDataWithNamespace,
50+
};
51+
};

assets/js/base/context/providers/cart-checkout/cart/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Internal dependencies
33
*/
4-
import { CheckoutProvider } from '../checkout';
4+
import { CheckoutProvider } from '../checkout-provider';
55

66
/**
77
* Cart provider

assets/js/base/context/providers/cart-checkout/checkout/processor/index.js renamed to assets/js/base/context/providers/cart-checkout/checkout-processor.js

Lines changed: 70 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,18 @@ import {
1818
/**
1919
* Internal dependencies
2020
*/
21-
import { preparePaymentData } from './utils';
22-
import { useCheckoutContext } from '../../checkout-state';
23-
import { useShippingDataContext } from '../../shipping';
24-
import { useCustomerDataContext } from '../../customer';
25-
import { usePaymentMethodDataContext } from '../../payment-methods';
26-
import { useValidationContext } from '../../../validation';
27-
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
28-
import { useStoreNotices } from '../../../../hooks/use-store-notices';
21+
import { preparePaymentData, processCheckoutResponseHeaders } from './utils';
22+
import { useCheckoutContext } from './checkout-state';
23+
import { useShippingDataContext } from './shipping';
24+
import { useCustomerDataContext } from './customer';
25+
import { usePaymentMethodDataContext } from './payment-methods';
26+
import { useValidationContext } from '../validation';
27+
import { useStoreCart } from '../../hooks/cart/use-store-cart';
28+
import { useStoreNotices } from '../../hooks/use-store-notices';
2929

3030
/**
3131
* CheckoutProcessor component.
3232
*
33-
* @todo Needs to consume all contexts.
34-
*
3533
* Subscribes to checkout context and triggers processing via the API.
3634
*/
3735
const CheckoutProcessor = () => {
@@ -45,6 +43,7 @@ const CheckoutProcessor = () => {
4543
isComplete: checkoutIsComplete,
4644
orderNotes,
4745
shouldCreateAccount,
46+
extensionData,
4847
} = useCheckoutContext();
4948
const { hasValidationErrors } = useValidationContext();
5049
const { shippingErrorStatus } = useShippingDataContext();
@@ -75,11 +74,18 @@ const CheckoutProcessor = () => {
7574
currentPaymentStatus.hasError ||
7675
shippingErrorStatus.hasError;
7776

77+
const paidAndWithoutErrors =
78+
! checkoutHasError &&
79+
! checkoutWillHaveError &&
80+
( currentPaymentStatus.isSuccessful || ! cartNeedsPayment ) &&
81+
checkoutIsProcessing;
82+
7883
// If express payment method is active, let's suppress notices
7984
useEffect( () => {
8085
setIsSuppressed( isExpressPaymentMethodActive );
8186
}, [ isExpressPaymentMethodActive, setIsSuppressed ] );
8287

88+
// Determine if checkout has an error.
8389
useEffect( () => {
8490
if (
8591
checkoutWillHaveError !== checkoutHasError &&
@@ -97,12 +103,6 @@ const CheckoutProcessor = () => {
97103
dispatchActions,
98104
] );
99105

100-
const paidAndWithoutErrors =
101-
! checkoutHasError &&
102-
! checkoutWillHaveError &&
103-
( currentPaymentStatus.isSuccessful || ! cartNeedsPayment ) &&
104-
checkoutIsProcessing;
105-
106106
useEffect( () => {
107107
currentBillingData.current = billingData;
108108
currentShippingAddress.current = shippingAddress;
@@ -156,10 +156,32 @@ const CheckoutProcessor = () => {
156156
isExpressPaymentMethodActive,
157157
] );
158158

159-
const processOrder = useCallback( () => {
159+
// redirect when checkout is complete and there is a redirect url.
160+
useEffect( () => {
161+
if ( currentRedirectUrl.current ) {
162+
window.location.href = currentRedirectUrl.current;
163+
}
164+
}, [ checkoutIsComplete ] );
165+
166+
const processOrder = useCallback( async () => {
167+
if ( isProcessingOrder ) {
168+
return;
169+
}
160170
setIsProcessingOrder( true );
161171
removeNotice( 'checkout' );
162-
let data = {
172+
173+
const paymentData = cartNeedsPayment
174+
? {
175+
payment_method: paymentMethodId,
176+
payment_data: preparePaymentData(
177+
paymentMethodData,
178+
shouldSavePayment,
179+
activePaymentMethod
180+
),
181+
}
182+
: {};
183+
184+
const data = {
163185
billing_address: emptyHiddenAddressFields(
164186
currentBillingData.current
165187
),
@@ -168,102 +190,71 @@ const CheckoutProcessor = () => {
168190
),
169191
customer_note: orderNotes,
170192
should_create_account: shouldCreateAccount,
193+
...paymentData,
194+
extensions: { ...extensionData },
171195
};
172-
if ( cartNeedsPayment ) {
173-
data = {
174-
...data,
175-
payment_method: paymentMethodId,
176-
payment_data: preparePaymentData(
177-
paymentMethodData,
178-
shouldSavePayment,
179-
activePaymentMethod
180-
),
181-
};
182-
}
196+
183197
triggerFetch( {
184198
path: '/wc/store/checkout',
185199
method: 'POST',
186200
data,
187201
cache: 'no-store',
188202
parse: false,
189203
} )
190-
.then( ( fetchResponse ) => {
191-
// Update nonce.
192-
triggerFetch.setNonce( fetchResponse.headers );
193-
194-
// Update user using headers.
195-
dispatchActions.setCustomerId(
196-
fetchResponse.headers.get( 'X-WC-Store-API-User' )
204+
.then( ( response ) => {
205+
processCheckoutResponseHeaders(
206+
response.headers,
207+
dispatchActions
197208
);
198-
199-
// Handle response.
200-
fetchResponse.json().then( function ( response ) {
201-
if ( ! fetchResponse.ok ) {
202-
// We received an error response.
203-
addErrorNotice(
204-
formatStoreApiErrorMessage( response ),
205-
{
206-
id: 'checkout',
207-
}
208-
);
209-
dispatchActions.setHasError();
210-
}
211-
dispatchActions.setAfterProcessing( response );
212-
setIsProcessingOrder( false );
213-
} );
214-
} )
215-
.catch( ( errorResponse ) => {
216-
// Update nonce.
217-
triggerFetch.setNonce( errorResponse.headers );
218-
219-
// If new customer ID returned, update the store.
220-
if ( errorResponse.headers?.get( 'X-WC-Store-API-User' ) ) {
221-
dispatchActions.setCustomerId(
222-
errorResponse.headers.get( 'X-WC-Store-API-User' )
223-
);
209+
if ( ! response.ok ) {
210+
throw new Error( response );
224211
}
225-
226-
errorResponse.json().then( function ( response ) {
212+
return response.json();
213+
} )
214+
.then( ( response ) => {
215+
dispatchActions.setAfterProcessing( response );
216+
setIsProcessingOrder( false );
217+
} )
218+
.catch( ( fetchResponse ) => {
219+
processCheckoutResponseHeaders(
220+
fetchResponse.headers,
221+
dispatchActions
222+
);
223+
fetchResponse.json().then( ( response ) => {
227224
// If updated cart state was returned, update the store.
228225
if ( response.data?.cart ) {
229226
receiveCart( response.data.cart );
230227
}
231228
addErrorNotice( formatStoreApiErrorMessage( response ), {
232229
id: 'checkout',
233230
} );
234-
235231
response.additional_errors?.forEach?.(
236232
( additionalError ) => {
237233
addErrorNotice( additionalError.message, {
238234
id: additionalError.error_code,
239235
} );
240236
}
241237
);
242-
243-
dispatchActions.setHasError();
238+
dispatchActions.setHasError( true );
244239
dispatchActions.setAfterProcessing( response );
245240
setIsProcessingOrder( false );
246241
} );
247242
} );
248243
}, [
249-
addErrorNotice,
244+
isProcessingOrder,
250245
removeNotice,
246+
orderNotes,
247+
shouldCreateAccount,
248+
cartNeedsPayment,
251249
paymentMethodId,
252-
activePaymentMethod,
253250
paymentMethodData,
254251
shouldSavePayment,
255-
cartNeedsPayment,
256-
receiveCart,
252+
activePaymentMethod,
253+
extensionData,
257254
dispatchActions,
258-
orderNotes,
259-
shouldCreateAccount,
255+
addErrorNotice,
256+
receiveCart,
260257
] );
261-
// redirect when checkout is complete and there is a redirect url.
262-
useEffect( () => {
263-
if ( currentRedirectUrl.current ) {
264-
window.location.href = currentRedirectUrl.current;
265-
}
266-
}, [ checkoutIsComplete ] );
267258

268259
// process order if conditions are good.
269260
useEffect( () => {

assets/js/base/context/providers/cart-checkout/checkout/index.js renamed to assets/js/base/context/providers/cart-checkout/checkout-provider.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundar
77
/**
88
* Internal dependencies
99
*/
10-
import { PaymentMethodDataProvider } from '../payment-methods';
11-
import { ShippingDataProvider } from '../shipping';
12-
import { CustomerDataProvider } from '../customer';
13-
import { CheckoutStateProvider } from '../checkout-state';
14-
import CheckoutProcessor from './processor';
10+
import { PaymentMethodDataProvider } from './payment-methods';
11+
import { ShippingDataProvider } from './shipping';
12+
import { CustomerDataProvider } from './customer';
13+
import { CheckoutStateProvider } from './checkout-state';
14+
import CheckoutProcessor from './checkout-processor';
1515

1616
/**
1717
* Checkout provider

assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Internal dependencies
33
*/
4-
import type { PaymentResultDataType } from './types';
4+
import type { PaymentResultDataType, CheckoutStateContextState } from './types';
55

66
export enum ACTION {
77
SET_IDLE = 'set_idle',
@@ -20,20 +20,15 @@ export enum ACTION {
2020
INCREMENT_CALCULATING = 'increment_calculating',
2121
DECREMENT_CALCULATING = 'decrement_calculating',
2222
SET_SHOULD_CREATE_ACCOUNT = 'set_should_create_account',
23+
SET_EXTENSION_DATA = 'set_extension_data',
2324
}
2425

25-
export interface ActionType {
26+
export interface ActionType extends Partial< CheckoutStateContextState > {
2627
type: ACTION;
2728
data?:
2829
| Record< string, unknown >
2930
| Record< string, never >
3031
| PaymentResultDataType;
31-
url?: string;
32-
customerId?: number;
33-
orderId?: number;
34-
shouldCreateAccount?: boolean;
35-
hasError?: boolean;
36-
orderNotes?: string;
3732
}
3833

3934
/**
@@ -52,10 +47,10 @@ export const actions = {
5247
( {
5348
type: ACTION.SET_PROCESSING,
5449
} as const ),
55-
setRedirectUrl: ( url: string ) =>
50+
setRedirectUrl: ( redirectUrl: string ) =>
5651
( {
5752
type: ACTION.SET_REDIRECT_URL,
58-
url,
53+
redirectUrl,
5954
} as const ),
6055
setProcessingResponse: ( data: PaymentResultDataType ) =>
6156
( {
@@ -107,4 +102,11 @@ export const actions = {
107102
type: ACTION.SET_ORDER_NOTES,
108103
orderNotes,
109104
} as const ),
105+
setExtensionData: (
106+
extensionData: Record< string, Record< string, unknown > >
107+
) =>
108+
( {
109+
type: ACTION.SET_EXTENSION_DATA,
110+
extensionData,
111+
} as const ),
110112
};

assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const DEFAULT_CHECKOUT_STATE_DATA: CheckoutStateContextType = {
4848
setCustomerId: ( id ) => void id,
4949
setOrderId: ( id ) => void id,
5050
setOrderNotes: ( orderNotes ) => void orderNotes,
51+
setExtensionData: ( extensionData ) => void extensionData,
5152
},
5253
onSubmit: () => void null,
5354
isComplete: false,
@@ -69,6 +70,7 @@ export const DEFAULT_CHECKOUT_STATE_DATA: CheckoutStateContextType = {
6970
isCart: false,
7071
shouldCreateAccount: false,
7172
setShouldCreateAccount: ( value ) => void value,
73+
extensionData: {},
7274
};
7375

7476
export const DEFAULT_STATE: CheckoutStateContextState = {
@@ -81,4 +83,5 @@ export const DEFAULT_STATE: CheckoutStateContextState = {
8183
customerId: checkoutData.customer_id,
8284
shouldCreateAccount: false,
8385
processingResponse: null,
86+
extensionData: {},
8487
};

0 commit comments

Comments
 (0)