Skip to content

Commit 8a57fa7

Browse files
committed
Implement Universal Links
1 parent 01ef5f8 commit 8a57fa7

File tree

3 files changed

+162
-34
lines changed

3 files changed

+162
-34
lines changed

sample/ios/ReactNative/AppDelegate.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
#import <React/RCTBundleURLProvider.h>
44

5+
// Required for deep linking
6+
#import <React/RCTLinkingManager.h>
7+
58
@implementation AppDelegate
69

710
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
@@ -14,6 +17,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
1417
return [super application:application didFinishLaunchingWithOptions:launchOptions];
1518
}
1619

20+
// Required for deep linking
21+
- (BOOL)application:(UIApplication *)application
22+
openURL:(NSURL *)url
23+
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
24+
{
25+
return [RCTLinkingManager application:application openURL:url options:options];
26+
}
27+
1728
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
1829
{
1930
return [self bundleURL];

sample/ios/ReactNative/Info.plist

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<true/>
3535
</dict>
3636
<key>NSLocationWhenInUseUsageDescription</key>
37-
<string>Your location may be required to locate pickup points near you when you request this shipping option.</string>
37+
<string>Your location is required to locate pickup points near you.</string>
3838
<key>UIAppFonts</key>
3939
<array>
4040
<string>Entypo.ttf</string>
@@ -53,5 +53,23 @@
5353
</array>
5454
<key>UIViewControllerBasedStatusBarAppearance</key>
5555
<false/>
56+
<key>CFBundleURLTypes</key>
57+
<array>
58+
<dict>
59+
<key>CFBundleURLName</key>
60+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
61+
62+
<!-- This is a custom scheme which will allow you to trigger the app from terminal -->
63+
<!-- "xcrun simctl openurl booted rn://cart" -->
64+
<key>CFBundleURLSchemes</key>
65+
<array>
66+
<string>rn</string>
67+
</array>
68+
</dict>
69+
</array>
70+
<key>LSApplicationQueriesSchemes</key>
71+
<array>
72+
<string>rn</string>
73+
</array>
5674
</dict>
5775
</plist>

sample/src/App.tsx

Lines changed: 132 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2222
*/
2323

2424
import type {PropsWithChildren, ReactNode} from 'react';
25-
import React, {useEffect} from 'react';
26-
import {Link, NavigationContainer} from '@react-navigation/native';
25+
import React, {useEffect, useState} from 'react';
26+
import {Appearance, Linking, StatusBar} from 'react-native';
27+
import {
28+
Link,
29+
NavigationContainer,
30+
useNavigation,
31+
type NavigationProp,
32+
} from '@react-navigation/native';
2733
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
2834
import {createNativeStackNavigator} from '@react-navigation/native-stack';
2935
import {ApolloClient, InMemoryCache, ApolloProvider} from '@apollo/client';
@@ -46,7 +52,6 @@ import type {
4652
} from '@shopify/checkout-sheet-kit';
4753
import {ConfigProvider} from './context/Config';
4854
import {ThemeProvider, getNavigationTheme, useTheme} from './context/Theme';
49-
import {Appearance, StatusBar} from 'react-native';
5055
import {CartProvider, useCart} from './context/Cart';
5156
import CartScreen from './screens/CartScreen';
5257
import ProductDetailsScreen from './screens/ProductDetailsScreen';
@@ -78,7 +83,7 @@ export type RootStackParamList = {
7883
Catalog: undefined;
7984
CatalogScreen: undefined;
8085
ProductDetails: {product: ShopifyProduct; variant?: ProductVariant};
81-
Cart: {userId: string};
86+
Cart: undefined;
8287
CartModal: undefined;
8388
Settings: undefined;
8489
};
@@ -115,6 +120,49 @@ const createNavigationIcon =
115120
return <Icon name={name} color={color} size={size} />;
116121
};
117122

123+
// See https://reactnative.dev/docs/linking#get-the-deep-link for more information
124+
const useInitialURL = (): {url: string | null} => {
125+
const [url, setUrl] = useState<string | null>(null);
126+
127+
useEffect(() => {
128+
const getUrlAsync = async () => {
129+
// Get the deep link used to open the app
130+
const initialUrl = await Linking.getInitialURL();
131+
132+
if (initialUrl !== url) {
133+
setUrl(initialUrl);
134+
}
135+
};
136+
137+
getUrlAsync();
138+
}, [url]);
139+
140+
return {
141+
url,
142+
};
143+
};
144+
145+
// This code is meant as example only.
146+
class StorefrontURL {
147+
readonly url: string;
148+
149+
constructor(url: string) {
150+
this.url = url;
151+
}
152+
153+
isThankYouPage(): boolean {
154+
return /thank[-_]you/i.test(this.url);
155+
}
156+
157+
isCheckout(): boolean {
158+
return this.url.includes('/checkout');
159+
}
160+
161+
isCart() {
162+
return this.url.includes('/cart');
163+
}
164+
}
165+
118166
function AppWithContext({children}: PropsWithChildren) {
119167
const shopify = useShopifyCheckoutSheet();
120168

@@ -210,48 +258,99 @@ function CartIcon() {
210258
);
211259
}
212260

213-
function AppWithNavigation() {
261+
function AppWithNavigation({children}: PropsWithChildren) {
214262
const {colorScheme, preference} = useTheme();
215-
const {totalQuantity} = useCart();
216-
217263
return (
218264
<NavigationContainer theme={getNavigationTheme(colorScheme, preference)}>
219-
<Tab.Navigator>
220-
<Tab.Screen
221-
name="Catalog"
222-
component={CatalogStack}
223-
options={{
224-
headerShown: false,
225-
tabBarIcon: createNavigationIcon('shop'),
226-
}}
227-
/>
228-
<Tab.Screen
229-
name="Cart"
230-
component={CartScreen}
231-
options={{
232-
tabBarIcon: createNavigationIcon('shopping-bag'),
233-
tabBarBadge: totalQuantity > 0 ? totalQuantity : undefined,
234-
}}
235-
/>
236-
<Tab.Screen
237-
name="Settings"
238-
component={SettingsScreen}
239-
options={{
240-
tabBarIcon: createNavigationIcon('cog'),
241-
}}
242-
/>
243-
</Tab.Navigator>
265+
{children}
244266
</NavigationContainer>
245267
);
246268
}
247269

270+
function Routes() {
271+
const {totalQuantity} = useCart();
272+
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
273+
const {url: initialUrl} = useInitialURL();
274+
const shopify = useShopifyCheckoutSheet();
275+
276+
useEffect(() => {
277+
async function handleUniversalLink(url: string) {
278+
const storefrontUrl = new StorefrontURL(url);
279+
280+
console.log('HANDLING UNIVERSAL LINK', url);
281+
282+
switch (true) {
283+
// Checkout URLs
284+
case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage():
285+
console.log('should present');
286+
shopify.present(url.replace('rn://', 'https://'));
287+
return;
288+
// Cart URLs
289+
case storefrontUrl.isCart():
290+
navigation.navigate('Cart');
291+
return;
292+
}
293+
294+
// Open everything else in a mobile browser
295+
const canOpenUrl = await Linking.canOpenURL(url);
296+
297+
if (canOpenUrl) {
298+
await Linking.openURL(url);
299+
}
300+
}
301+
302+
if (initialUrl) {
303+
handleUniversalLink(initialUrl);
304+
}
305+
306+
// Subscribe to universal links
307+
const subscription = Linking.addEventListener('url', ({url}) => {
308+
handleUniversalLink(url);
309+
});
310+
311+
return () => {
312+
subscription.remove();
313+
};
314+
}, [initialUrl, shopify, navigation]);
315+
316+
return (
317+
<Tab.Navigator>
318+
<Tab.Screen
319+
name="Catalog"
320+
component={CatalogStack}
321+
options={{
322+
headerShown: false,
323+
tabBarIcon: createNavigationIcon('shop'),
324+
}}
325+
/>
326+
<Tab.Screen
327+
name="Cart"
328+
component={CartScreen}
329+
options={{
330+
tabBarIcon: createNavigationIcon('shopping-bag'),
331+
tabBarBadge: totalQuantity > 0 ? totalQuantity : undefined,
332+
}}
333+
/>
334+
<Tab.Screen
335+
name="Settings"
336+
component={SettingsScreen}
337+
options={{
338+
tabBarIcon: createNavigationIcon('cog'),
339+
}}
340+
/>
341+
</Tab.Navigator>
342+
);
343+
}
344+
248345
function App() {
249346
return (
250347
<ErrorBoundary>
251348
<ShopifyCheckoutSheetProvider configuration={config}>
252349
<AppWithTheme>
253350
<AppWithContext>
254-
<AppWithNavigation />
351+
<AppWithNavigation>
352+
<Routes />
353+
</AppWithNavigation>
255354
</AppWithContext>
256355
</AppWithTheme>
257356
</ShopifyCheckoutSheetProvider>

0 commit comments

Comments
 (0)