@@ -22,8 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2222*/
2323
2424import React , { useCallback , useMemo , useState } from 'react' ;
25- import { Platform } from 'react-native' ;
25+ import { Platform , UIManager } from 'react-native' ;
2626import type { AcceleratedCheckoutWallet , CheckoutException } from '..' ;
27+ import { CheckoutProtocol , type ProtocolHandlers } from '../protocol' ;
2728import RCTAcceleratedCheckoutButtons from '../specs/RCTAcceleratedCheckoutButtonsNativeComponent' ;
2829
2930export enum RenderState {
@@ -111,6 +112,13 @@ interface CommonAcceleratedCheckoutButtonsProps {
111112 */
112113 onRenderStateChange ?: ( event : RenderStateChangeEvent ) => void ;
113114
115+ /**
116+ * Checkout Protocol event handlers scoped to this button instance.
117+ *
118+ * Currently supports CheckoutProtocol.start.
119+ */
120+ events ?: ProtocolHandlers ;
121+
114122 /**
115123 * Called when a link is clicked within the checkout
116124 */
@@ -162,6 +170,13 @@ export type AcceleratedCheckoutButtonsProps = (CartProps | VariantProps) &
162170 */
163171
164172const defaultStyles = { flex : 1 } ;
173+ const nativeComponentName = 'RCTAcceleratedCheckoutButtons' ;
174+ const protocolEventTypesConstant = 'checkoutProtocolEventTypes' ;
175+ const checkoutProtocolEventTypeValues = Object . values ( CheckoutProtocol ) ;
176+ const checkoutProtocolEventTypes : ReadonlySet < string > = new Set (
177+ checkoutProtocolEventTypeValues ,
178+ ) ;
179+ let verifiedProtocolEventParitySignature : string | undefined ;
165180
166181export const AcceleratedCheckoutButtons : React . FC <
167182 AcceleratedCheckoutButtonsProps
@@ -174,6 +189,7 @@ export const AcceleratedCheckoutButtons: React.FC<
174189 onCancel,
175190 onRenderStateChange,
176191 onClickLink,
192+ events,
177193 ...props
178194} ) => {
179195 const isCart = isCartProps ( props ) ;
@@ -221,6 +237,19 @@ export const AcceleratedCheckoutButtons: React.FC<
221237 [ onClickLink ] ,
222238 ) ;
223239
240+ const handleDispatch = useCallback (
241+ ( event : { nativeEvent : unknown } ) => {
242+ const nativeEvent = event . nativeEvent as { value ?: unknown } ;
243+ if ( typeof nativeEvent ?. value !== 'string' ) {
244+ logDispatchError ( 'dispatch event is missing a string `value`' ) ;
245+ return ;
246+ }
247+
248+ routeProtocolDispatchEnvelope ( nativeEvent . value , events ) ;
249+ } ,
250+ [ events ] ,
251+ ) ;
252+
224253 const handleSizeChange = useCallback (
225254 ( event : { nativeEvent : { height : number } } ) => {
226255 setDynamicHeight ( event . nativeEvent . height ) ;
@@ -268,6 +297,8 @@ export const AcceleratedCheckoutButtons: React.FC<
268297 }
269298 }
270299
300+ verifyProtocolEventParity ( ) ;
301+
271302 return (
272303 < RCTAcceleratedCheckoutButtons
273304 testID = "accelerated-checkout-buttons"
@@ -281,6 +312,7 @@ export const AcceleratedCheckoutButtons: React.FC<
281312 onCancel = { handleCancel }
282313 onRenderStateChange = { handleRenderStateChange }
283314 onClickLink = { handleClickLink }
315+ onDispatch = { handleDispatch }
284316 onSizeChange = { handleSizeChange }
285317 />
286318 ) ;
@@ -316,3 +348,145 @@ function isVariantProps(
316348) : props is VariantProps {
317349 return 'variantId' in props && 'quantity' in props && props . quantity > 0 ;
318350}
351+
352+ function verifyProtocolEventParity ( ) : void {
353+ const nativeTypes = getNativeProtocolEventTypes ( ) ;
354+ const signature = buildProtocolEventParitySignature ( nativeTypes ) ;
355+ if ( verifiedProtocolEventParitySignature === signature ) return ;
356+
357+ verifiedProtocolEventParitySignature = signature ;
358+
359+ if ( ! Array . isArray ( nativeTypes ) ) {
360+ logProtocolEventParityWarning (
361+ `native view manager did not report a \`${ protocolEventTypesConstant } \` array. ` +
362+ 'The bundled native component is likely older than this JS package.' ,
363+ ) ;
364+ return ;
365+ }
366+
367+ const jsSet = new Set < string > ( checkoutProtocolEventTypeValues ) ;
368+ const nativeSet = new Set < string > ( nativeTypes ) ;
369+
370+ const missingFromJs = [ ...nativeSet ] . filter ( t => ! jsSet . has ( t ) ) . sort ( ) ;
371+ const missingFromNative = [ ...jsSet ] . filter ( t => ! nativeSet . has ( t ) ) . sort ( ) ;
372+
373+ if ( missingFromJs . length === 0 && missingFromNative . length === 0 ) {
374+ return ;
375+ }
376+
377+ const lines = [
378+ `js = [${ [ ...jsSet ] . sort ( ) . join ( ', ' ) } ]` ,
379+ `native = [${ [ ...nativeSet ] . sort ( ) . join ( ', ' ) } ]` ,
380+ ] ;
381+ if ( missingFromJs . length > 0 ) {
382+ lines . push ( `events missing from js: ${ missingFromJs . join ( ', ' ) } ` ) ;
383+ }
384+ if ( missingFromNative . length > 0 ) {
385+ lines . push ( `events missing from native: ${ missingFromNative . join ( ', ' ) } ` ) ;
386+ }
387+
388+ logProtocolEventParityWarning ( lines . join ( '\n ' ) ) ;
389+ }
390+
391+ function buildProtocolEventParitySignature (
392+ nativeTypes : readonly string [ ] | undefined | null ,
393+ ) : string {
394+ return JSON . stringify ( {
395+ js : [ ...checkoutProtocolEventTypeValues ] . sort ( ) ,
396+ native : Array . isArray ( nativeTypes ) ? [ ...nativeTypes ] . sort ( ) : nativeTypes ,
397+ } ) ;
398+ }
399+
400+ function getNativeProtocolEventTypes ( ) : readonly string [ ] | undefined | null {
401+ const viewManagerConfig = UIManager . getViewManagerConfig ?.(
402+ nativeComponentName ,
403+ ) as
404+ | {
405+ Constants ?: Record < string , unknown > ;
406+ }
407+ | undefined ;
408+
409+ return viewManagerConfig ?. Constants ?. [ protocolEventTypesConstant ] as
410+ | readonly string [ ]
411+ | undefined
412+ | null ;
413+ }
414+
415+ function routeProtocolDispatchEnvelope (
416+ envelopeJson : string ,
417+ events : ProtocolHandlers | undefined ,
418+ ) : void {
419+ let envelope : unknown ;
420+ try {
421+ envelope = JSON . parse ( envelopeJson ) ;
422+ } catch {
423+ logDispatchError ( 'dispatch envelope is not valid JSON' , envelopeJson ) ;
424+ return ;
425+ }
426+
427+ if ( ! isPlainObject ( envelope ) || typeof envelope . type !== 'string' ) {
428+ logDispatchError (
429+ 'dispatch envelope is missing a string `type` discriminator' ,
430+ envelopeJson ,
431+ ) ;
432+ return ;
433+ }
434+
435+ if ( ! checkoutProtocolEventTypes . has ( envelope . type ) ) {
436+ logUnknownDispatchType ( envelope . type ) ;
437+ return ;
438+ }
439+
440+ const handler = ( events as Record <
441+ string ,
442+ ( ( payload : unknown ) => void ) | undefined
443+ > | undefined ) ?. [ envelope . type ] ;
444+
445+ if ( handler == null ) {
446+ return ;
447+ }
448+
449+ if ( ! isPlainObject ( envelope . payload ) ) {
450+ logDispatchError (
451+ `protocol envelope "${ envelope . type } " payload is not an object` ,
452+ envelopeJson ,
453+ ) ;
454+ return ;
455+ }
456+
457+ handler ( envelope . payload ) ;
458+ }
459+
460+ function isPlainObject ( value : unknown ) : value is Record < string , unknown > {
461+ return typeof value === 'object' && value !== null && ! Array . isArray ( value ) ;
462+ }
463+
464+ function logUnknownDispatchType ( type : string ) : void {
465+ // eslint-disable-next-line no-console
466+ console . warn (
467+ `[ShopifyAcceleratedCheckouts] Ignoring protocol dispatch envelope with unknown type "${ type } ". ` +
468+ 'Native emitted a Checkout Protocol event this JS package does not know how to handle. ' +
469+ 'Confirm native and JS package versions are compatible.' ,
470+ ) ;
471+ }
472+
473+ function logProtocolEventParityWarning ( detail : string ) : void {
474+ // eslint-disable-next-line no-console
475+ console . warn (
476+ '[ShopifyAcceleratedCheckouts] Checkout Protocol event list out of sync between JS ' +
477+ 'and native. Rebuild your host app so the bundled native component matches ' +
478+ `this version of '@shopify/checkout-kit-react-native'.\n ${ detail } ` ,
479+ ) ;
480+ }
481+
482+ function logDispatchError ( detail : string , raw ?: string ) : void {
483+ const message = `[ShopifyAcceleratedCheckouts] Failed to handle protocol dispatch: ${ detail } ` ;
484+ if ( raw == null ) {
485+ // eslint-disable-next-line no-console
486+ console . error ( message ) ;
487+ return ;
488+ }
489+
490+ // eslint-disable-next-line no-console
491+ console . error ( message , raw ) ;
492+ }
0 commit comments