Skip to main content

ReactivClipKit

Last updated: October 16, 2026

ReactivClipKit developer documentation banner: Build native App Clip experiences for iOS

Use ReactivClipKit to put your Reactiv shopping experience inside an App Clip or your existing iOS app. Shoppers can browse products and collections, add to cart and check out. You don't build those screens yourself.

ReactivClipKit ("ClipKit") is a closed-source iOS framework from Reactiv, distributed as a Swift package. Layout, content and styling come from the Reactiv Dashboard. Your code only starts ClipKit, shows its view, and passes it a few system events.

An App Clip is a small part of your iOS app that opens instantly from a link, QR code or NFC tag. Shoppers don't need to install your full app first.

You can integrate ClipKit in two ways:

IntegrationWhat it doesUse it when
Standalone App ClipClipKit is the whole App Clip. This is the standard integration.You want shoppers to buy without installing your app.
Full app (ClipKit 2.3 and later)ClipKit opens as a full-screen cover inside your existing app when a Reactiv link or push arrives.Shoppers already have your app installed.

The first half of this page shows you how to set up each integration. The second half explains how ClipKit works, what data it handles, and how to plan the rollout. Engineering and security reviewers will find that material there.

tip

Prefer video? Watch the ClipKit integration walkthrough. The package lives in the ReactivClipKit-SPM repository. For the wider platform, see the Reactiv platform overview.

Before you begin​

Check the requirements​

RequirementDetails
iOS16.0 or later. iOS 26 is supported with Xcode 26.
Xcode16.0 or later. Xcode 26 is recommended.
Swift5.9 or later
App entry pointSwiftUI (@main App)
Package managerSwift Package Manager
Full app modeClipKit 2.3 or later. For a UIKit or React Native app, contact Reactiv first.

Get your credentials​

Reactiv issues these values through the Dashboard. If you don't have them, ask your Reactiv integration contact.

ValueWhat it is
appIdentifierThe UUID of your Reactiv app
reactivEventsTokenA bearer token (JWT) for Reactiv's events and push APIs
appStoreIDYour app's numeric ID in App Store Connect, used for store links
parentBundleIdentifierThe bundle identifier of your full iOS app, for example com.yourcompany.app

If you run several regional storefronts, you get a UUID, store URL and events token for each one. See Serve several storefronts.

Set up a standalone App Clip​

Follow these steps when ClipKit is the whole App Clip. Make every change on the App Clip target, not on your full app's target. Capabilities and Info.plist keys don't carry over between targets.

1. Add ClipKit to your App Clip target​

  • In Xcode, choose File > Add Package Dependencies…
  • Enter https://github.com/reactivapp/ReactivClipKit-SPM
  • Set the version rule to Up to Next Major Version, starting at 2.3.0
  • Add the ReactivClipKit product to your App Clip target

If you manage dependencies in Package.swift, add this instead:

dependencies: [
.package(url: "https://github.com/reactivapp/ReactivClipKit-SPM", from: "2.3.0")
]

ReactivClipKit now appears under Package Dependencies in the Project navigator.

2. Add an App Group​

ClipKit keeps a small amount of shared state, including a pseudonymous identifier, in an App Group. An App Group is a storage area that targets from the same developer can share.

  • Select your App Clip target and open Signing & Capabilities
  • Click + Capability and add App Groups
  • Add the group identifier your Reactiv integration contact gives you

The App Clip's .entitlements file now lists the group under com.apple.security.application-groups. Your full app doesn't need this group unless you want to read ClipKit's shared state there.

3. Turn on push notifications​

ClipKit uses push notifications to bring shoppers back, for example when an item is back in stock.

  • Select your App Clip target and open Signing & Capabilities
  • Click + Capability and add Push Notifications
  • In the App Clip's Info.plist, set NSAppClipRequestEphemeralUserNotification to true:
<key>NSAppClip</key>
<dict>
<key>NSAppClipRequestEphemeralUserNotification</key>
<true/>
</dict>

The capability adds aps-environment to the App Clip's .entitlements file. The Info.plist key gives the App Clip an 8-hour notification window, so shoppers never see a permission prompt.

warning

Without the capability, registerForRemoteNotifications() fails with no valid "aps-environment" entitlement string found for application. Without the Info.plist key, iOS silently drops notifications sent to the App Clip.

4. Let your store's domain open the App Clip​

Skip this step if shoppers only open the App Clip through Apple's default appclip.apple.com links.

To open it from links on your own domain, the App Clip and your domain must name each other. Apple calls this associated domains.

  • On the App Clip target, click + Capability and add Associated Domains
  • Add one appclips: entry for each store domain, for example appclips:shop.example.com
  • Serve https://shop.example.com/.well-known/apple-app-site-association from your store. List the App Clip's app ID, which is your team ID plus the App Clip's bundle ID:
{ "appclips": { "apps": ["ABCDE12345.com.example.app.Clip"] } }

Once the build is uploaded, App Store Connect shows a domain status for it. If it shows No Domain URL Status data, the appclips: entry is missing from the App Clip target. You can't create advanced App Clip experiences on your domain until it's fixed. Apple explains the setup in Associating your App Clip with your website.

5. Pass notification events to ClipKit​

ClipKit doesn't register for push notifications by itself. Your App Clip registers, then hands ClipKit the device token and any notification taps. The device token is the address Apple's push service (APNs) uses to reach this device.

Add an AppDelegate.swift file to the App Clip target:

// AppDelegate.swift
import ReactivClipKit
import UIKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications()
return true
}

// Required: hand the APNs device token to ClipKit
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
NotificationCenter.default.postDeviceTokenReceived(deviceToken: deviceToken)
}

// Required: hand notification taps to ClipKit
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
NotificationCenter.default.postNotificationTapped(response: response)
completionHandler()
}
}
warning

Don't call UNUserNotificationCenter.current().requestAuthorization(...) in an App Clip. The App Clip already gets its notification window from the Info.plist key in step 3. A permission dialog would interrupt the shopper. Call only registerForRemoteNotifications().

6. Start ClipKit and show its view​

Start ClipKit in your SwiftUI App's init, not in the AppDelegate. Then make ReactivClipView() the root view.

// MyAppClip.swift
import ReactivClipKit
import SwiftUI

@main
struct MyAppClip: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
do {
try ReactivClipInitialize(
appIdentifier: "your-app-id",
reactivEventsToken: "your-events-token",
appStoreID: "123456789",
parentBundleIdentifier: "com.yourcompany.app"
)
} catch {
print("ReactivClipKit initialization failed: \(error)")
}
}

var body: some Scene {
WindowGroup {
ReactivClipView()
}
}
}

Run the App Clip target. It opens the experience you published in the Reactiv Dashboard, and ReactivClipIsInitialized() returns true. If you see an error in the console instead, see Troubleshooting.

7. Test a landing page​

The Dashboard can send shoppers to a landing page, such as pages/test. To test one, run the App Clip with a matching invocation URL, the link that opens the App Clip.

  • Select the App Clip scheme and open Edit Scheme… > Run > Arguments
  • Under Environment Variables, add _XCAppClipURL
  • Set its value to the full page URL, for example https://shop.example.com/pages/test
  • Run the App Clip target

The App Clip opens on that landing page.

If you see the fallback Oops screen instead:

  • Check that the page is published in the Dashboard.
  • Check that the URL is a full URL that matches your store's domain and path.
  • Stop the App Clip and run it again. After Dashboard changes, you sometimes need to relaunch twice.

Embed ClipKit in your existing app​

Follow these steps to add ClipKit to your existing SwiftUI app. When a Reactiv link or push notification arrives, the shopping experience opens as a full-screen cover. When the shopper closes it, they're back on the screen they left.

Full app mode needs ClipKit 2.3 or later. Only three places in your app touch ClipKit.

note

This API is stable for the 2.3.x line. A namespace reorganization is planned for 3.x. Reactiv will publish a migration guide before 3.0 ships.

1. Check your app's setup​

  • Your app target has the Push Notifications capability. If your app already sends push notifications through a service like Firebase, OneSignal or Klaviyo, it has it.
  • Your App Clip is registered under your app in App Store Connect. Reactiv handles provisioning with you, so check with your integration contact.

2. Add ClipKit to your app target​

  • In Xcode, choose File > Add Package Dependencies…
  • Enter https://github.com/reactivapp/ReactivClipKit-SPM
  • Set the version rule to Up to Next Major Version, starting at 2.3.0
  • Add the ReactivClipKit product to your app target

ReactivClipKit now appears under Package Dependencies in the Project navigator.

3. Start ClipKit in full app mode and wrap your root view​

In your @main App, pass "REACTIV_FULL_APP_MODE": true when you start ClipKit. Then wrap your root view in ReactivClipHost.

// MyHostApp.swift
import ReactivClipKit
import SwiftUI

@main
struct MyHostApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
do {
try ReactivClipInitialize(
appIdentifier: "your-app-id",
reactivEventsToken: "your-events-token",
appStoreID: "123456789",
parentBundleIdentifier: "com.yourcompany.app",
initializationOptions: [
"REACTIV_FULL_APP_MODE": true
]
)
} catch {
print("ReactivClipKit initialization failed: \(error)")
}
}

var body: some Scene {
WindowGroup {
ReactivClipHost {
MyHostHomeScreen() // Your app's existing root view
}
}
}
}

ReactivClipHost shows your own screens by default. When a Reactiv link or push arrives, it opens ReactivClipView() as a full-screen cover. It also handles closing the cover, and it holds on to links and pushes that arrive while your app is still launching.

Full app mode also changes how ClipKit behaves:

  • It hides App Clip-only UI, such as the App Store overlay and Update Now prompts.
  • It cancels only the notifications ClipKit scheduled, never your app's.
  • It never asks for notification permission or registers for push. Your app already does that.
  • It reports analytics under the platform name Reactiv Clip Embedded.
warning

Put ReactivClipHost at the scene root. Don't add your own .onOpenURL or .onContinueUserActivity for appclip.apple.com links at the scene level. They would fire alongside the handlers inside ReactivClipHost.

Add these methods to your existing AppDelegate. ClipKit assumes your app already sets the notification center delegate, asks for permission and registers for push. The example shows that setup so the file is complete. Keep your own version if you have one.

// AppDelegate.swift
import ReactivClipKit
import UIKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
// Your app's existing push setup
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
application.registerForRemoteNotifications()
return true
}

// 1. Hand the APNs device token to ClipKit, which registers it with Reactiv's push API
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
NotificationCenter.default.postDeviceTokenReceived(deviceToken: deviceToken)
}

// 2. Hand notification taps to ClipKit. The closure runs for pushes that aren't Reactiv's.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
NotificationCenter.default.handleReactivNotificationTap(response: response) {
// Route your app's own push notifications here, or leave this empty.
}
completionHandler()
}

// 3. Hand appclip.apple.com links to ClipKit
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if let url = userActivity.webpageURL, url.host?.lowercased() == "appclip.apple.com" {
NotificationCenter.default.forwardInvocationURL(url)
return true
}
return false
}

// Show a banner when a push arrives while your app is open
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .list])
}
}

What each forwarding method does:

  • Device token. ClipKit reuses the token your app already has. It never registers for push itself.
  • Notification taps. ClipKit checks the payload for a reactiv_kind key. If it's there, ReactivClipHost opens the experience. If it isn't, your closure runs.
  • Links. Tools like Branch, OneSignal, Firebase Dynamic Links and Klaviyo often intercept Universal Links before SwiftUI sees them. Forwarding from the AppDelegate makes sure ClipKit still gets Reactiv's links. If your app has no such tools, you can leave this method out. It's harmless to keep.

5. Test the integration​

Test a push notification in the Simulator. Save this payload as my-payload.apns. Reactiv pushes include reactiv_kind plus the field that kind needs, here reactiv_handle:

{
"Simulator Target Bundle": "com.yourcompany.app",
"aps": {
"alert": { "title": "Back in stock", "body": "Your favorite is back" },
"sound": "default"
},
"reactiv_kind": "product",
"reactiv_handle": "heavyweight-tee"
}

Send it to the booted Simulator, or drag the file onto the Simulator window:

xcrun simctl push booted com.yourcompany.app ./my-payload.apns

The banner appears. Tap it, and the experience opens on the heavyweight-tee product page. This works whether your app is in the foreground, in the background or closed. With several simulators booted, replace booted with a device UDID from xcrun simctl list devices booted.

Test a link in the Simulator. This needs Associated Domains on your build and an App Clip registered in App Store Connect:

xcrun simctl openurl booted "https://appclip.apple.com/id?p=com.yourcompany.app.Clip&action=home"

The experience opens on its home screen. If your development build isn't set up for this, see Test links without Associated Domains.

Test on a device. This needs your app and App Clip to be live on the App Store.

  • Build and run your app on an iPhone with the same bundle IDs as the live app
  • In Notes or Messages, tap https://appclip.apple.com/id?p=com.yourcompany.app.Clip&action=home, using your App Clip's bundle ID

Your app comes to the foreground and opens the experience. The close chevron takes you back to your own screens.

Troubleshooting​

Starting ClipKit throws an error. ReactivClipInitialize throws a ReactivClipInitError. Print it in your catch block and match it here:

ErrorCause and fix
missingAppIdentifierThe app ID is empty or invalid. Check appIdentifier.
missingEventsTokenThe events token is missing. Check reactivEventsToken.
missingAppStoreIDThe App Store ID is missing. Check appStoreID.
missingParentBundleIDThe parent bundle ID is missing. Check parentBundleIdentifier.
multipleInitializationClipKit was already started. Start it once, in your App's init.

The App Clip shows no content. Check your app ID and tokens, and look for errors in the console. Then confirm that ReactivClipIsInitialized() returns true.

Push notifications don't arrive.

  • Check that the Push Notifications capability is on the right target.
  • Check that you set the notification center delegate and call registerForRemoteNotifications().
  • Check that you pass the token with postDeviceTokenReceived(deviceToken:).
  • In an App Clip, check that NSAppClipRequestEphemeralUserNotification is set.

Tapping a notification doesn't open the experience.

  • In an App Clip, call postNotificationTapped(response:).
  • In a full app, call handleReactivNotificationTap(response:ifNotReactiv:) and set REACTIV_FULL_APP_MODE.
  • Make sure the push payload includes reactiv_kind. Without it, ClipKit treats the push as your app's.

The experience opens on the home screen instead of the right page. The push had reactiv_kind but not the field that kind needs, such as reactiv_handle. ClipKit falls back to the home screen. Check the payloads your push system sends.

A link doesn't open the experience in your full app.

  • The link's host must be exactly appclip.apple.com. A Rejected invocation URL message in the console means ClipKit ignored a different host, as designed.
  • Your App Clip must be registered under your app in App Store Connect. Your app doesn't need its own entitlement for this, because iOS routes through the App Clip's parent-application-identifiers.
  • Forward appclip.apple.com links from application(_:continue:restorationHandler:). Another SDK may be intercepting them.

Tapping a push while the app is closed doesn't open the experience. Mount ReactivClipHost in the root scene at launch. Don't put it inside a view that appears later.

No analytics events arrive. Subscribe to events after ClipKit starts, as shown in Send events to your analytics.

Customize your integration​

Everything above covers a working integration. These options are for specific needs.

Serve several storefronts​

If your brand runs regional storefronts, start ClipKit once with one StoreDescriptor per store. Use this instead of ReactivClipInitialize in your App's init:

let stores: [StoreDescriptor] = [
StoreDescriptor(uuid: "intl-uuid", storeURL: "https://domain.com", eventsToken: "token-intl"),
StoreDescriptor(uuid: "ca-uuid", storeURL: "https://domain.com/ca", eventsToken: "token-ca"),
StoreDescriptor(uuid: "au-uuid", storeURL: "https://domain.com/au", eventsToken: "token-au"),
StoreDescriptor(uuid: "gb-uuid", storeURL: "https://domain.com/gb", eventsToken: "token-gb"),
]

do {
try ReactivClipInitializeMultiStore(
stores: stores,
appStoreID: "123456789",
parentBundleIdentifier: "com.yourcompany.app"
)
} catch {
print("ReactivClipKit initialization failed: \(error)")
}

In a full app integration, keep initializationOptions: ["REACTIV_FULL_APP_MODE": true] when you switch to ReactivClipInitializeMultiStore. Without it, ClipKit runs in standalone App Clip mode.

ClipKit picks the store whose storeURL is the longest match for the start of the invocation URL. A link to https://domain.com/ca/products/tee opens the Canadian store. If nothing matches, ClipKit uses the first store in the list.

Turn off the cart​

Set CARTLESS_MODE to make the experience browse-only. The cart button disappears, and the add-to-cart message no longer opens the cart.

try ReactivClipInitialize(
appIdentifier: "your-app-id",
reactivEventsToken: "your-events-token",
appStoreID: "123456789",
parentBundleIdentifier: "com.yourcompany.app",
initializationOptions: [
"CARTLESS_MODE": true
]
)

Both start functions accept these options:

KeyTypeDefaultWhat it does
CARTLESS_MODEBoolfalseHides the cart for a browse-only experience
REACTIV_FULL_APP_MODEBoolfalseTurns on full app mode for ClipKit inside your existing app

Use your brand fonts​

Custom fonts are experimental. Make these changes on the App Clip target:

  • Add your .ttf or .otf files to the target
  • List each file name under UIAppFonts in the Info.plist
  • Set ReactivClipFontFamilyName in the Info.plist to the family's base name, for example BrandSans
  • Include three weights: BrandSans-Regular, BrandSans-SemiBold and BrandSans-Bold

With all three weights present, ClipKit uses your font everywhere. If a weight is missing, ClipKit substitutes the closest weight or the default font, so the result may not match your brand.

Send events to your analytics​

ClipKit publishes typed events for product views, cart changes, checkout, navigation and more. Subscribe after ClipKit starts, then pass each event to your analytics tool.

// AnalyticsManager.swift
import ReactivClipKit

final class AnalyticsManager {
static let shared = AnalyticsManager()

func start() {
let publisher = ReactivClipEvents.publisher

// One event type, with typed data
publisher.on(.productViewed) { event in
if let product = event.productViewedData {
print("Product viewed: \(product.productTitle) (\(product.productId))")
}
}

// Typed data through the generic accessor
publisher.on(.cartItemAdded) { event in
if let cart: CartItemAddedData = event.typedData() {
print("Item added to cart \(cart.cartId)")
}
}

// Every event
publisher.onEvent { event in
print("Event received: \(event.type.rawValue)")
}
}

func stop() {
ReactivClipEvents.publisher.removeAllHandlers()
}
}

Call AnalyticsManager.shared.start() right after ReactivClipInitialize in your App's init. When a shopper opens a product, the console shows lines like Product viewed: Heavyweight Tee (…) and Event received: productViewed. Replace the print calls with calls to your analytics SDK.

GroupEvent types
ProductproductViewed, productShared
CartcartItemAdded, cartItemRemoved, cartItemUpdated, cartView
CheckoutcheckoutStarted, checkoutSuccess, checkoutFailed
AppscreenViewed, notificationOpened, notificationScheduled, applicationOpened
CollectioncollectionViewed
UIbuttonTapped, searchFocus, searchApplied
Customcustom

The sample App Clip in the package's Examples/ folder includes a fuller AnalyticsManager.

This applies to full app mode. ClipKit only handles links in this form:

https://appclip.apple.com/id?p=<your-clip-bundle-id>&action=<action>

Links on your own domain, such as https://shop.yourcompany.com/products/tee, go to your app's own navigation. To send one to ClipKit, convert it to the appclip.apple.com form and forward it:

// MyHostApp.swift, inside body
WindowGroup {
ReactivClipHost {
MyHostHomeScreen()
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
guard let url = activity.webpageURL, url.host == "shop.yourcompany.com" else { return }
if let appClipURL = appClipURL(for: url) {
NotificationCenter.default.forwardInvocationURL(appClipURL)
}
}
}

// Elsewhere in your app
func appClipURL(for merchantURL: URL) -> URL? {
var components = URLComponents(string: "https://appclip.apple.com/id")
components?.queryItems = [
URLQueryItem(name: "p", value: "com.yourcompany.app.Clip"),
URLQueryItem(name: "action", value: "home"),
]
return components?.url
}

Tapping a link to shop.yourcompany.com now opens the experience on its home screen. This handler only matches your own domain, so it doesn't conflict with ReactivClipHost.

note

This example opens the home screen for every link. To open a specific product or page, ask your Reactiv integration contact for the action and parameter names to use.

Hide Reactiv banners in some flows​

This applies to full app mode. The willPresent method in step 4 shows every push as a banner. To hide Reactiv banners at certain times, such as during your own checkout, check for reactiv_kind:

func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let isReactivPush = notification.request.content.userInfo["reactiv_kind"] != nil
if isReactivPush && CheckoutState.shared.isInCheckout {
completionHandler([]) // Hide Reactiv banners during your checkout
} else {
completionHandler([.banner, .sound, .list])
}
}

CheckoutState stands for whatever state your app already keeps about its checkout flow.

Present the experience yourself​

Almost every full app integration should use ReactivClipHost. Skip it only if you need full control, such as a sheet instead of a full-screen cover, or presentation driven by your own router.

Without ReactivClipHost, you take over three jobs:

  • Links that arrive during launch. Listen for com.reactivapp.invocationURLReceived as well as .onOpenURL. Otherwise you miss links delivered before SwiftUI is ready.
  • Duplicate handling. Attach .onOpenURL and .onContinueUserActivity in one place only, or a link can fire twice.
  • Presentation details. You handle state preservation and animation timing.

This example presents the experience as a sheet. A shared state object lets both SwiftUI and the AppDelegate open it.

// HostPresentationState.swift
import Combine

@MainActor
final class HostPresentationState: ObservableObject {
static let shared = HostPresentationState()
@Published var showClipKit = false
private init() {}
}
// RootView.swift
import Combine
import ReactivClipKit
import SwiftUI

struct RootView: View {
@ObservedObject private var state = HostPresentationState.shared

var body: some View {
MyHostHomeScreen()
.sheet(isPresented: $state.showClipKit) {
ReactivClipView()
}
.onOpenURL { url in
guard url.scheme?.lowercased() == "https", url.host == "appclip.apple.com" else { return }
NotificationCenter.default.forwardInvocationURL(url)
state.showClipKit = true
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
guard let url = activity.webpageURL, url.host == "appclip.apple.com" else { return }
NotificationCenter.default.forwardInvocationURL(url)
state.showClipKit = true
}
// Links delivered before SwiftUI was ready
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("com.reactivapp.invocationURLReceived"))) { _ in
state.showClipKit = true
}
// The shopper tapped the close chevron
.onReceive(ReactivClipCommands.publisher.filter { $0.type == .clipDismissRequested }) { _ in
state.showClipKit = false
}
}
}

In your AppDelegate, open the experience when a Reactiv push is tapped:

func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
NotificationCenter.default.handleReactivNotificationTap(response: response) {
// Route your app's own push notifications here
}
if response.notification.request.content.userInfo["reactiv_kind"] != nil {
Task { @MainActor in HostPresentationState.shared.showClipKit = true }
}
completionHandler()
}

Use RootView() in place of ReactivClipHost in your App's body. A Reactiv link or push now opens the experience in a sheet, and the close chevron dismisses it.

If your development build can't receive appclip.apple.com links, use a custom URL scheme for testing only. Register a devtest scheme under URL Types in your app's Info tab. Then convert those links to https and forward them:

// DevSchemeURLs.swift. Development only, remove from release builds.
import ReactivClipKit
import SwiftUI

extension View {
func forwardDevSchemeURLs() -> some View {
onOpenURL { url in
guard url.scheme?.lowercased() == "devtest" else { return }
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.scheme = "https"
if let httpsURL = components?.url {
NotificationCenter.default.forwardInvocationURL(httpsURL)
}
}
}
}
// MyHostApp.swift, inside body
WindowGroup {
ReactivClipHost {
MyHostHomeScreen()
}
#if DEBUG
.forwardDevSchemeURLs()
#endif
}

Run this command, and the experience opens on its home screen:

xcrun simctl openurl booted "devtest://appclip.apple.com/id?p=com.yourcompany.app.Clip&action=home"

API reference​

Starting ClipKit​

// Start ClipKit for one storefront. Throws ReactivClipInitError.
try ReactivClipInitialize(
appIdentifier: String, // Your Reactiv app ID
reactivEventsToken: String, // Events token
appStoreID: String, // App Store Connect app ID
parentBundleIdentifier: String, // Your full app's bundle ID
initializationOptions: [String: Any]? = nil // CARTLESS_MODE, REACTIV_FULL_APP_MODE
)

// Start ClipKit for several storefronts. Throws ReactivClipInitError.
try ReactivClipInitializeMultiStore(
stores: [StoreDescriptor],
appStoreID: String,
parentBundleIdentifier: String,
initializationOptions: [String: Any]? = nil
)

public struct StoreDescriptor {
public let uuid: String // Store UUID from the Reactiv Dashboard
public let storeURL: String // For example "https://domain.com/ca"
public let eventsToken: String // Events token for this store
}

// Returns true once ClipKit has started
let isInitialized: Bool = ReactivClipIsInitialized()

Views​

// The root view of a standalone App Clip
ReactivClipView()

// Full app container (2.3+). Needs REACTIV_FULL_APP_MODE.
public struct ReactivClipHost<HostContent: View>: View {
public init(@ViewBuilder hostContent: @escaping () -> HostContent)
}
// Hand over the APNs device token (both integrations)
NotificationCenter.default.postDeviceTokenReceived(deviceToken: Data)

// Hand over a notification tap (standalone App Clip)
NotificationCenter.default.postNotificationTapped(response: UNNotificationResponse)

// Hand over a notification tap (full app, 2.3+).
// Reactiv pushes open the experience; ifNotReactiv runs for anything else.
NotificationCenter.default.handleReactivNotificationTap(
response: UNNotificationResponse,
ifNotReactiv: @escaping () -> Void = {}
)

// Hand over a link (2.3+). Only https://appclip.apple.com/... links are accepted.
// Anything else is dropped with a warning in the console.
NotificationCenter.default.forwardInvocationURL(_ url: URL)

Events​

let publisher = ReactivClipEvents.publisher

publisher.on(.productViewed) { event in } // One event type
publisher.onEvent { event in } // Every event
publisher.removeAllHandlers() // Stop all handlers

event.productViewedData // Typed data for each event type
let data: CartItemAddedData? = event.typedData()

Commands (2.3+)​

Commands are control signals for your app, separate from analytics events. They arrive on the main queue. ReactivClipHost listens for them for you.

public enum ReactivClipCommands {
public static var publisher: AnyPublisher<ReactivClipCommand, Never> { get }
}

public struct ReactivClipCommand: Equatable {
public let type: ReactivClipCommandType
public let timestamp: Date
}

public enum ReactivClipCommandType: String, CaseIterable, Codable {
case clipDismissRequested // The shopper tapped the close chevron
}

Errors​

enum ReactivClipInitError: Error {
case missingAppIdentifier // The app ID is empty or invalid
case missingEventsToken // The events token is missing
case missingAppStoreID // The App Store ID is missing
case missingParentBundleID // The parent bundle ID is missing
case multipleInitialization // ClipKit has already started
}

How ClipKit works​

The rest of this page is for engineering, security and procurement reviewers. It describes ClipKit 2.3. Unless a section says otherwise, it covers the standalone App Clip integration. Statements about Apple's platform link to Apple's documentation.

Architecture​

ComponentRoleControlled by
Your full iOS appThe parent app. In the standard integration, it doesn't link ClipKit.You
App Clip targetA separate target in your Xcode project that links ClipKit. You build, sign and submit it with your app.You
ClipKitA compiled framework inside the App Clip. It shows the experience, including cart and checkout, and emits analytics events.Reactiv
Reactiv servicesServe the merchant's published configuration, plus commerce, analytics and push features.Reactiv
App Store ConnectWhere you define App Clip experiences: invocation URLs and card content.You, on Apple's platform
Associated domains (AASA)A file on your domain that names the App Clip allowed to handle its links.You
Attribution SDK (optional)For example AppsFlyer. Sits in the App Clip target next to ClipKit and provides attribution and deep-link data.You and the SDK vendor
ShopifyCatalog, cart, checkout and payment for the merchant's storefront.Shopify and the merchant

Where ClipKit runs​

In the standard integration, ClipKit runs only in the App Clip. You link it into the App Clip target, not your main app target. Reactiv code doesn't run as part of your main app's executable. Your main app gains only the App Clip embedding build phase that Xcode adds for any App Clip. ClipKit doesn't change your main app's sources or dependencies.

Apple keeps the App Clip separate. Apple documents an App Clip as a separate target, embedded in your full app's bundle, with its own data container. App Clip restrictions limit its functionality, background activity and size (Creating an App Clip with Xcode, Choosing the right functionality for your App Clip). The App Clip and your full app share data only through capabilities and configuration you turn on (Sharing data between your App Clip and your full app).

ClipKit uses a small shared container. ClipKit needs an App Group capability on the App Clip target. An App Group is a storage area that targets from the same developer can share. ClipKit keeps limited state there, including a pseudonymous identifier it generates. Your full app can read that state only if you add the same capability and code to it. The standard integration doesn't need that. ClipKit doesn't use the keychain.

Full app mode is different. In full app mode, you link ClipKit into your main app target. It shows the experience over your app's screens when a Reactiv link or push arrives. In that mode, Reactiv code does run inside your main app's executable. It's a separate integration that you choose explicitly. The boundary statements on this page don't cover it unless they say so.

How an App Clip opens​

Apple controls how an App Clip opens. Shoppers can open one from:

  • An App Clip Code, NFC tag or QR code
  • A Safari Smart App Banner, or a link in Messages, on a website or in email
  • A Maps place card or Siri Suggestions

iOS then shows the system App Clip card. Its content comes from the App Clip experiences you define in App Store Connect, and code can't change it (Configuring the launch experience of your App Clip). Apple creates a default appclip.apple.com link without any server changes. Links on your own domain need the appclips: associated-domain entitlement and an appclips entry in your domain's apple-app-site-association file (Associating your App Clip with your website).

ClipKit takes over after the shopper taps Open. iOS passes the invocation to the App Clip. ClipKit uses it, with the merchant's Reactiv configuration, to show the right experience. ClipKit has no say in whether the card appears, what it shows, or whether iOS allows the launch.

What runs on the device and what runs on servers​

Reactiv's services supply what the App Clip shows. The App Clip itself is client code that Apple distributes with your app. Apple's App Clip model has no way for a server to supply the App Clip's executable at runtime. Apple's App Store Review Guidelines also don't allow any app, including an App Clip, to download and run code. So Reactiv's services work alongside ClipKit. They can't replace the framework that shows the experience.

ResponsibilityWhere it runs
Showing and running the experience, presenting checkout, emitting eventsOn the device, inside the App Clip, in ClipKit
Serving configuration, commerce, analytics and push featuresReactiv services
Catalog, cart, checkout, paymentShopify
Invocation, App Clip card, size limits, review, distribution, removal after inactivityApple

ClipKit and attribution SDKs​

Attribution, analytics, deep linking and showing the experience are separate jobs.

  • ClipKit shows and runs the experience. It also publishes a typed event stream, described in Send events to your analytics. Your App Clip can forward those events to any analytics SDK you already use.
  • An attribution SDK, such as AppsFlyer, sits in the App Clip target next to ClipKit if you include one. AppsFlyer documents its App Clip support as attribution, measurement and OneLink routing. You add its standard SDK to the App Clip target and forward the invocation to it. To attribute installs of your full app that start in the App Clip, you also set up an App Group (AppsFlyer App Clip SDK integration, App Clip to full app install). ClipKit's public API lets you forward the invocation to such an SDK.
  • App Clips limit attribution. Apple doesn't provide SKAdNetwork in App Clips. App Clips can't request App Tracking Transparency permission, and identifierForVendor is empty (Choosing the right functionality for your App Clip). AppsFlyer states that SKAdNetwork and advertising identifiers aren't available in App Clips (AppsFlyer App Clip overview).

An attribution SDK doesn't replace ClipKit, and ClipKit doesn't replace an attribution SDK. If you already use one, you still need client code that shows the experience. In a Reactiv integration, that code is ClipKit. If you build the App Clip experience with your own code instead, ClipKit isn't involved at runtime.

The AppsFlyer details above come from AppsFlyer's documentation as of this page's date. Confirm current behavior with AppsFlyer.

Data and security​

What data ClipKit handles​

Data collected on the device:

  • Shopping events: screens and products viewed, cart changes, search terms, checkout steps and completed purchases
  • Device and app context
  • An approximate location, derived from the network connection
  • A pseudonymous identifier that ClipKit generates
  • Any advertising click identifiers in the invocation link

ClipKit has no shopper sign-in. It sends contact details in only two cases:

  • When a shopper submits a sign-up form the merchant has turned on: email address, phone number and name.
  • After a completed checkout, for the merchant's advertising and marketing: name and partial billing address (state and postal code).

ClipKit sends the device's own location only for store locator and in-store availability features, and only after the shopper allows it.

Data ClipKit receives from Reactiv. The merchant's published configuration, feature configuration, and commerce data such as store availability, reviews and bundles.

Data ClipKit sends to Reactiv. The events and context above, the push device token when your app forwards it, and crash and error diagnostics.

Other services ClipKit talks to. The merchant's Shopify storefront domain and Shopify's CDN, for catalog, cart, checkout and media. Location features may derive an approximate location from network information. Crash and error diagnostics go to Reactiv's error-monitoring provider.

Credentials. You start ClipKit with merchant-specific identifiers and an events token from Reactiv.

What ClipKit stores on the device.

  • Cached configuration and catalog responses
  • Cached images
  • Diagnostics waiting to upload
  • A small amount of preference state
  • The shared container state described in Where ClipKit runs

ClipKit 2.3 in the standard integration doesn't collect or store shopper credentials or payment details. Shoppers enter payment details in Shopify's hosted checkout.

How long data lasts. Apple removes an App Clip and its data from the device after a period of inactivity (App Clips overview). Reactiv handles the data it receives under the Data Processing Addendum and the retention practices in the Security Overview.

Security model​

App boundary. ClipKit runs inside the App Clip target's bundle and data container. In the standard integration, it isn't part of your main app's executable.

Apple's App Clip restrictions. App Clips can't run in the background, request App Tracking Transparency permission or use SKAdNetwork. They also can't use a documented list of frameworks (Choosing the right functionality for your App Clip). Apple enforces an uncompressed size limit that depends on the App Clip's minimum iOS version and how it opens. These limits apply to any code in the App Clip. They don't replace reviewing what ClipKit does within them.

Entitlements and Info.plist keys on the App Clip target.

ItemRequired byPurpose
com.apple.developer.parent-application-identifiersAppleNames your full app. Every App Clip needs it.
com.apple.developer.associated-domains with appclips: entriesAppleNeeded only for links on your own domain. Apple recommends adding it to both your app and App Clip targets.
App Group capabilityReactivNeeded on the App Clip target for limited shared state. See Where ClipKit runs. Your full app needs it only if you read that state there.
Push Notifications capability (aps-environment) and NSAppClipRequestEphemeralUserNotificationApple, when you use notificationsApple lets App Clips schedule or receive notifications for up to 8 hours after each launch (Enabling notifications in App Clips). Longer use needs an explicit permission request.
NSLocationWhenInUseUsageDescriptionReactiv, when location features are onClipKit asks for When In Use location only when the shopper opens a store locator or in-store availability feature the merchant turned on. It never asks at launch.
UIAppFonts and ReactivClipFontFamilyNameReactiv, optionalCustom fonts

ClipKit 2.3 in the standard integration asks for no other system permission.

Network. ClipKit uses HTTPS, through the system TLS stack, to reach Reactiv services, Shopify and the services listed in What data ClipKit handles.

How the binary is distributed. ClipKit ships as a precompiled XCFramework through a Swift Package Manager binary target that Reactiv hosts. Swift Package Manager checks each download against the checksum pinned in the package manifest. The current ClipKit XCFramework doesn't include a privacy manifest. A future release will include the privacy declarations for the required-reason APIs ClipKit uses.

Remote configuration. Reactiv can change some ClipKit behavior, and all merchant content, without a release from you. Remote configuration only selects behavior already in the shipped ClipKit binary. It can't deliver or run new native code. Configuration can include web content that ClipKit shows in a web view. That content runs as web content, not native code. Framework upgrades always need a release from you. Reactiv may require you to upgrade outdated builds under the support process in the Service Level Agreement.

Web content. ClipKit uses WKWebView for merchant web content and for Shopify's hosted checkout. Web content doesn't get unrestricted access to ClipKit's native features.

How to review a closed-source framework​

ClipKit ships as a compiled framework, and the standard integration doesn't include its source code. Focus your security review on what you can see:

  • The integration boundary
  • Declared capabilities
  • Runtime behavior and network destinations
  • Data handling
  • The documentation Reactiv provides

You can inspect:

  • The public API, through ClipKit's Swift interface files
  • Which target links ClipKit, and the entitlements and Info.plist keys on the App Clip target
  • Network traffic from the App Clip, with standard proxy tools
  • Files written to the App Clip's container and the shared container
  • The package manifest, version and checksum

Review materials. Start with:

During an enterprise review, Reactiv can share more detail on network destinations, data schemas, credential lifecycle, release verification and runtime security. Send security questions to support@reactiv.ai.

Plan your rollout​

Who does what​

Several of these tracks exist for any App Clip, with or without ClipKit.

TrackWorkOwner
ClipKit integration codeAdd the package to the App Clip target, start ClipKit with Reactiv's identifiers, and show ReactivClipView(). Forward push registration and taps if you use notifications. Subscribe to events if you forward them to analytics.Your iOS team
Apple App Clip setupCreate the App Clip target if you don't have one. Set the parent-application-identifiers entitlement, choose the minimum iOS version, and add the Info.plist keys in Security model.Your iOS team
Domain and AASA setupAdd appclips: associated domains and publish the appclips entry in apple-app-site-association. Needed only for links on your own domain, not Apple's default link.Your web or infrastructure team
App Store Connect setupCreate the default App Clip experience and any advanced experiences: invocation URLs, card image, title, subtitle and action.Your product or release team
Third-party SDK setupAdd and configure any attribution or analytics SDK in the App Clip target, plus any App Group it needs.You, following the vendor's guidance
QAReactiv supports integration QA and checks that the App Clip opens correctly. You test the integrated build in your own app and release environment.Reactiv and you
Security reviewAssess the integration against your partner SDK policy, using the materials in How to review a closed-source framework.Your security team
Release and App Store submissionSubmit the App Clip with your next full app release. Apple doesn't accept an App Clip on its own, and App Clips aren't available through enterprise distribution (Distributing your App Clip).Your release team

Effort depends on your existing App Clip, associated domains, attribution, build, security review and release setup. Say you already have an App Clip target, associated domains and App Store Connect access. Then the ClipKit-specific code is a few dozen lines. That estimate covers the code only, not review, QA or the release cycle. If ClipKit delivers a custom experience rather than the standard Shopify storefront, Reactiv scopes the effort per engagement.

Updates, outages and removal​

Updating ClipKit. Reactiv publishes each release to the package repository as a versioned binary target, with a checksum and a changelog entry. To adopt a release, update the pinned version and ship the App Clip in a full app release. ClipKit doesn't update itself. Reactiv delivers fixes as new releases under the Service Level Agreement, and you adopt them in your own release cycle.

What changes without a release. Merchant content, layout, branding and feature settings published from the Dashboard. Also the configuration Reactiv serves, described under remote configuration in Security model.

What needs a release. New ClipKit versions, and any change to the App Clip target's code, entitlements or build settings. Reactiv may require you to upgrade outdated ClipKit builds under the Service Level Agreement's support process.

When Reactiv is unreachable. The App Clip may show previously cached configuration, or an error screen instead of the experience. Catalog, cart and checkout depend on Shopify's availability, separately from Reactiv.

Removing ClipKit.

  • Remove the package from the App Clip target
  • Remove the target, or stop shipping it
  • Remove the App Clip experiences in App Store Connect

iOS removes installed App Clips after a period of inactivity. Reactiv handles data it already received under the Data Processing Addendum.

Assumptions and known limitations​

What ClipKit assumes about your setup.

  • You own the parent iOS app, its Apple Developer team and its App Store Connect record, and you can add an App Clip target.
  • Apple allows one App Clip target per app (Choosing the right functionality for your App Clip). If you already ship an App Clip, you add ClipKit to that target, next to your own App Clip code. This depends on compatibility and Apple's size limit.
  • You control the domain used for links on your own domain, and you can publish an apple-app-site-association file on it.
  • Your App Clip target uses the SwiftUI app lifecycle and targets iOS 16 or later.
  • You accept that the App Clip ships inside your full app's App Store releases.

Known limitations.

  • Apple requires that "the full app must include the same functionality as the App Clip" (Choosing the right functionality for your App Clip). If your full app doesn't already offer the ClipKit experience, plan how it will. Options include full app mode, your own implementation, or another arrangement agreed with Reactiv. Reactiv doesn't control Apple's review of this requirement.
  • ClipKit's catalog, cart and checkout use Shopify's Storefront API and Checkout Sheet Kit. The standard integration assumes a Shopify storefront.
  • The Reactiv Dashboard's configuration model defines what the experience can do. Anything outside it needs custom development, scoped per engagement.
  • Apple sets the App Clip platform limits, which apply to the whole App Clip. They cover size, background activity, unavailable frameworks, the notification window and removal after inactivity.
  • Full app mode changes the integration boundary. Assess it separately.

FAQ for enterprise teams​

Mobile engineering

  • What exactly do we integrate? A Swift Package Manager binary dependency in the App Clip target, one call to start ClipKit, and one view. Push and event forwarding are optional.
  • Does Reactiv code run inside our main app? Not in the standard integration. It runs inside the App Clip target. Full app mode is the exception, and you choose it explicitly.
  • What changes in our existing app? You add an App Clip target if you don't have one, with the entitlements and Info.plist keys in Security model. Xcode adds the App Clip embedding build phase it adds for any App Clip. ClipKit doesn't change your main app's sources.
  • Why can't this run on a server? The App Clip is client code that Apple distributes with your app. Servers supply what it shows, not the code that shows it.
  • Do we need ClipKit if we already use AppsFlyer? Yes, if Reactiv shows the experience. AppsFlyer handles attribution and routing for the App Clip, but it doesn't show the experience.
  • We already have an App Clip. Does ClipKit need its own? No. Apple allows only one App Clip target per app, so you add ClipKit to your existing one. This depends on compatibility with your App Clip code and Apple's size limit. See Assumptions and known limitations.
  • Does our full app need the same functionality? Apple requires it. See Assumptions and known limitations.

Security

  • What data can ClipKit access? Its own container, the shared App Group container on the App Clip target, and the network. It gets When In Use location only after the shopper opens a location feature. Apple's App Clip restrictions in Security model apply.
  • What data does Reactiv receive? The categories in What data ClipKit handles.
  • How do we evaluate a closed-source binary? See How to review a closed-source framework.

Procurement and legal

Product

  • What can change without a release? Merchant content, layout, branding and feature settings published from the Dashboard, and configuration Reactiv serves.
  • What needs a release? New ClipKit versions, and changes to the App Clip target's code, entitlements or build settings.
  • Can one App Clip serve several storefronts? Yes. See Serve several storefronts.

Versioning and support​

ClipKit follows semantic versioning. Release notes are in the changelog. ClipKit is proprietary software, and its license agreement governs its use.

To report an issue, open one on GitHub or contact your Reactiv integration team. You can also email support@reactivapp.com.


Apple, App Clips, App Store, Swift, SwiftUI, and Xcode are trademarks of Apple Inc., registered in the U.S. and other countries and regions. iOS is a trademark or registered trademark of Cisco in the U.S. and other countries and is used under license. Shopify is a trademark of Shopify Inc. AppsFlyer is a trademark of AppsFlyer Ltd. All other trademarks are the property of their respective owners.

Was this page helpful?