Docs

Native mobile SDKs

AdminUpdated Sep 15, 2026

Native mobile SDKs

Cookie Munch ships first-class native clients for iOS/macOS (Swift), Android (Kotlin), and Flutter (Dart) — plus a React Native SDK that all three mirror. They share one design, byte-for-byte compatible with the web embed and each other:

  • The same four categories: necessary (always granted) + preferences, statistics, marketing.

  • An implied default until the user makes an explicit choice, after which the record flips to explicit — same two-value method field as the web engine.

  • A pluggable storage backend, defaulting to in-memory, with a durable option per platform (Keychain on iOS, EncryptedSharedPreferences on Android, a plain file on Flutter).

  • Offline-safe sync: every decision is persisted locally first, then POSTed to your self-hosted server's public ingest endpoint, POST /api/v1/consent, with the region as an X-CookieMunch-Region header. A failed or offline POST never throws back into your app and never loses the local decision — it's simply not retried until the next explicit action (there's no background retry queue).

  • A gate helper on every platform — the native equivalent of the web embed's prior-blocking: hand it a category and a closure/block, and it runs the block exactly once, either immediately (if already granted) or the instant the category is granted. Use it to defer initializing anything that must not run pre-consent: analytics SDKs, ad SDKs, crash reporters that capture PII.

  • A drop-in banner widget (SwiftUI ConsentBannerView, Jetpack Compose ConsentBanner, Flutter ConsentBanner) that renders only until the user has responded, wired directly to the client. All three are optional — the core client has no UI dependency, so you can build your own banner against the same API.

All three SDKs are built and unit-tested in CI (swift build/swift test for iOS, Gradle for Android, flutter test for Flutter) and all three expose a real, usable public API — none of them is CI-only scaffolding.

iOS / macOS (Swift)

Package: CookieMunch (formerly published as ForgeConsent), source under native/ios/. Targets iOS 15+, macOS 12+, tvOS 15+, watchOS 8+.

Install (Swift Package Manager)

In Xcode: File → Add Package Dependencies…, or add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/your-org/cookiemunch", from: "0.1.0")
],
targets: [
    .target(name: "App", dependencies: [
        .product(name: "CookieMunch", package: "cookiemunch")
    ])
]

Initialize

import CookieMunch

let consent = CookieMunchConsent(
    cbid: "your-site-id",
    apiURL: "https://cmp.example.com",
    storage: KeychainConsentStorage(),   // durable across launches
    region: "EU"                         // sent as X-CookieMunch-Region
)

// Restore any prior decision at launch.
await consent.load()

CookieMunchConsent is @MainActor and conforms to ObservableObject, so it drops straight into SwiftUI — observe $state, or subscribe imperatively with onChange.

Reading and recording consent

await consent.accept()                                     // grant all
await consent.decline()                                    // deny all non-necessary
await consent.set(statistics: true)                         // merge one category
await consent.submit(Choices(preferences: true,
                             statistics: false,
                             marketing: true))               // fully custom

consent.getState()          // current ConsentState
consent.hasResponse         // true once an explicit decision exists
consent.granted(.marketing) // true/false for a given ConsentCategory

let cancel = consent.onChange { state in
    print("marketing granted:", state.marketing)
}
// later…
cancel()

ConsentCategory is .necessary | .preferences | .statistics | .marketing. Choices carries the three user-controllable fields (preferences, statistics, marketing); necessary is implicit and never part of the wire payload.

Gating SDK initialization

// Runs now if statistics is already granted, otherwise the instant it is.
consent.gate(.statistics) {
    Analytics.start()
}

consent.gate(.marketing) {
    AdSDK.initialize()
}

Each gated closure fires at most once; .necessary is always granted so gate(.necessary) runs immediately.

SwiftUI banner

struct RootView: View {
    @StateObject private var consent = CookieMunchConsent(
        cbid: "your-site-id",
        apiURL: "https://cmp.example.com",
        storage: KeychainConsentStorage()
    )

    var body: some View {
        HomeView()
            .overlay(alignment: .bottom) {
                ConsentBannerView(consent: consent)
            }
            .task { await consent.load() }
    }
}

Storage backends

Backend

Use

InMemoryConsentStorage()

default; volatile — tests and previews

UserDefaultsConsentStorage(suiteName:)

conventional, non-sensitive persistence

KeychainConsentStorage(service:accessGroup:)

secure, survives reinstalls; not synced to iCloud Keychain; pass accessGroup to share across an app group

Implement ConsentStorage yourself for anything else; the network layer is similarly injectable via ConsentTransport for unit tests.

App Tracking Transparency

When marketing becomes granted, the client automatically calls ATTrackingManager.requestTrackingAuthorization, keeping the OS prompt consistent with the consent banner. Add NSUserTrackingUsageDescription to your Info.plist.

Android (Kotlin)

Module: net.cookiemunch:cookiemunch, source under native/android/cookiemunch/. Requires minSdk 24, compileSdk 34, Kotlin 1.9.22, JDK 17, Jetpack Compose.

Install (Gradle, as a project module)

// settings.gradle.kts
include(":cookiemunch")

// app/build.gradle.kts
dependencies {
    implementation(project(":cookiemunch"))
}

Initialize

The secure() factory builds an EncryptedSharedPreferences-backed client and loads any persisted record immediately:

import net.cookiemunch.Category
import net.cookiemunch.Choices
import net.cookiemunch.CookieMunchConsent

val consent = CookieMunchConsent.secure(
    context = applicationContext,
    cbid = "your-site-cbid",
    apiUrl = "https://cmp.example.com",
    region = "EU", // sent as X-CookieMunch-Region
)

Or construct CookieMunchConsent(...) directly for an in-memory client (the default storage), and call .load() yourself.

Reading and recording consent

All decision-recording calls are suspend — call them from a coroutine:

lifecycleScope.launch { consent.accept() }
lifecycleScope.launch { consent.decline() }
lifecycleScope.launch {
    consent.submitCustom(Choices(preferences = true, statistics = false, marketing = true))
}
lifecycleScope.launch { consent.set(Category.STATISTICS, true) }

val hasResponded = consent.hasResponse()
consent.onChange { state -> /* react to changes */ }
// or collect the StateFlow directly:
val state by consent.state.collectAsState() // in a @Composable

Category is NECESSARY | PREFERENCES | STATISTICS | MARKETING; Choices carries preferences/statistics/marketing (necessary is implicit).

Gating SDK initialization

gate returns the block's result, or null when the category isn't granted:

consent.gate(Category.STATISTICS) {
    analytics.start()
}

val id = consent.gate(Category.MARKETING) { adSdk.deviceId() } // null if not granted

Compose banner

setContent {
    Column {
        // ... your app ...
        ConsentBanner(consent) // renders only until the visitor responds
    }
}

Storage

Backend

Notes

InMemoryConsentStorage

default; nothing hits disk

EncryptedPrefsConsentStorage

EncryptedSharedPreferences (androidx.security:security-crypto:1.1.0-alpha06), AES-256 SIV keys / AES-256 GCM values, Keystore-backed master key via MasterKey.Builder

Implement ConsentStorage yourself for DataStore or anything else. The sync POST runs on Dispatchers.IO; inject a custom ConsentTransport to swap the HTTP layer (default uses HttpURLConnection, no extra dependencies).

Flutter (Dart)

Package: cookie_munch, source under native/flutter/. The consent core is pure Dart (dart:* + package:http only, no platform channels), so it runs identically on Android, iOS, macOS, Windows, and Linux — desktop is a first-class target, not an afterthought.

Install

# pubspec.yaml
dependencies:
  cookie_munch:
    path: ../native/flutter   # or your package path / git ref

Then flutter pub get.

Initialize

Configure the app-wide shared instance once at startup:

import 'package:flutter/material.dart';
import 'package:cookie_munch/cookie_munch.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await CookieMunchConsent.configure(
    cbid: 'your-site-id',
    apiUrl: 'https://your-server.com',
    region: 'gb',
    // Default storage is in-memory. For durability across restarts:
    // storage: FileConsentStorage('/absolute/path/consent.json'),
  );
  runApp(const MyApp());
}

CookieMunchConsent.configure() both constructs and load()s the client; access it anywhere afterward via CookieMunchConsent.instance (throws if called before configure). You can also construct CookieMunchConsent(...) directly instead of using the shared instance, e.g. for dependency injection or tests.

Reading and recording consent

final c = CookieMunchConsent.instance;

if (c.allows(ConsentCategory.marketing)) { /* enable marketing SDKs */ }

await c.acceptAll();   // alias: c.accept()
await c.rejectAll();   // alias: c.decline()
await c.submitCustom(preferences: true, statistics: true, marketing: false);
await c.set(ConsentCategory.statistics, true); // toggle one category

final off = c.onChange((state) => print('marketing=${state.marketing}'));
// ...later: off();
// or a stream:
c.changes.listen((state) { /* ... */ });

ConsentCategory is necessary | preferences | statistics | marketing; Choices carries preferences/statistics/marketing (Choices.all / Choices.none are convenience constants).

Gating SDK initialization

// Analytics init runs only after 'statistics' consent — immediately or later.
CookieMunchConsent.instance.gate(ConsentCategory.statistics, () {
  MyAnalytics.start();
});

// Returns a cancel fn if you need to drop a still-pending gate.
final cancel = c.gate(ConsentCategory.marketing, initAdSdk);

Banner widget

MaterialApp(
  home: Scaffold(
    body: Stack(
      children: const [
        // ... your app ...
        Align(alignment: Alignment.bottomCenter, child: ConsentBanner()),
      ],
    ),
  ),
);

ConsentBanner themes itself from your Theme and renders only while !consent.hasResponse; pass consent: explicitly to bind it to a non-shared client, or override title/message/acceptLabel/rejectLabel/primaryColor.

Storage

Storage

Persistence

Platforms

InMemoryConsentStorage (default)

none

all, including web

FileConsentStorage(path)

file on disk

mobile + desktop

your own ConsentStorage

anything

your choice

For mobile-conventional persistence via shared_preferences, implement ConsentStorage in your own app (the core package never imports it, so it never leaks a plugin dependency into desktop builds — see the package README for a short adapter example).

Flutter-free core

For services, isolates, or plain dart test, import the core without any Flutter dependency:

import 'package:cookie_munch/cookie_munch_core.dart';

iOS App Tracking Transparency (from Flutter)

Add the app_tracking_transparency package and NSUserTrackingUsageDescription, then gate the OS prompt on marketing the same way the native iOS SDK does internally:

CookieMunchConsent.instance.gate(ConsentCategory.marketing, () {
  AppTrackingTransparency.requestTrackingAuthorization();
});

Wire format and consistency

Every platform posts the same shape to POST /api/v1/consent:

{
  "cbid": "your-site-id",
  "stamp": "a v4 UUID identifying this decision",
  "choices": { "preferences": false, "statistics": true, "marketing": false },
  "method": "explicit",
  "ver": 1,
  "utc": 1234567890123,
  "url": "app://your-site-id"
}

with the region as an X-CookieMunch-Region header — the same public ingest endpoint the web embed and the server-side SDKs use (see JavaScript API and Install & initialize). Since the record shape and category names are identical everywhere, a decision made on one surface reads consistently in your dashboard regardless of which SDK produced it.

Was this page helpful?