11import React , { useCallback , useMemo , useState } from 'react' ;
2- import { Platform } from 'react-native' ;
2+ import { Platform , UIManager } from 'react-native' ;
33import type { AcceleratedCheckoutWallet , CheckoutException } from '..' ;
4+ import { CheckoutProtocol , type ProtocolHandlers } from '../protocol' ;
45import RCTAcceleratedCheckoutButtons from '../specs/RCTAcceleratedCheckoutButtonsNativeComponent' ;
56
67export enum RenderState {
@@ -88,6 +89,13 @@ interface CommonAcceleratedCheckoutButtonsProps {
8889 */
8990 onRenderStateChange ?: ( event : RenderStateChangeEvent ) => void ;
9091
92+ /**
93+ * Checkout Protocol event handlers scoped to this button instance.
94+ *
95+ * Currently supports CheckoutProtocol.start.
96+ */
97+ events ?: ProtocolHandlers ;
98+
9199 /**
92100 * Called when a link is clicked within the checkout
93101 */
@@ -139,6 +147,13 @@ export type AcceleratedCheckoutButtonsProps = (CartProps | VariantProps) &
139147 */
140148
141149const defaultStyles = { flex : 1 } ;
150+ const nativeComponentName = 'RCTAcceleratedCheckoutButtons' ;
151+ const protocolEventTypesConstant = 'checkoutProtocolEventTypes' ;
152+ const checkoutProtocolEventTypeValues = Object . values ( CheckoutProtocol ) ;
153+ const checkoutProtocolEventTypes : ReadonlySet < string > = new Set (
154+ checkoutProtocolEventTypeValues ,
155+ ) ;
156+ let verifiedProtocolEventParitySignature : string | undefined ;
142157
143158export const AcceleratedCheckoutButtons : React . FC <
144159 AcceleratedCheckoutButtonsProps
@@ -151,6 +166,7 @@ export const AcceleratedCheckoutButtons: React.FC<
151166 onCancel,
152167 onRenderStateChange,
153168 onClickLink,
169+ events,
154170 ...props
155171} ) => {
156172 const isCart = isCartProps ( props ) ;
@@ -198,6 +214,19 @@ export const AcceleratedCheckoutButtons: React.FC<
198214 [ onClickLink ] ,
199215 ) ;
200216
217+ const handleDispatch = useCallback (
218+ ( event : { nativeEvent : unknown } ) => {
219+ const nativeEvent = event . nativeEvent as { value ?: unknown } ;
220+ if ( typeof nativeEvent ?. value !== 'string' ) {
221+ logDispatchError ( 'dispatch event is missing a string `value`' ) ;
222+ return ;
223+ }
224+
225+ routeProtocolDispatchEnvelope ( nativeEvent . value , events ) ;
226+ } ,
227+ [ events ] ,
228+ ) ;
229+
201230 const handleSizeChange = useCallback (
202231 ( event : { nativeEvent : { height : number } } ) => {
203232 setDynamicHeight ( event . nativeEvent . height ) ;
@@ -245,6 +274,8 @@ export const AcceleratedCheckoutButtons: React.FC<
245274 }
246275 }
247276
277+ verifyProtocolEventParity ( ) ;
278+
248279 return (
249280 < RCTAcceleratedCheckoutButtons
250281 testID = "accelerated-checkout-buttons"
@@ -258,6 +289,7 @@ export const AcceleratedCheckoutButtons: React.FC<
258289 onCancel = { handleCancel }
259290 onRenderStateChange = { handleRenderStateChange }
260291 onClickLink = { handleClickLink }
292+ onDispatch = { handleDispatch }
261293 onSizeChange = { handleSizeChange }
262294 />
263295 ) ;
@@ -293,3 +325,145 @@ function isVariantProps(
293325) : props is VariantProps {
294326 return 'variantId' in props && 'quantity' in props && props . quantity > 0 ;
295327}
328+
329+ function verifyProtocolEventParity ( ) : void {
330+ const nativeTypes = getNativeProtocolEventTypes ( ) ;
331+ const signature = buildProtocolEventParitySignature ( nativeTypes ) ;
332+ if ( verifiedProtocolEventParitySignature === signature ) return ;
333+
334+ verifiedProtocolEventParitySignature = signature ;
335+
336+ if ( ! Array . isArray ( nativeTypes ) ) {
337+ logProtocolEventParityWarning (
338+ `native view manager did not report a \`${ protocolEventTypesConstant } \` array. ` +
339+ 'The bundled native component is likely older than this JS package.' ,
340+ ) ;
341+ return ;
342+ }
343+
344+ const jsSet = new Set < string > ( checkoutProtocolEventTypeValues ) ;
345+ const nativeSet = new Set < string > ( nativeTypes ) ;
346+
347+ const missingFromJs = [ ...nativeSet ] . filter ( t => ! jsSet . has ( t ) ) . sort ( ) ;
348+ const missingFromNative = [ ...jsSet ] . filter ( t => ! nativeSet . has ( t ) ) . sort ( ) ;
349+
350+ if ( missingFromJs . length === 0 && missingFromNative . length === 0 ) {
351+ return ;
352+ }
353+
354+ const lines = [
355+ `js = [${ [ ...jsSet ] . sort ( ) . join ( ', ' ) } ]` ,
356+ `native = [${ [ ...nativeSet ] . sort ( ) . join ( ', ' ) } ]` ,
357+ ] ;
358+ if ( missingFromJs . length > 0 ) {
359+ lines . push ( `events missing from js: ${ missingFromJs . join ( ', ' ) } ` ) ;
360+ }
361+ if ( missingFromNative . length > 0 ) {
362+ lines . push ( `events missing from native: ${ missingFromNative . join ( ', ' ) } ` ) ;
363+ }
364+
365+ logProtocolEventParityWarning ( lines . join ( '\n ' ) ) ;
366+ }
367+
368+ function buildProtocolEventParitySignature (
369+ nativeTypes : readonly string [ ] | undefined | null ,
370+ ) : string {
371+ return JSON . stringify ( {
372+ js : [ ...checkoutProtocolEventTypeValues ] . sort ( ) ,
373+ native : Array . isArray ( nativeTypes ) ? [ ...nativeTypes ] . sort ( ) : nativeTypes ,
374+ } ) ;
375+ }
376+
377+ function getNativeProtocolEventTypes ( ) : readonly string [ ] | undefined | null {
378+ const viewManagerConfig = UIManager . getViewManagerConfig ?.(
379+ nativeComponentName ,
380+ ) as
381+ | {
382+ Constants ?: Record < string , unknown > ;
383+ }
384+ | undefined ;
385+
386+ return viewManagerConfig ?. Constants ?. [ protocolEventTypesConstant ] as
387+ | readonly string [ ]
388+ | undefined
389+ | null ;
390+ }
391+
392+ function routeProtocolDispatchEnvelope (
393+ envelopeJson : string ,
394+ events : ProtocolHandlers | undefined ,
395+ ) : void {
396+ let envelope : unknown ;
397+ try {
398+ envelope = JSON . parse ( envelopeJson ) ;
399+ } catch {
400+ logDispatchError ( 'dispatch envelope is not valid JSON' , envelopeJson ) ;
401+ return ;
402+ }
403+
404+ if ( ! isPlainObject ( envelope ) || typeof envelope . type !== 'string' ) {
405+ logDispatchError (
406+ 'dispatch envelope is missing a string `type` discriminator' ,
407+ envelopeJson ,
408+ ) ;
409+ return ;
410+ }
411+
412+ if ( ! checkoutProtocolEventTypes . has ( envelope . type ) ) {
413+ logUnknownDispatchType ( envelope . type ) ;
414+ return ;
415+ }
416+
417+ const handler = ( events as Record <
418+ string ,
419+ ( ( payload : unknown ) => void ) | undefined
420+ > | undefined ) ?. [ envelope . type ] ;
421+
422+ if ( handler == null ) {
423+ return ;
424+ }
425+
426+ if ( ! isPlainObject ( envelope . payload ) ) {
427+ logDispatchError (
428+ `protocol envelope "${ envelope . type } " payload is not an object` ,
429+ envelopeJson ,
430+ ) ;
431+ return ;
432+ }
433+
434+ handler ( envelope . payload ) ;
435+ }
436+
437+ function isPlainObject ( value : unknown ) : value is Record < string , unknown > {
438+ return typeof value === 'object' && value !== null && ! Array . isArray ( value ) ;
439+ }
440+
441+ function logUnknownDispatchType ( type : string ) : void {
442+ // eslint-disable-next-line no-console
443+ console . warn (
444+ `[ShopifyAcceleratedCheckouts] Ignoring protocol dispatch envelope with unknown type "${ type } ". ` +
445+ 'Native emitted a Checkout Protocol event this JS package does not know how to handle. ' +
446+ 'Confirm native and JS package versions are compatible.' ,
447+ ) ;
448+ }
449+
450+ function logProtocolEventParityWarning ( detail : string ) : void {
451+ // eslint-disable-next-line no-console
452+ console . warn (
453+ '[ShopifyAcceleratedCheckouts] Checkout Protocol event list out of sync between JS ' +
454+ 'and native. Rebuild your host app so the bundled native component matches ' +
455+ `this version of '@shopify/checkout-kit-react-native'.\n ${ detail } ` ,
456+ ) ;
457+ }
458+
459+ function logDispatchError ( detail : string , raw ?: string ) : void {
460+ const message = `[ShopifyAcceleratedCheckouts] Failed to handle protocol dispatch: ${ detail } ` ;
461+ if ( raw == null ) {
462+ // eslint-disable-next-line no-console
463+ console . error ( message ) ;
464+ return ;
465+ }
466+
467+ // eslint-disable-next-line no-console
468+ console . error ( message , raw ) ;
469+ }
0 commit comments