Developer Docs / iOS SDK

GrowthCat iOS Referral Redemption SDK

GrowthCat is a lightweight Swift Package for referral-code redemption flows on top of RevenueCat. Use the built-inGrowthCatReferralViewfor the fastest path or build your own UI with the headless APIs.

Built For

In-app referral and promo-code redemption UX on iOS.

Includes

Validation, RevenueCat attribute syncing, Apple redemption handoff, and entitlement verification.

Not Included

Campaign management, referral creation, payouts, dashboards, or attribution reporting UI.

1. Overview

This SDK is focused on redemption-only flows. It validates referral and promo codes, sets the RevenueCat subscriber attribute$referrer_code, copies the validated code to pasteboard, opens Apple's native code redemption sheet, and verifies entitlement activation after the Apple flow returns.

Included

  • Validate entered referral and promo codes.
  • Set RevenueCat subscriber attribute `$referrer_code`.
  • Open Apple native code redemption sheet.
  • Verify entitlement activation after Apple flow.

Not included

  • Creating referral codes or campaigns.
  • Campaign lifecycle management or expiry controls.
  • Admin dashboards, payout logic, or attribution reporting UI.

No backend integration is required. Install the SDK, configure it in your app, and GrowthCat handles the validation flow for you.

What happens during redemption

  1. GrowthCat validates the code.
  2. GrowthCat sets the RevenueCat attribute `$referrer_code`.
  3. The validated code is copied to pasteboard automatically.
  4. Apple's native redemption sheet opens automatically.
  5. The SDK verifies entitlement activation when your app becomes active again.

2. Requirements

  • iOS 16+
  • RevenueCat configured by your app before any GrowthCat call
  • GrowthCat app SDK key (`gc_live_...`)
  • GrowthCat SDK installed and initialized in your app

To get started, integrate the SDK into your app and complete the client-side configuration. No backend work is required.

3. Production readiness checklist

  • RevenueCat SDK is configured before any GrowthCat calls.
  • GrowthCat is initialized once at app startup.
  • `verificationEntitlementID` matches your RevenueCat entitlement exactly.
  • `.growthCatOnSuccess` is the only unlock point for premium access.
  • `.growthCatOnError` is treated as pending and reconciled later.
  • Referral entry point exists in a persistent surface such as settings, paywall, or onboarding.

4. Workspace selection

The SDK chooses the workspace itself and sends it in the X-GrowthCat-Workspace header on every request.

  • By default, DEBUG builds use sandbox.
  • Release builds use live.
  • If you pass environmentMode: .production, the SDK always uses live, even in a DEBUG build.
  • The header value is always either sandbox or live.

5. Installation

Add the package in Xcode via Swift Package Manager:

https://github.com/lapreamarcelo/GrowthCatSDK-ios.git

Then import the module where you need it:

swift
import GrowthCat

6. Initialize SDK

Initialize GrowthCat once, usually in `App.init()` after RevenueCat is configured.

swift
public enum GrowthCatEnvironmentMode: Sendable {
    case automatic
    case production
}
swift
public static func initialize(
    apiKey: String,
    environmentMode: GrowthCatEnvironmentMode = .automatic,
    logsEnabled: Bool = false
)

There are two practical modes for initialization:

  • Use .automatic for the default behavior.
  • Set environmentMode: .production to always use the live workspace.

Default behavior:

swift
import SwiftUI
import GrowthCat

@main
struct DemoApp: App {
    init() {
        GrowthCat.initialize(
            apiKey: "gc_live_123456",
            environmentMode: .automatic,
            logsEnabled: true
        )
    }

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

In a DEBUG build this resolves to sandbox. In a release build this resolves to live.

Force production from a debug build:

swift
GrowthCat.initialize(
    apiKey: "gc_live_123456",
    environmentMode: .production,
    logsEnabled: true
)

There is no public force-sandbox override. That is intentional, because it is too easy to leave behind accidentally.

7. SDK config feature flags

GrowthCat stores the latest backend-provided SDK flags so your app can decide whether to show redeem UI or allow redeem actions. The SDK does not enforce these flags automatically.

swift
if let sdkConfig = GrowthCat.shared.sdkConfig {
    if sdkConfig.showCodeRedeemUI {
        // Show your GrowthCat entry point.
    }

    if sdkConfig.allowCodeRedeemExecution {
        // Let the user start the redeem flow.
    }
}

if let readiness = GrowthCat.shared.sdkReadiness {
    print("Can validate codes: \(readiness.canValidateCodes)")
}

If you want to refresh them manually, for example after login or app foreground:

swift
Task {
    do {
        let sdkConfig = try await GrowthCat.shared.refreshSDKConfig()
        print("Latest SDK config: \(sdkConfig)")
    } catch {
        print("Failed to refresh SDK config: \(error)")
    }
}

Use these as optional feature flags

  • `showCodeRedeemUI`: whether you want to present any redeem-code entry points.
  • `allowCodeRedeemExecution`: whether you want to allow the user to actually start redemption.
  • `sdkReadiness`: backend readiness status from the bootstrap call, useful for debug diagnostics.

8. Integration options

Recommended default

Start with the built-in view if you want RevenueCat-style integration semantics and the safest success/error callback model.

Option A: Built-in view + modifiers

Use GrowthCatReferralView and attach callback modifiers for success and pending/error handling.

swift
import SwiftUI
import GrowthCat

struct SettingsView: View {
    @State private var showReferral = false

    var body: some View {
        Button("Redeem Influencer Code") {
            showReferral = true
        }
        .sheet(isPresented: $showReferral) {
            GrowthCatReferralView(
                theme: .default,
                strings: .default,
                verificationEntitlementID: "pro",
                verificationTimeout: 20
            )
            .growthCatOnSuccess { result in
                print("Access confirmed for code: \(result.normalizedCode)")
                // Safe point to unlock premium features.
            }
            .growthCatOnError { error in
                print("Redemption not confirmed: \(error)")
                // Keep user in pending state and reconcile later.
            }
        }
    }
}
  • User enters a code and taps apply.
  • The SDK validates first, then copies the code and opens Apple's sheet.
  • The SDK shows a confirming-access loader while entitlement verification runs.
  • Invalid codes show a clear inline error and the Apple sheet does not open.

Callback guarantees

  • `growthCatOnSuccess`: code validated, Apple sheet shown, app returned to active state, and `verificationEntitlementID` is active in RevenueCat.
  • `growthCatOnError`: entitlement was not confirmed in this attempt. Common causes are user cancel, sync delay, network issues, or entitlement not active yet.

Treat `.growthCatOnSuccess` as the confirmed unlock point. Treat `.growthCatOnError` as pending and reconcile later on app active or next launch.

swift
GrowthCatReferralView(
    verificationEntitlementID: "pro",
    verificationTimeout: 20
)
.growthCatOnSuccess { _ in
    Task {
        await api.markReferralRedeemed()
        // unlockFeaturesForCurrentUser()
    }
}
.growthCatOnError { error in
    Task {
        await api.markReferralPendingVerification(reason: error.localizedDescription)
    }
}

Option B: Fully custom UI with `GrowthCatRedeemer`

Use your own views and bind to `GrowthCatRedeemer` when you want UI control without dropping to raw APIs.

swift
import SwiftUI
import GrowthCat

struct CustomReferralView: View {
    @State private var code = ""
    @StateObject private var redeemer = GrowthCatRedeemer()

    var body: some View {
        VStack(spacing: 12) {
            TextField("Your code", text: $code)
                .textFieldStyle(.roundedBorder)

            Button(redeemer.isLoading ? "Applying..." : "Apply") {
                Task { await redeemer.redeem(code) }
            }
            .disabled(redeemer.isLoading || code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)

            if let error = redeemer.lastError {
                Text("Error: \(error.localizedDescription)")
                    .font(.footnote)
            }

            if let result = redeemer.lastResult {
                Text("Applied: \(result.normalizedCode)")
                    .font(.footnote)
            }
        }
        .padding()
    }
}

A successful `redeem(...)` result here means the code validated and Apple's sheet was presented. It does not include the built-in view's post-Apple entitlement confirmation callbacks. Verify entitlement after the app returns before unlocking access.

Option C: Raw headless API

swift
let result = try await GrowthCat.shared.redeemReferralCode("ABC123")

let isVerified = await GrowthCat.shared.verifyAppleRedemption(
    entitlementID: "pro",
    timeout: 20
)

Use this path if you want full control over state handling, UI, and lifecycle coordination.

Verification recovery and retry

If the Apple sheet was shown but entitlement confirmation did not land yet, keep the user pending and retry on app active or next launch.

swift
@MainActor
func reconcileReferralStatus() async {
    let confirmed = await GrowthCat.shared.verifyAppleRedemption(
        entitlementID: "pro",
        timeout: 8
    )

    if confirmed {
        await api.markReferralRedeemed()
        // unlockFeaturesForCurrentUser()
    } else {
        await api.markReferralPendingVerification(reason: "Entitlement not active yet")
    }
}
swift
@MainActor
func refreshReferralRedemptionStatus() async {
    let status = await GrowthCat.shared.verifyAppleRedemption(
        entitlementID: "pro",
        timeout: 8
    )

    if status {
        await api.markReferralRedeemed()
    }
}

8. Built-in view customization

If you pass no theme, GrowthCat uses an adaptive default style for light and dark mode. Customize colors, font, and shape with `GrowthCatTheme`, and customize copy with `GrowthCatStrings`.

swift
let theme = GrowthCatTheme(
    primaryColor: .orange,
    backgroundColor: Color(.systemGroupedBackground),
    textColor: .primary,
    fontName: "AvenirNext-DemiBold",
    cornerRadius: 14
)
swift
let strings = GrowthCatStrings(
    title: "Have a Promo Code?",
    description: "Enter your influencer code to unlock your offer.",
    howItWorksTitle: "How it works",
    howItWorksStepValidate: "1. We validate your code instantly.",
    howItWorksStepCopy: "2. If valid, we copy it for you.",
    howItWorksStepApple: "3. Apple redemption opens so you can paste and continue.",
    placeholder: "Enter code",
    buttonText: "Apply Discount",
    successMessage: "Code validated. Opening Apple redemption...",
    invalidCodeMessage: "That code does not exist. Please try again.",
    networkErrorMessage: "Network error. Please try again."
)
swift
GrowthCatReferralView(theme: theme, strings: strings, verificationEntitlementID: "pro")

9. Reusable referral banners

Use `GrowthCatBannerView` to surface referral entry points in paywalls, settings, or onboarding flows.

swift
import SwiftUI
import GrowthCat

struct PaywallHeader: View {
    @State private var showReferralSheet = false

    var body: some View {
        GrowthCatBannerView(
            theme: GrowthCatBannerTheme(
                backgroundColor: Color(red: 0.93, green: 0.97, blue: 1.0),
                accentColor: .blue
            ),
            strings: GrowthCatBannerStrings(
                title: String(localized: "growthcat.banner.title"),
                subtitle: String(localized: "growthcat.banner.subtitle"),
                callToActionText: String(localized: "growthcat.banner.cta")
            ),
            configuration: GrowthCatBannerConfiguration(
                style: .soft,
                size: .regular,
                interaction: .fullBanner,
                cornerRadius: 16,
                iconSystemName: "gift.fill"
            )
        )
        .growthCatOnTap {
            showReferralSheet = true
        }
        .sheet(isPresented: $showReferralSheet) {
            GrowthCatReferralView(verificationEntitlementID: "pro")
        }
    }
}
  • `GrowthCatBannerTheme`: colors plus optional font.
  • `GrowthCatBannerStrings`: localized title, subtitle, and CTA text.
  • `GrowthCatBannerConfiguration.style`: `.solid`, `.soft`, `.outlined`.
  • `GrowthCatBannerConfiguration.size`: `.compact`, `.regular`, `.prominent`.
  • `GrowthCatBannerConfiguration.interaction`: `.fullBanner` or `.callToActionOnly`.

10. Localization guide

GrowthCat does not impose a localization system. Pass already-localized text into `GrowthCatStrings`, typically using `String(localized:)` and your app's String Catalog keys.

swift
let growthCatStrings = GrowthCatStrings(
    title: String(localized: "growthcat.title"),
    description: String(localized: "growthcat.description"),
    howItWorksTitle: String(localized: "growthcat.how_it_works_title"),
    howItWorksStepValidate: String(localized: "growthcat.how_it_works_validate"),
    howItWorksStepCopy: String(localized: "growthcat.how_it_works_copy"),
    howItWorksStepApple: String(localized: "growthcat.how_it_works_apple"),
    placeholder: String(localized: "growthcat.placeholder"),
    buttonText: String(localized: "growthcat.button_text"),
    successMessage: String(localized: "growthcat.success_message"),
    invalidCodeMessage: String(localized: "growthcat.invalid_code"),
    networkErrorMessage: String(localized: "growthcat.network_error")
)

GrowthCatReferralView(strings: growthCatStrings, verificationEntitlementID: "pro")

Recommended localization keys

  • `growthcat.title`
  • `growthcat.description`
  • `growthcat.how_it_works_title`
  • `growthcat.how_it_works_validate`
  • `growthcat.how_it_works_copy`
  • `growthcat.how_it_works_apple`
  • `growthcat.placeholder`
  • `growthcat.button_text`
  • `growthcat.success_message`
  • `growthcat.invalid_code`
  • `growthcat.network_error`
  • `growthcat.banner.title`
  • `growthcat.banner.subtitle`
  • `growthcat.banner.cta`
  1. Add the new language in your String Catalog or localized strings files.
  2. Translate your `growthcat.*` keys.
  3. Build `GrowthCatStrings` from localized keys.
  4. Pass those strings into `GrowthCatReferralView`.

11. Error handling

`redeemReferralCode(_:)` throws `GrowthCatError`.

swift
- notInitialized
- invalidCode(message:)
- unauthorized(message:)
- rateLimited(retryAfter:)
- network(message:)
- server(statusCode:message:)
- revenueCatUnavailable(message:)
- unknown(message:)

Best practices

  • Initialize GrowthCat once at startup.
  • Configure RevenueCat before redemption calls.
  • Keep RevenueCat identity stable before starting the referral flow.
  • Pass `verificationEntitlementID` to verify redemption outcome.
  • Use `.growthCatOnSuccess` as confirmed redemption.
  • Keep referral entry points visible in durable parts of the app.

12. Minimal API reference

swift
public enum GrowthCat {
    public static func initialize(
        apiKey: String,
        environmentMode: GrowthCatEnvironmentMode = .automatic,
        logsEnabled: Bool = false
    )
    public static var shared: GrowthCatClient { get }
}

public protocol GrowthCatRedeeming {
    var sdkConfig: SDKConfig? { get }
    var sdkReadiness: SDKReadiness? { get }
    func redeemReferralCode(_ code: String) async throws -> ReferralRedemptionResult
    func validateReferralCode(_ code: String) async throws -> ReferralRedemptionResult
    func refreshSDKConfig() async throws -> SDKConfig
    func openAppleRedemptionSheet() async throws
    func verifyAppleRedemption(entitlementID: String, timeout: TimeInterval) async -> Bool
}

@MainActor
public final class GrowthCatRedeemer: ObservableObject {
    @Published public private(set) var isLoading: Bool
    @Published public private(set) var lastResult: ReferralRedemptionResult?
    @Published public private(set) var lastError: GrowthCatError?
    public func redeem(_ code: String) async -> Result<ReferralRedemptionResult, GrowthCatError>
}