This repository has been archived by the owner on Feb 23, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 219
Add CartEventsContext
to dispatch events in the Cart block
#7694
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
46493cc
Explicitly set type of CartLineItemRow to ForwardRefExoticComponent
opr 6a24480
Change CartLineItemRow to extend React.RefAttributes
opr 1efe8eb
Add CartEventsContext with onProceedToCheckout event
opr aa3704a
Wrap Cart in CartEventsProvider
opr 4377bb5
Dispatch onProceedToCheckout event when button is pressed
opr 7d7cf56
Block user from proceeding if the input is invalid
opr 14d41f0
Update type of children on CartEventsProvider
opr 80af322
Add test for ProceedToCheckout block
opr 84809ce
Add tests for CartEventProvider
opr bddf8f6
Re-add dependencies after merge conflict
opr 56a186a
bot: update checkstyle.xml
github-actions[bot] 1373f82
Rename hasValidationErrors to hasValidationError & get error from store
opr e7f7256
Add strictLimits to dependency array
opr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
assets/js/base/context/providers/cart-checkout/cart-events/event-emit.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useMemo } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
emitterCallback, | ||
reducer, | ||
emitEvent, | ||
emitEventWithAbort, | ||
ActionType, | ||
} from '../../../event-emit'; | ||
|
||
// These events are emitted when the Cart status is BEFORE_PROCESSING and AFTER_PROCESSING | ||
// to enable third parties to hook into the cart process | ||
const EVENTS = { | ||
PROCEED_TO_CHECKOUT: 'cart_proceed_to_checkout', | ||
}; | ||
|
||
type EventEmittersType = Record< string, ReturnType< typeof emitterCallback > >; | ||
|
||
/** | ||
* Receives a reducer dispatcher and returns an object with the | ||
* various event emitters for the payment processing events. | ||
* | ||
* Calling the event registration function with the callback will register it | ||
* for the event emitter and will return a dispatcher for removing the | ||
* registered callback (useful for implementation in `useEffect`). | ||
* | ||
* @param {Function} observerDispatch The emitter reducer dispatcher. | ||
* @return {Object} An object with the various payment event emitter registration functions | ||
*/ | ||
const useEventEmitters = ( | ||
observerDispatch: React.Dispatch< ActionType > | ||
): EventEmittersType => { | ||
const eventEmitters = useMemo( | ||
() => ( { | ||
onProceedToCheckout: emitterCallback( | ||
EVENTS.PROCEED_TO_CHECKOUT, | ||
observerDispatch | ||
), | ||
} ), | ||
[ observerDispatch ] | ||
); | ||
return eventEmitters; | ||
}; | ||
|
||
export { EVENTS, useEventEmitters, reducer, emitEvent, emitEventWithAbort }; |
78 changes: 78 additions & 0 deletions
78
assets/js/base/context/providers/cart-checkout/cart-events/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
|
||
import { | ||
createContext, | ||
useContext, | ||
useReducer, | ||
useRef, | ||
useEffect, | ||
} from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
useEventEmitters, | ||
reducer as emitReducer, | ||
emitEventWithAbort, | ||
EVENTS, | ||
} from './event-emit'; | ||
import type { emitterCallback } from '../../../event-emit'; | ||
|
||
type CartEventsContextType = { | ||
// Used to register a callback that will fire when the cart has been processed and has an error. | ||
onProceedToCheckout: ReturnType< typeof emitterCallback >; | ||
// Used to register a callback that will fire when the cart has been processed and has an error. | ||
dispatchOnProceedToCheckout: () => Promise< unknown[] >; | ||
}; | ||
|
||
const CartEventsContext = createContext< CartEventsContextType >( { | ||
onProceedToCheckout: () => () => void null, | ||
dispatchOnProceedToCheckout: () => new Promise( () => void null ), | ||
} ); | ||
|
||
export const useCartEventsContext = () => { | ||
return useContext( CartEventsContext ); | ||
}; | ||
|
||
/** | ||
* Checkout Events provider | ||
* Emit Checkout events and provide access to Checkout event handlers | ||
* | ||
* @param {Object} props Incoming props for the provider. | ||
* @param {Object} props.children The children being wrapped. | ||
*/ | ||
export const CartEventsProvider = ( { | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
} ): JSX.Element => { | ||
const [ observers, observerDispatch ] = useReducer( emitReducer, {} ); | ||
const currentObservers = useRef( observers ); | ||
const { onProceedToCheckout } = useEventEmitters( observerDispatch ); | ||
|
||
// set observers on ref so it's always current. | ||
useEffect( () => { | ||
currentObservers.current = observers; | ||
}, [ observers ] ); | ||
|
||
const dispatchOnProceedToCheckout = async () => { | ||
return await emitEventWithAbort( | ||
currentObservers.current, | ||
EVENTS.PROCEED_TO_CHECKOUT, | ||
null | ||
); | ||
}; | ||
|
||
const cartEvents = { | ||
onProceedToCheckout, | ||
dispatchOnProceedToCheckout, | ||
}; | ||
return ( | ||
<CartEventsContext.Provider value={ cartEvents }> | ||
{ children } | ||
</CartEventsContext.Provider> | ||
); | ||
}; |
45 changes: 45 additions & 0 deletions
45
assets/js/base/context/providers/cart-checkout/cart-events/test/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useCartEventsContext } from '@woocommerce/base-context'; | ||
import { useEffect } from '@wordpress/element'; | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { CartEventsProvider } from '../index'; | ||
import Block from '../../../../../../blocks/cart/inner-blocks/proceed-to-checkout-block/block'; | ||
|
||
describe( 'CartEventsProvider', () => { | ||
it( 'allows observers to unsubscribe', async () => { | ||
const mockObserver = jest.fn(); | ||
const MockObserverComponent = () => { | ||
const { onProceedToCheckout } = useCartEventsContext(); | ||
useEffect( () => { | ||
const unsubscribe = onProceedToCheckout( () => { | ||
mockObserver(); | ||
unsubscribe(); | ||
} ); | ||
}, [ onProceedToCheckout ] ); | ||
return <div>Mock observer</div>; | ||
}; | ||
|
||
render( | ||
<CartEventsProvider> | ||
<div> | ||
<MockObserverComponent /> | ||
<Block checkoutPageId={ 0 } className="test-block" /> | ||
</div> | ||
</CartEventsProvider> | ||
); | ||
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument(); | ||
const button = screen.getByText( 'Proceed to Checkout' ); | ||
// Click twice. The observer should unsubscribe after the first click. | ||
button.click(); | ||
button.click(); | ||
await waitFor( () => { | ||
expect( mockObserver ).toHaveBeenCalledTimes( 1 ); | ||
} ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
assets/js/blocks/cart/inner-blocks/proceed-to-checkout-block/test/block.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import { useCartEventsContext } from '@woocommerce/base-context'; | ||
import { useEffect } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Block from '../block'; | ||
import { CartEventsProvider } from '../../../../../base/context/providers'; | ||
|
||
describe( 'ProceedToCheckoutBlock', () => { | ||
it( 'dispatches the onProceedToCheckout event when the button is clicked', async () => { | ||
const mockObserver = jest.fn(); | ||
const MockObserverComponent = () => { | ||
const { onProceedToCheckout } = useCartEventsContext(); | ||
useEffect( () => { | ||
return onProceedToCheckout( mockObserver ); | ||
}, [ onProceedToCheckout ] ); | ||
return <div>Mock observer</div>; | ||
}; | ||
|
||
render( | ||
<CartEventsProvider> | ||
<div> | ||
<MockObserverComponent /> | ||
<Block checkoutPageId={ 0 } className="test-block" /> | ||
</div> | ||
</CartEventsProvider> | ||
); | ||
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument(); | ||
const button = screen.getByText( 'Proceed to Checkout' ); | ||
button.click(); | ||
await waitFor( () => { | ||
expect( mockObserver ).toHaveBeenCalled(); | ||
} ); | ||
} ); | ||
} ); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should combine Cart and CartEvents providers? At the moment, the Cart provider just passed the children down and has a redirectUrl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see Mike's comment below