iOS & macOS SDK
Swift Package. iOS 16+ and macOS 13+. Zero external dependencies. Currently at v0.7.0. On macOS the cross-platform core works the same — referrals, onboarding claim, the wishlist API, and deep-link parsing; in-app Safari presentation, the wishlist view, and shake-to-report are iOS-only (on macOS, open hosted flow URLs yourself).
Install
In Xcode → File → Add Package Dependencies…, paste:
https://github.com/fil-technology/appmate-ios
Or in Package.swift:
.package(url: "https://github.com/fil-technology/appmate-ios", from: "0.2.0")
Configure
Once at launch:
import AppMate
RetentionFlow.configure(
.init(
appSlug: "my-ios-app",
baseURL: URL(string: "https://cancel.appmate.cloud")!,
urlScheme: "myapp"
)
)Start the cancel flow
RetentionFlow.startCancelFlow(
userId: currentUser.id,
attributes: ["plan": "monthly"]
) { link in
switch link.action {
case .openPremium(let paywallId):
navigateToPaywall(variant: paywallId)
case .openOffer(let offerId):
OfferRouter.present(offerId)
case .openSupport(let topic, let message):
openSupportInbox(topic: topic, prefilled: message)
case .openFeature(let id): openFeature(id)
case .returnToApp: break
case .manageSubscription:
Task { await RetentionFlow.presentManageSubscriptions() }
case .externalURL(let url):
UIApplication.shared.open(url)
case .none: break
}
}Coexisting with your existing deep links
AppMate URLs are namespaced as {yourscheme}://retention-flow/action?.... The SDK's parser returns nil for URLs that don't match, so it never claims someone else's URL. Chain it in front of your existing handler:
.onOpenURL { url in
if let link = RetentionFlow.deepLink(from: url) {
handleAppMate(link)
return // AppMate URL — done
}
handleMyExistingDeepLinks(url) // everything else
}manage_subscription doesn't even use your scheme — it routes to Apple's StoreKit sheet directly. Your URL handler never sees it.Onboarding funnel (web → app)
Recover the answers + email captured by a web onboarding funnel on first launch. See the onboarding guide for the full flow.
// Deferred handoff — call on first launch (paste banner shows):
Task {
if let result = await RetentionFlow.fetchOnboardingResult(userId: user?.id) {
if let goal = result.values(forStep: "goal").first { applyPreset(goal) }
if let email = result.email { prefillSignup(email: email) }
}
}
// Or run the funnel in-app for an installed user:
RetentionFlow.startOnboardingFlow(userId: user.id) { result in
guard let result else { return }
apply(result)
}Referral (share with a friend)
Install-attributed referrals — see the referral guide. Don't grant the referrer's reward on share; it's earned only when a friend installs.
// Share:
if let url = await RetentionFlow.referralShareLink(userId: user.id) {
presentShareSheet(items: [shareMessage, url])
}
// New user, first launch (paste banner shows):
if let attr = await RetentionFlow.attributeReferral(userId: user.id),
let reward = attr.refereeReward {
// unit is "week" (free weeks) or your custom currency ("drop", …)
grantReward(amount: reward.amount, unit: reward.unit)
}
// Referrer, every launch:
let earned = await RetentionFlow.claimReferralRewards(userId: user.id)
if earned.amount > 0 { grantReward(amount: earned.amount, unit: earned.unit) }Manage subscriptions helper
Wraps StoreKit 2's AppStore.showManageSubscriptions(in:) with an App Store URL fallback for Simulator and signed-out devices.
Task { await RetentionFlow.presentManageSubscriptions() }Feature wishlist (native board)
For an iOS app, present the wishlist natively— don't link the hosted web page. The native board renders in-app, dedupes votes/comments by your userId, and skips the Safari bounce. Easiest is one call:
// Present the board as a sheet from anywhere (e.g. a Settings row): RetentionFlow.presentWishlist(userId: currentUser?.id)
Prefer to place it in your own hierarchy? Drop in the WishlistView SwiftUI screen — it works in a sheet, pushed on a NavigationStack, or inside a tab:
import AppMate // In a tab, a navigation stack, or a sheet: WishlistView(userId: currentUser?.id) // Building a custom UI instead? Use the typed data API: let page = try await RetentionFlow.wishlistIdeas(sort: .votes) try await RetentionFlow.submitWishlistIdea(title: "Dark mode") try await RetentionFlow.voteWishlistIdea(ideaId: page.items[0].id)
The hosted page at appmate.cloud/wishlist/{slug} and the embed are for the web; reach for them on a website, not inside the app. See the feature wishlist guide for the full data API.
Shake for feedback
Let testers shake the device on any screen to open a bottom sheet of your flows — suggest a feature, report a bug, contact you, or whatever subset you choose. Configure it once at launch; the SDK detects the shake for you (no per-screen wiring), and shake-to-undo keeps working. You can also open the same menu from a button with RetentionFlow.presentMenu().
// After configure(_:), enable it once:
RetentionFlow.enableShakeMenu(
title: "Help us improve",
message: "Shake any time to reach us.",
userId: user?.id, // forwarded to whichever flow opens
items: [
.suggestFeature(), // native wishlist board
.reportBug(), // hosted report form
.contact(), // hosted contact form
// .feedback(), .cancelSubscription(),
// .custom(title: "Email us", systemImage: "envelope") { openMail() },
]
)
// Open it yourself too (e.g. a Help button):
RetentionFlow.presentMenu()Each item maps to one flow. suggestFeature opens the native wishlist board; the hosted forms (report, contact, feedback) open in a Safari sheet. Pass a flowSlug to target a non-primary flow, and override title/systemImage to rename or re-icon any row. Those web flows live on your apex host — the SDK derives it from baseURL (dropping a leading cancel.); set webBaseURL in the config if you use custom domains.
Soft-fail behaviour
If the AppMate server is unreachable, onAction is invoked with .manageSubscription so you can route the user straight to the App Store — they're never blocked from cancelling.
Action reference
.returnToApp— open your home screen..openPremium(paywallId:)— show your paywall;paywallIdmay be set to target a variant..openOffer(id:)— your app maps the id to a StoreKit promo, RevenueCat offering, or custom paywall..openSupport(topic:message:)— pre-fill your support sheet..openFeature(id:)— deep-link to a feature, tutorial, or onboarding screen..manageSubscription— present StoreKit 2 manage-subs..externalURL(URL)— open in user's browser..onboardingComplete(claimToken:)— a web-to-app onboarding funnel finished;startOnboardingFlowhandles it for you..none— future / unknown — handle defensively.
Full source + tests: github.com/fil-technology/appmate-ios.