Native mobile SDKs
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
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-valuemethodfield 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 anX-CookieMunch-Regionheader. 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
gatehelper 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 ComposeConsentBanner, FlutterConsentBanner) 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 |
|---|---|
| default; volatile — tests and previews |
| conventional, non-sensitive persistence |
| secure, survives reinstalls; not synced to iCloud Keychain; pass |
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 @ComposableCategory 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 grantedCompose banner
setContent {
Column {
// ... your app ...
ConsentBanner(consent) // renders only until the visitor responds
}
}Storage
Backend | Notes |
|---|---|
| default; nothing hits disk |
| EncryptedSharedPreferences ( |
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 refThen 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 |
|---|---|---|
| none | all, including web |
| file on disk | mobile + desktop |
your own | 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.