Skip to content

Commit 292ec83

Browse files
authored
Implement Universal Links for iOS (#144)
Implement Universal Links
1 parent 126ab4b commit 292ec83

File tree

3 files changed

+159
-34
lines changed

3 files changed

+159
-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: 129 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,96 @@ 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+
switch (true) {
281+
// Checkout URLs
282+
case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage():
283+
shopify.present(url);
284+
return;
285+
// Cart URLs
286+
case storefrontUrl.isCart():
287+
navigation.navigate('Cart');
288+
return;
289+
}
290+
291+
// Open everything else in a mobile browser
292+
const canOpenUrl = await Linking.canOpenURL(url);
293+
294+
if (canOpenUrl) {
295+
await Linking.openURL(url);
296+
}
297+
}
298+
299+
if (initialUrl) {
300+
handleUniversalLink(initialUrl);
301+
}
302+
303+
// Subscribe to universal links
304+
const subscription = Linking.addEventListener('url', ({url}) => {
305+
handleUniversalLink(url);
306+
});
307+
308+
return () => {
309+
subscription.remove();
310+
};
311+
}, [initialUrl, shopify, navigation]);
312+
313+
return (
314+
<Tab.Navigator>
315+
<Tab.Screen
316+
name="Catalog"
317+
component={CatalogStack}
318+
options={{
319+
headerShown: false,
320+
tabBarIcon: createNavigationIcon('shop'),
321+
}}
322+
/>
323+
<Tab.Screen
324+
name="Cart"
325+
component={CartScreen}
326+
options={{
327+
tabBarIcon: createNavigationIcon('shopping-bag'),
328+
tabBarBadge: totalQuantity > 0 ? totalQuantity : undefined,
329+
}}
330+
/>
331+
<Tab.Screen
332+
name="Settings"
333+
component={SettingsScreen}
334+
options={{
335+
tabBarIcon: createNavigationIcon('cog'),
336+
}}
337+
/>
338+
</Tab.Navigator>
339+
);
340+
}
341+
248342
function App() {
249343
return (
250344
<ErrorBoundary>
251345
<ShopifyCheckoutSheetProvider configuration={config}>
252346
<AppWithTheme>
253347
<AppWithContext>
254-
<AppWithNavigation />
348+
<AppWithNavigation>
349+
<Routes />
350+
</AppWithNavigation>
255351
</AppWithContext>
256352
</AppWithTheme>
257353
</ShopifyCheckoutSheetProvider>

0 commit comments

Comments
 (0)