Skip to main content

Handle offsite payments

Some payment providers redirect buyers outside your app to complete a transaction, either to a web page or an external banking app. To bring buyers back to your app, configure Universal Links (iOS) and App Links (Android) on your storefront domain.



Anchor to Step 1: Configure your storefrontStep 1: Configure your storefront

Set up your storefront to generate the .well-known files that iOS and Android use to verify your app owns the domain.

  1. Go to your app's configuration in the Shopify admin.
  2. Click the name of your app.
  3. Click API Integrations.
  4. In the Storefront API integration section, click Configure.
  5. Configure link handling for your platforms:
    • iOS: Expand the iOS Buy SDK configuration section, enter your Apple App ID, and select Use iOS universal links.
    • Android: Expand the Android Buy SDK configuration section and provide your Android application ID and SHA256 fingerprint certificate values from the Play Store.

This generates two .well-known JSON files on your custom domain:

  • apple-app-site-association for iOS
  • assetlinks.json for Android

.well-known/apple-app-site-association (iOS)

{
"applinks": {
"apps": [],
"details": [
{
"appID": "{APPLE_TEAM_ID}.{BUNDLE_IDENTIFIER}",
"paths": [
"NOT /admin/*",
"NOT /*/amazon_payments/callback",
"NOT /*/checkouts/*/express/callback",
"/*"
]
}
]
}
}

.well-known/assetlinks.json (Android)

[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "{PACKAGE_NAME}",
"sha256_cert_fingerprints": [
"{SHA256_FINGERPRINT}"
]
}
}
]

The /* catch-all in apple-app-site-association tells iOS to open any URL from your domain in your app, except paths excluded by NOT rules. The handle_all_urls relation in assetlinks.json does the same for Android. iOS periodically re-fetches these files. Android verifies them at install time and on app updates.

This affects all links from your domain:

  • Email links: Links in emails (such as abandoned carts) open in your app.
  • Shared links: Web links from your domain open in your app unless excluded.
  • Web view links: Links opened in a web view don't trigger the app unless you configure the web view to handle them.
  • Excluded paths: (iOS only) URLs with a NOT clause open in the browser.

Anchor to Step 2: Enable linking in your appStep 2: Enable linking in your app

Add an entitlements file that specifies the domains your app handles:

  1. Open your project in Xcode.
  2. Select your app target, then click Signing & Capabilities.
  3. Click + Capability and add Associated Domains.
  4. Add each domain in the format applinks:example.com:

Example.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:example.com</string>
<string>applinks:subdomain.example.com</string>
</array>
</dict>
</plist>

Add an intent filter for your domain to AndroidManifest.xml, then handle incoming intents in your activity:

AndroidManifest.xml

<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" android:host="example.com" />
</intent-filter>
</activity>

Anchor to Configure linking for React NativeConfigure linking for React Native

React Native apps need both platform configurations. Apply the iOS steps to App.entitlements and the Android steps to AndroidManifest.xml.


Anchor to Step 3: Route incoming URLsStep 3: Route incoming URLs

Update your app's entry point to route incoming URLs:

  • /checkout URLs go to Checkout Kit.
  • /cart URLs open your cart screen.
  • Unrecognized URLs fall back to the mobile browser so buyers can complete their purchase.
// App.swift

import SwiftUI
import ShopifyCheckoutSheetKit

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
handleUniversalLink(url: url)
}
}
}

private func handleUniversalLink(url: URL) {
let storefrontUrl = StorefrontURL(from: url)

switch true {
case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage():
presentCheckout(url)
case storefrontUrl.isCart():
navigateToCart()
default:
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}
}

struct ContentView: View {
var body: some View {
Text("Hello, World!")
}
}

public struct StorefrontURL {
public let url: URL

init(from url: URL) {
self.url = url
}

public func isThankYouPage() -> Bool {
return url.path.range(of: "/thank[-_]you", options: .regularExpression) != nil
}

public func isCheckout() -> Bool {
return url.path.contains("/checkouts/")
}

public func isCart() -> Bool {
return url.path.contains("/cart")
}
}
// AppDelegate.swift

import UIKit
import ShopifyCheckoutSheetKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
handleUniversalLink(url: url)
return true
}

return false
}

private func handleUniversalLink(url: URL) {
let storefrontUrl = StorefrontURL(from: url)

switch true {
case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage():
presentCheckout(url)
case storefrontUrl.isCart():
navigateToCart()
default:
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}
}

public struct StorefrontURL {
public let url: URL

init(from url: URL) {
self.url = url
}

public func isThankYouPage() -> Bool {
return url.path.range(of: "/thank[-_]you", options: .regularExpression) != nil
}

public func isCheckout() -> Bool {
return url.path.contains("/checkouts/")
}

public func isCart() -> Bool {
return url.path.contains("/cart")
}
}
// MainActivity.kt

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
App()
}

handleIntent(intent)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
if (intent.action == Intent.ACTION_VIEW) {
intent.data?.let { uri ->
when {
uri.path?.contains("/checkouts/") == true -> {
// Route checkout URLs to Checkout Kit
ShopifyCheckoutSheetKit.present(uri.toString(), this, checkoutEventProcessor)
}
uri.path == "/cart" -> navigateToCart()
else -> {
// Open unrecognized URLs in the browser
startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
}
}
}

private fun navigateToCart() {
// Navigate to the cart screen
}
}
// src/App.tsx

import React, { useState, useEffect } from 'react';
import { Linking } from 'react-native';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { useShopifyCheckoutSheet } from '@shopify/checkout-sheet-kit';

type RootStackParamList = {
Cart: undefined;
};

const useInitialURL = (): {url: string | null} => {
const [url, setUrl] = useState<string | null>(null);

useEffect(() => {
const getUrlAsync = async () => {
const initialUrl = await Linking.getInitialURL();

if (initialUrl !== url) {
setUrl(initialUrl);
}
};

getUrlAsync();
}, [url]);

return {
url,
};
};

function Routes() {
const shopify = useShopifyCheckoutSheet();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const {url: initialUrl} = useInitialURL();

useEffect(() => {
async function handleUniversalLink(url: string) {
const storefrontUrl = new StorefrontURL(url);

switch (true) {
case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage():
shopify.present(url);
return;
case storefrontUrl.isCart():
navigation.navigate('Cart');
return;
}

const canOpenUrl = await Linking.canOpenURL(url);

if (canOpenUrl) {
await Linking.openURL(url);
}
}

if (initialUrl) {
handleUniversalLink(initialUrl);
}

const subscription = Linking.addEventListener('url', ({url}) => {
handleUniversalLink(url);
});

return () => {
subscription.remove();
};
}, [initialUrl, shopify, navigation]);

return (
// Route components
);
}

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('/checkouts/');
}

isCart() {
return this.url.includes('/cart');
}
}

Anchor to Step 4: Test your configurationStep 4: Test your configuration

Verify that your custom domain serves the .well-known files. The response returns valid JSON containing your app ID (iOS) or package name (Android). If you get a 404 or empty response, then check that your custom domain is configured correctly:

curl https://example.com/.well-known/apple-app-site-association | jq .
curl https://example.com/.well-known/assetlinks.json | jq .

Trigger URLs manually to test linking in a simulator or emulator. Your app opens and routes to the appropriate screen. If the browser opens instead, then check your entitlements (iOS) or intent filters (Android):

xcrun simctl openurl booted "https://www.example.com/cart"
adb shell am start -a android.intent.action.VIEW -d "https://example.com/cart"

Open Settings on a physical device (not a simulator) and search for Universal Links. Look for your domain in the diagnostics list with a green checkmark. If it shows a warning or doesn't appear, then reinstall the app:

Settings > DeveloperUniversal Links > DiagnosticsLink configured
iOS Settings showing the Developer section.
Universal Links diagnostics screen showing associated domains.
A configured Universal Link pointing to the storefront domain.

Use the App Links Assistant in Android Studio to verify your app links.

Android Studio App Links Assistant showing verified app links.


Was this page helpful?