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:

text
https://github.com/fil-technology/appmate-ios

Or in Package.swift:

swift
.package(url: "https://github.com/fil-technology/appmate-ios", from: "0.2.0")

Configure

Once at launch:

swift
import AppMate

RetentionFlow.configure(
    .init(
        appSlug: "my-ios-app",
        baseURL: URL(string: "https://cancel.appmate.cloud")!,
        urlScheme: "myapp"
    )
)

Start the cancel flow

swift
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:

swift
.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.

swift
// 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.

swift
// 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.

swift
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:

swift
// 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:

swift
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().

swift
// 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; paywallId may 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; startOnboardingFlow handles it for you.
  • .none — future / unknown — handle defensively.

Full source + tests: github.com/fil-technology/appmate-ios.