diff --git a/sample/ios/ReactNative/AppDelegate.mm b/sample/ios/ReactNative/AppDelegate.mm index 51f09dfd..12c8852a 100644 --- a/sample/ios/ReactNative/AppDelegate.mm +++ b/sample/ios/ReactNative/AppDelegate.mm @@ -2,6 +2,9 @@ #import +// Required for deep linking +#import + @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions @@ -14,6 +17,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +// Required for deep linking +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:application openURL:url options:options]; +} + - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { return [self bundleURL]; diff --git a/sample/ios/ReactNative/Info.plist b/sample/ios/ReactNative/Info.plist index 6ed5b71d..49515ba6 100644 --- a/sample/ios/ReactNative/Info.plist +++ b/sample/ios/ReactNative/Info.plist @@ -34,7 +34,7 @@ NSLocationWhenInUseUsageDescription - Your location may be required to locate pickup points near you when you request this shipping option. + Your location is required to locate pickup points near you. UIAppFonts Entypo.ttf @@ -53,5 +53,23 @@ UIViewControllerBasedStatusBarAppearance + CFBundleURLTypes + + + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + + + + CFBundleURLSchemes + + rn + + + + LSApplicationQueriesSchemes + + rn + diff --git a/sample/src/App.tsx b/sample/src/App.tsx index a60b5f69..5f4044cd 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -22,8 +22,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO */ import type {PropsWithChildren, ReactNode} from 'react'; -import React, {useEffect} from 'react'; -import {Link, NavigationContainer} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; +import {Appearance, Linking, StatusBar} from 'react-native'; +import { + Link, + NavigationContainer, + useNavigation, + type NavigationProp, +} from '@react-navigation/native'; import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import {createNativeStackNavigator} from '@react-navigation/native-stack'; import {ApolloClient, InMemoryCache, ApolloProvider} from '@apollo/client'; @@ -46,7 +52,6 @@ import type { } from '@shopify/checkout-sheet-kit'; import {ConfigProvider} from './context/Config'; import {ThemeProvider, getNavigationTheme, useTheme} from './context/Theme'; -import {Appearance, StatusBar} from 'react-native'; import {CartProvider, useCart} from './context/Cart'; import CartScreen from './screens/CartScreen'; import ProductDetailsScreen from './screens/ProductDetailsScreen'; @@ -78,7 +83,7 @@ export type RootStackParamList = { Catalog: undefined; CatalogScreen: undefined; ProductDetails: {product: ShopifyProduct; variant?: ProductVariant}; - Cart: {userId: string}; + Cart: undefined; CartModal: undefined; Settings: undefined; }; @@ -115,6 +120,49 @@ const createNavigationIcon = return ; }; +// See https://reactnative.dev/docs/linking#get-the-deep-link for more information +const useInitialURL = (): {url: string | null} => { + const [url, setUrl] = useState(null); + + useEffect(() => { + const getUrlAsync = async () => { + // Get the deep link used to open the app + const initialUrl = await Linking.getInitialURL(); + + if (initialUrl !== url) { + setUrl(initialUrl); + } + }; + + getUrlAsync(); + }, [url]); + + return { + url, + }; +}; + +// This code is meant as example only. +class StorefrontURL { + readonly url: string; + + constructor(url: string) { + this.url = url; + } + + isThankYouPage(): boolean { + return /thank[-_]you/i.test(this.url); + } + + isCheckout(): boolean { + return this.url.includes('/checkout'); + } + + isCart() { + return this.url.includes('/cart'); + } +} + function AppWithContext({children}: PropsWithChildren) { const shopify = useShopifyCheckoutSheet(); @@ -210,48 +258,96 @@ function CartIcon() { ); } -function AppWithNavigation() { +function AppWithNavigation({children}: PropsWithChildren) { const {colorScheme, preference} = useTheme(); - const {totalQuantity} = useCart(); - return ( - - - 0 ? totalQuantity : undefined, - }} - /> - - + {children} ); } +function Routes() { + const {totalQuantity} = useCart(); + const navigation = useNavigation>(); + const {url: initialUrl} = useInitialURL(); + const shopify = useShopifyCheckoutSheet(); + + useEffect(() => { + async function handleUniversalLink(url: string) { + const storefrontUrl = new StorefrontURL(url); + + switch (true) { + // Checkout URLs + case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage(): + shopify.present(url); + return; + // Cart URLs + case storefrontUrl.isCart(): + navigation.navigate('Cart'); + return; + } + + // Open everything else in a mobile browser + const canOpenUrl = await Linking.canOpenURL(url); + + if (canOpenUrl) { + await Linking.openURL(url); + } + } + + if (initialUrl) { + handleUniversalLink(initialUrl); + } + + // Subscribe to universal links + const subscription = Linking.addEventListener('url', ({url}) => { + handleUniversalLink(url); + }); + + return () => { + subscription.remove(); + }; + }, [initialUrl, shopify, navigation]); + + return ( + + + 0 ? totalQuantity : undefined, + }} + /> + + + ); +} + function App() { return ( - + + +