11import type { PropsWithChildren , ReactNode } from 'react' ;
2- import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
3- import { Appearance , Linking , Pressable , StatusBar } from 'react-native' ;
2+ import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
3+ import {
4+ Appearance ,
5+ Linking ,
6+ Pressable ,
7+ StatusBar ,
8+ StyleSheet ,
9+ View ,
10+ } from 'react-native' ;
411import {
512 NavigationContainer ,
613 useNavigation ,
@@ -43,6 +50,7 @@ import ErrorBoundary from './ErrorBoundary';
4350import env from 'react-native-config' ;
4451import { createDebugLogger } from './utils' ;
4552import { useShopifyEventHandlers } from './hooks/useCheckoutEventHandlers' ;
53+ import { useE2ECartBootstrap } from './hooks/useE2ECartBootstrap' ;
4654
4755const log = createDebugLogger ( 'ENV' ) ;
4856
@@ -83,6 +91,12 @@ const Tab = createBottomTabNavigator<RootStackParamList>();
8391const Stack = createNativeStackNavigator < RootStackParamList > ( ) ;
8492const AccountStack = createNativeStackNavigator < AccountStackParamList > ( ) ;
8593
94+ const styles = StyleSheet . create ( {
95+ routes : {
96+ flex : 1 ,
97+ } ,
98+ } ) ;
99+
86100export const cache = new InMemoryCache ( ) ;
87101
88102const client = new ApolloClient ( {
@@ -118,26 +132,47 @@ const createNavigationIcon =
118132 return < Icon name = { name } color = { color } size = { size } /> ;
119133 } ;
120134
135+ type InitialURLState = {
136+ url : string | null ;
137+ resolved : boolean ;
138+ } ;
139+
121140// See https://reactnative.dev/docs/linking#get-the-deep-link for more information
122- const useInitialURL = ( ) : { url : string | null } => {
123- const [ url , setUrl ] = useState < string | null > ( null ) ;
141+ const useInitialURL = ( ) : InitialURLState => {
142+ const [ state , setState ] = useState < InitialURLState > ( {
143+ url : null ,
144+ resolved : false ,
145+ } ) ;
124146
125147 useEffect ( ( ) => {
126- const getUrlAsync = async ( ) => {
127- // Get the deep link used to open the app
128- const initialUrl = await Linking . getInitialURL ( ) ;
148+ let isMounted = true ;
129149
130- if ( initialUrl !== url ) {
131- setUrl ( initialUrl ) ;
150+ const getUrlAsync = async ( ) => {
151+ let initialUrl : string | null = null ;
152+
153+ try {
154+ // Get the deep link used to open the app
155+ initialUrl = await Linking . getInitialURL ( ) ;
156+ } catch ( error ) {
157+ console . warn ( 'Failed to get initial URL' , error ) ;
158+ } finally {
159+ if ( isMounted ) {
160+ setState ( {
161+ url : initialUrl ,
162+ resolved : true ,
163+ } ) ;
164+ }
132165 }
133166 } ;
134167
135168 getUrlAsync ( ) ;
136- } , [ url ] ) ;
137169
138- return {
139- url,
140- } ;
170+ return ( ) => {
171+ isMounted = false ;
172+ } ;
173+ } , [ ] ) ;
174+
175+ return state ;
141176} ;
142177
143178// This code is meant as example only.
@@ -415,12 +450,24 @@ function AppWithNavigation(props: {children: React.ReactNode}) {
415450function Routes ( ) {
416451 const { totalQuantity} = useCart ( ) ;
417452 const navigation = useNavigation < NavigationProp < RootStackParamList > > ( ) ;
418- const { url : initialUrl } = useInitialURL ( ) ;
453+ const { url : initialUrl , resolved : initialUrlResolved } = useInitialURL ( ) ;
454+ const handledInitialUrlRef = useRef < string | null > ( null ) ;
455+ const [ linkingReady , setLinkingReady ] = useState ( false ) ;
419456 const shopify = useShopifyCheckout ( ) ;
420457 const eventHandlers = useShopifyEventHandlers ( 'UniversalLink' ) ;
458+ const navigateToCart = useCallback ( ( ) => {
459+ navigation . navigate ( 'Cart' ) ;
460+ } , [ navigation ] ) ;
461+ const handleE2ECartBootstrap = useE2ECartBootstrap ( {
462+ onCartReady : navigateToCart ,
463+ } ) ;
421464
422465 useEffect ( ( ) => {
423466 async function handleUniversalLink ( url : string ) {
467+ if ( await handleE2ECartBootstrap ( url ) ) {
468+ return ;
469+ }
470+
424471 const storefrontUrl = new StorefrontURL ( url ) ;
425472
426473 switch ( true ) {
@@ -433,7 +480,7 @@ function Routes() {
433480 return ;
434481 // Cart URLs
435482 case storefrontUrl . isCart ( ) :
436- navigation . navigate ( 'Cart' ) ;
483+ navigateToCart ( ) ;
437484 return ;
438485 }
439486
@@ -445,58 +492,86 @@ function Routes() {
445492 }
446493 }
447494
448- if ( initialUrl ) {
449- handleUniversalLink ( initialUrl ) ;
450- }
495+ let isMounted = true ;
451496
452497 // Subscribe to universal links
453498 const subscription = Linking . addEventListener ( 'url' , ( { url} ) => {
454499 handleUniversalLink ( url ) ;
455500 } ) ;
456501
502+ const prepareInitialLinking = async ( ) => {
503+ if ( ! initialUrlResolved ) {
504+ return ;
505+ }
506+
507+ if ( initialUrl && handledInitialUrlRef . current !== initialUrl ) {
508+ handledInitialUrlRef . current = initialUrl ;
509+ await handleUniversalLink ( initialUrl ) ;
510+ }
511+
512+ if ( isMounted ) {
513+ setLinkingReady ( true ) ;
514+ }
515+ } ;
516+
517+ prepareInitialLinking ( ) ;
518+
457519 return ( ) => {
520+ isMounted = false ;
458521 subscription . remove ( ) ;
459522 } ;
460- } , [ initialUrl , shopify , navigation , eventHandlers ] ) ;
523+ } , [
524+ initialUrl ,
525+ initialUrlResolved ,
526+ shopify ,
527+ navigation ,
528+ eventHandlers ,
529+ handleE2ECartBootstrap ,
530+ navigateToCart ,
531+ ] ) ;
461532
462533 return (
463- < Tab . Navigator >
464- < Tab . Screen
465- name = "Catalog"
466- component = { CatalogStack }
467- options = { {
468- headerShown : false ,
469- tabBarButtonTestID : 'catalog-tab' ,
470- tabBarIcon : createNavigationIcon ( 'shop' ) ,
471- } }
472- />
473- < Tab . Screen
474- name = "Cart"
475- component = { CartScreen }
476- options = { {
477- tabBarButtonTestID : 'cart-tab' ,
478- tabBarIcon : createNavigationIcon ( 'shopping-bag' ) ,
479- tabBarBadge : totalQuantity > 0 ? totalQuantity : undefined ,
480- } }
481- />
482- < Tab . Screen
483- name = "Account"
484- component = { AccountStackScreen }
485- options = { {
486- headerShown : false ,
487- tabBarButtonTestID : 'account-tab' ,
488- tabBarIcon : createNavigationIcon ( 'user' ) ,
489- } }
490- />
491- < Tab . Screen
492- name = "Settings"
493- component = { SettingsScreen }
494- options = { {
495- tabBarButtonTestID : 'settings-tab' ,
496- tabBarIcon : createNavigationIcon ( 'cog' ) ,
497- } }
498- />
499- </ Tab . Navigator >
534+ < View
535+ style = { styles . routes }
536+ testID = { linkingReady ? 'checkout-kit-sample-ready' : undefined } >
537+ < Tab . Navigator >
538+ < Tab . Screen
539+ name = "Catalog"
540+ component = { CatalogStack }
541+ options = { {
542+ headerShown : false ,
543+ tabBarButtonTestID : 'catalog-tab' ,
544+ tabBarIcon : createNavigationIcon ( 'shop' ) ,
545+ } }
546+ />
547+ < Tab . Screen
548+ name = "Cart"
549+ component = { CartScreen }
550+ options = { {
551+ tabBarButtonTestID : 'cart-tab' ,
552+ tabBarIcon : createNavigationIcon ( 'shopping-bag' ) ,
553+ tabBarBadge : totalQuantity > 0 ? totalQuantity : undefined ,
554+ } }
555+ />
556+ < Tab . Screen
557+ name = "Account"
558+ component = { AccountStackScreen }
559+ options = { {
560+ headerShown : false ,
561+ tabBarButtonTestID : 'account-tab' ,
562+ tabBarIcon : createNavigationIcon ( 'user' ) ,
563+ } }
564+ />
565+ < Tab . Screen
566+ name = "Settings"
567+ component = { SettingsScreen }
568+ options = { {
569+ tabBarButtonTestID : 'settings-tab' ,
570+ tabBarIcon : createNavigationIcon ( 'cog' ) ,
571+ } }
572+ />
573+ </ Tab . Navigator >
574+ </ View >
500575 ) ;
501576}
502577
0 commit comments