Skip to content

About

Public Swift Package that wraps our iOS Framework

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Repository files navigation

Kovalee-SDK

KovaleeSDK is an efficient iOS framework, that's packed with tools specifically for tracking user behavior, handling user purchases, and smoothly integrating ads.

We all know that integrating tracking tools can feel like solving a Rubik's cube. The hurdles of Apple's restrictions and the hoops we jump through with third-party frameworks make the process complex and time-consuming.

We've tackled this issue head-on at Kovalee. KovaleeSDK is our answer - a framework designed to handle the heavy lifting, automating many of the typically tedious steps and wrapping much of the tracking functionality into the SDK.

KovaleeSDK is split into multiple modules, distributed through SPM, each one taking care of a different area.

  • KovaleeAttribution
  • KovaleePurchases
  • KovaleeRemoteConfig
  • KovaleeTikTok

Installation

📦 Swift Package

The package can be simply installed as a dependency from XCode.

  • Add a package by selecting File → Add Packages… in Xcode’s menu bar.
  • Search for the Kovalee iOS SDK using the repo's URL:
    https://github.com/cotyapps/Kovalee-iOS-SDK
  • Set the Dependency Rule to be Up to Next Major Version.
  • Select Add Package.

And you are good to go.

Requirements

Platform Minimum target
iOS 15.0+

Integration

To properly work, this Package needs a set of configurations files that should be created by the team @Kovalee.

If you are interested in using this package for your iOS app, feel free to get in contact.

Configuration

Initialize the SDK in your AppDelegate or app entry point. Use the .development environment during development for debug logging, and .production for release builds:

UIKit (AppDelegate):

import KovaleeSDK

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _: UIApplication,
        willFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        #if DEBUG
        let configuration = Configuration(environment: .development, logLevel: .debug)
        #else
        let configuration = Configuration(environment: .production)
        #endif

        Kovalee.initialize(configuration: configuration)
        return true
    }
}

SwiftUI:

import SwiftUI
import KovaleeSDK

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

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

Amplitude Session Replay

KovaleeSDK supports Amplitude Session Replay, which lets you capture and replay user sessions to better understand user behavior in your app.

Enabling Session Replay

To enable Amplitude Session Replay, set enableAmplitudeSessionReplay to true in your Configuration:

Kovalee.initialize(
    configuration: Configuration(
        environment: .production,
        enableAmplitudeSessionReplay: true
    )
)

Session Replay is disabled by default. When enabled, the SDK automatically adds the AmplitudeSwiftSessionReplayPlugin to the Amplitude instance, and session replays will be available in your Amplitude dashboard.

TikTok Integration

KovaleeSDK supports TikTok Events API through the KovaleeTikTok module, allowing you to track app events and send them to TikTok for ad attribution and optimization.

Setup

  1. Add the KovaleeTikTok library to your app target in Xcode:

    • Select your app target → General → Frameworks, Libraries, and Embedded Content → + → select KovaleeTikTok
  2. Add your TikTok credentials to KovaleeKeys.json:

    {
      "tiktok": {
        "appId": "YOUR_APP_ID",
        "tiktokAppId": "YOUR_TIKTOK_APP_ID",
        "accessToken": "YOUR_ACCESS_TOKEN"
      }
    }
  3. Add import KovaleeTikTok alongside KovaleeSDK where you initialize the SDK:

    import KovaleeSDK
    import KovaleeTikTok

TikTok will be automatically initialized during Kovalee.initialize() when the keys are present. No additional setup call is needed.

If you don't have TikTok integration keys, reach out to the Kovalee team to get them configured for your app.

Usage

// Track an event
Kovalee.trackTikTokEvent("purchase", properties: ["value": 9.99, "currency": "USD"])

// Identify a user
Kovalee.identifyTikTokUser(externalId: "user_123", email: "user@example.com")

// Logout
Kovalee.logoutTikTok()

// Enable/disable tracking
Kovalee.setTikTokTrackingEnabled(false)

// Flush queued events
Kovalee.flushTikTokEvents()

RevenueCat Paywalls

KovaleeSDKUI presents RevenueCat-hosted (remote) paywalls with all Kovalee tracking wired automatically — page_view_paywall, the full payment_* lifecycle (including restore failure), and the value-carrying ad-SDK conversions:

import KovaleeSDKUI
import RevenueCat

MainView()
    .kovaleePaywall(
        isPresented: $showPaywall,      // gate on !isUserPremium at the call site
        offering: offering,              // nil → the RC "current" offering
        source: "progress"
    ) { customerInfo in
        isUserPremium = true
    } onDismiss: { unlocked in           // purchased OR restored an entitlement
        if unlocked { isUserPremium = true }
    }

The paywall renders the template configured for the offering in the RevenueCat dashboard, so design changes ship without app updates. Prefer this over hand-wiring RevenueCatUI's presentPaywallIfNeeded — hand-rolled callbacks historically missed the ad-SDK conversions entirely.

Full guide — API, restore semantics, migration from hand-rolled paywalls: Documentation/PAYWALLS.md.

Purchase Conversion Tracking

Every completed subscription purchase can fire value-carrying conversion events to TikTok (StartTrial/Subscribe) and Firebase (GA4 purchase, real charges only) — in one consent-gated dispatch, fired exactly once per purchase.

A purchase must reach the dispatch through exactly one of:

  1. Native purchases — Kovalee.purchase(...) / Kovalee.purchaseSubscription(...) fire it automatically.
  2. RevenueCat remote paywalls — present them with the .kovaleePaywall(isPresented:offering:source:) modifier (KovaleeSDKUI); it wires page_view_paywall, the whole payment_* lifecycle, and the conversions automatically.
  3. Custom RevenueCatUI wiring — capture the package in purchaseStarted, then call Kovalee.trackSubscriptionConversion(package:transaction:) in purchaseCompleted.

The Firebase purchase event requires "analyticsEnabled": true in the firebase block of KovaleeKeys.json.

Full guide — events, per-sink requirements, pitfalls, and how to verify an integration: Documentation/PURCHASE_TRACKING.md.

Google On-Device Conversion Measurement (ODM)

Since 2.6.5, KovaleeAttribution includes Adjust's Google ODM module. To turn it on in your app:

  1. Update the SDK to 2.6.5 or newer. If your app pins firebase-ios-sdk to 12.x, allow 13.x.
  2. In Info.plist, never set GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE to false.
  3. Ask for tracking with Kovalee.promptTrackingAuthorization().

To check it works: after a fresh install, adj_google_odm_info_processed is 1 in the app's UserDefaults.

Subscription Upsell

KovaleeSDKUI ships a turnkey subscription-to-lifetime flow: detect an eligible subscription — a trial about to expire, or an established subscriber active past a threshold (see Eligibility conditions) → present a RevenueCat-hosted upsell paywall (typically lifetime) → on purchase, route the user to Apple's Manage Subscriptions sheet so they can turn off auto-renewal on the original subscription (Apple does not allow developer-side cancellation).

The host app never sees subscription state, paywall internals, or the manage-subscriptions sheet — it provides configuration and (optionally) receives the final outcome.

Setup

  1. Add the KovaleeSDKUI library to your app target. It depends on RevenueCatUI, which is fetched automatically.

  2. Build a Configuration:

    import KovaleeSDKUI
    
    let upsellConfig = SubscriptionUpsell.Configuration(
        offeringId: "lifetime_upsell",          // RevenueCat offering to present
        trigger: .yearly,                        // watch for an expiring yearly trial
        triggerWithin: 48 * 3600,                // …expiring within 48h (default: 48h, optional)
        storageKey: "trial_to_lifetime_v1",      // namespace for the show-once flag
        condition: nil,                          // see Eligibility conditions (default: .trialExpiring(within: triggerWithin))
        showCloseButton: false,                  // overlay an X if your paywall has no dismiss UI (default: false)
        cancelPromptStyle: nil                   // per-flow override; nil = shared KovaleeUI.configuration.style (default: nil)
    )

    offeringId, trigger, and storageKey are required; everything else has a default. trigger can be .yearly, .monthly, .weekly, .anySubscription, or .productIdentifiers([...]) for explicit product IDs.

Eligibility conditions

What makes a matching subscription eligible is controlled by the optional condition parameter:

Condition Fires when
.trialExpiring(within:) An active trial entitlement expires within the given window. This is the default — omitting condition is equivalent to .trialExpiring(within: triggerWithin).
.activeLongerThan(_:) An active, non-trial subscription was first purchased more than the given interval ago. Use it to re-engage established subscribers — e.g. upselling a long-time yearly subscriber to lifetime.
// Re-engage subscribers who have been paying yearly for 90+ days
let upsellConfig = SubscriptionUpsell.Configuration(
    offeringId: "lifetime_upsell",
    trigger: .yearly,
    storageKey: "established_to_lifetime_v1",
    condition: .activeLongerThan(90 * 24 * 3600)
)

In both cases trigger scopes which products are considered.

Usage

SwiftUI — drop the modifier on your root view. It runs the check on appear and on every foreground; the show-once flag (keyed by storageKey) keeps it idempotent.

ContentView()
    .checkSubscriptionUpsell(configuration: upsellConfig) { outcome in
        // .notTriggered | .dismissed | .purchased
    }

UIKit — call from the view controller that should host the paywall:

SubscriptionUpsell.checkAndPresentIfNeeded(
    configuration: upsellConfig,
    from: self
) { outcome in
    // …
}

Deep link / marketing re-engagement — bypass the eligibility-condition and show-once gates and present unconditionally. The built-in handler matches <scheme>://upsell (custom schemes only — http/https are refused):

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let url = URLContexts.first?.url,
          let windowScene = scene as? UIWindowScene,
          let root = windowScene.keyWindow?.rootViewController else { return }

    if SubscriptionUpsell.handleDeepLink(url, configuration: upsellConfig, from: root) {
        return
    }
    // …your other deep-link handlers
}

Add an ?offering=<id> query item to target a specific paywall, overriding configuration.offeringId for that link only — e.g. <scheme>://upsell?offering=Default_2 to email a harder-discount offering. Every other configuration field is preserved; without the param the configured offering is used.

Or present directly:

SubscriptionUpsell.presentNow(configuration: upsellConfig, from: presenter)

Styling the congrats screen

The post-purchase "Lifetime unlocked" / cancel-prompt screens derive their look from the shared KovaleeUI.configuration.style — the same KovaleeUIStyle the feedback sheets use — so they match your app out of the box. On top of the shared colors, KovaleeUIStyle carries upsell-only fonts and the two hero SF Symbols:

KovaleeUI.configuration.style = KovaleeUIStyle(
    backgroundColor: .black,
    primaryColor: .white,                    // title
    secondaryColor: .white.opacity(0.8),     // body
    ctaColor: .yellow,                        // hero icon + primary button
    confirmationIcon: "crown.fill",           // "Lifetime unlocked" hero icon
    cancelPromptIcon: "bell.slash.fill"       // "One last step" hero icon
)

The primary button's text color is derived automatically for contrast against ctaColor. Per-flow overrides take precedence via Configuration.cancelPromptStyle.

Debug / QA

The SDK's DebugView exposes a Subscription Upsell section (visible when Enable Debug Mode is on) with:

  • Force Trigger on Launch — behaves as if a matching trial entitlement existed, regardless of subscription state. Gated to debug/TestFlight builds.
  • Reset Shown State — clears the show-once flag so the flow can fire again.
  • Preview Congrats Screen — opens the post-purchase screen directly to validate theming.
  • Present Paywall — lists every RevenueCat offering (the active one marked (current)) and force-presents its upsell paywall immediately, bypassing eligibility and the show-once gate. Always shows a close button so the paywall is dismissable regardless of the offering's template.

Outcomes

Outcome Meaning
.notTriggered No entitlement met the eligibility condition (no expiring trial / subscription not active long enough), the flow already ran for this storageKey, or the offering / network lookup failed.
.dismissed The user closed the paywall without purchasing.
.purchased The upsell was purchased. Fires after the congrats screen is dismissed.

In-App Feedback

KovaleeSDKUI ships two ready-made feedback flows (iOS 17+). Metadata (OS/app version, RevenueCat & Amplitude ids, subscription status) is filled automatically from the SDK.

Flow What it collects Backend (Firebase callable)
Founder Free-form message + email + phone writeToSheet
Features Multi-choice survey → optional notes → confirmation sendForm
import KovaleeSDKUI

Configuration

All feedback content, styling, and choices are configured once through KovaleeUI.configuration — typically in App.init or AppDelegate.didFinishLaunching. This is the single entry point; the call sites stay configuration-free.

KovaleeUI.configuration.appIcon         = Image("AppIcon")
KovaleeUI.configuration.feedbackChoices = ["Daily journal", "Streaks", "Community challenges", "Other"]
KovaleeUI.configuration.style           = .myAppStyle      // shared by feedback + upsell

KovaleeUI.configuration.founderFeedbackText = FeedbackText(
    cta: "Send feedback",
    title: "Hey, I'm the founder.",
    introText: "Tell me what to build next.",
    imageName: "founder-photo",   // an asset in YOUR app bundle
    successText: "Thanks!"
)

KovaleeUI.configuration.featureFeedbackText = FeatureFeedbackText(
    choicesTitle: "What should we build next?",
    choicesSubtitle: "Pick the features you'd use most.",
    notesTitle: "Anything else?",
    notesSubtitle: "Tell us what would make the app better.",
    notesPlaceholder: "Type here…",
    confirmationTitle: "Feedback received",
    confirmationMessage: "Thanks for helping shape the app."
)

featureFeedbackText is required before presenting the features survey — showFeatures() triggers an assertion failure in debug builds (and is a no-op in release) if it hasn't been set.

Usage

Hold a FeedbackCoordinator, attach .userFeedback(coordinator:), and call showFounder() / showFeatures(). The call sites take no content — everything is read from KovaleeUI.configuration:

struct HomeView: View {
    @State private var feedback = FeedbackCoordinator()

    var body: some View {
        content
            .userFeedback(coordinator: feedback)
    }

    func contactSupport() {
        feedback.showFounder()
    }

    func askWhatsNext() {
        feedback.showFeatures(
            onChoicesButtonTapped: { /* analytics */ },
            onNotesActionTapped: { /* analytics */ }
        )
    }
}

Styling

The feedback sheets and the subscription-upsell post-purchase screens share a single KovaleeUIStyle. All fields are defaulted, so .default works out of the box — set KovaleeUI.configuration.style to theme them:

extension KovaleeUIStyle {
    static let myAppStyle = KovaleeUIStyle(
        backgroundColor: Color("background"),
        primaryColor: .white,                      // titles
        secondaryColor: .white.opacity(0.8),       // subtitle / body text
        secondaryBackgroundColor: Color("card"),   // fields & unselected chips
        ctaColor: Color("accent"),                 // CTA + selected chip
        selectedColor: .white,                     // selected chip label
        unselectedColor: .white,                   // unselected chip label
        buttonCornerRadius: 16,
        symbol: nil                                // optional SF Symbol on the founder CTA
    )
}

Debug / QA

The SDK's DebugView exposes an In-App Feedback section (visible when Enable Debug Mode is on, iOS 17+) with Preview Founder Feedback and Preview Feature Survey buttons that present each flow using the current KovaleeUI.configuration.

See Documentation/FEEDBACK.md for the full reference, the UIKit examples, and styling details.

About

Public Swift Package that wraps our iOS Framework

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages