Skip to content

Commit c77059a

Browse files
committed
Add RN sample E2E bootstrap support
1 parent c6799a7 commit c77059a

11 files changed

Lines changed: 500 additions & 109 deletions

File tree

platforms/react-native/sample/android/app/src/main/AndroidManifest.template.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
<!-- Note: the host here will be replaced on application start -->
3636
<data android:scheme="https" android:host="{{STOREFRONT_DOMAIN}}" />
3737
</intent-filter>
38+
39+
<intent-filter>
40+
<action android:name="android.intent.action.VIEW" />
41+
<category android:name="android.intent.category.DEFAULT" />
42+
<category android:name="android.intent.category.BROWSABLE" />
43+
44+
<data android:scheme="com.shopify.checkoutkit.reactnativedemo" android:host="cart" />
45+
</intent-filter>
3846
</activity>
3947
</application>
4048
</manifest>

platforms/react-native/sample/ios/AppDelegate.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
3131

3232
return true
3333
}
34+
35+
func application(
36+
_ app: UIApplication,
37+
open url: URL,
38+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
39+
) -> Bool {
40+
return RCTLinkingManager.application(app, open: url, options: options)
41+
}
42+
43+
func application(
44+
_ application: UIApplication,
45+
continue userActivity: NSUserActivity,
46+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
47+
) -> Bool {
48+
return RCTLinkingManager.application(
49+
application,
50+
continue: userActivity,
51+
restorationHandler: restorationHandler
52+
)
53+
}
3454
}
3555

3656
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {

platforms/react-native/sample/ios/CheckoutKitReactNativeDemo/Info.plist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<key>CFBundleURLSchemes</key>
2929
<array>
3030
<string>rn</string>
31+
<string>com.shopify.checkoutkit.reactnativedemo</string>
3132
</array>
3233
</dict>
3334
</array>

platforms/react-native/sample/src/App.tsx

Lines changed: 131 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import 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';
411
import {
512
NavigationContainer,
613
useNavigation,
@@ -43,6 +50,7 @@ import ErrorBoundary from './ErrorBoundary';
4350
import env from 'react-native-config';
4451
import {createDebugLogger} from './utils';
4552
import {useShopifyEventHandlers} from './hooks/useCheckoutEventHandlers';
53+
import {useE2ECartBootstrap} from './hooks/useE2ECartBootstrap';
4654

4755
const log = createDebugLogger('ENV');
4856

@@ -83,6 +91,12 @@ const Tab = createBottomTabNavigator<RootStackParamList>();
8391
const Stack = createNativeStackNavigator<RootStackParamList>();
8492
const AccountStack = createNativeStackNavigator<AccountStackParamList>();
8593

94+
const styles = StyleSheet.create({
95+
routes: {
96+
flex: 1,
97+
},
98+
});
99+
86100
export const cache = new InMemoryCache();
87101

88102
const 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}) {
415450
function 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

Comments
 (0)