Open-source Consent Management Platform SDK for Android & iOS
Quick Start · Configuration · API · WebView Bridge · Docs
- Two native SDKs — Kotlin + Compose (Android), Swift + SwiftUI (iOS)
- 4 layout options — Banner, Popup, Fullscreen, Bottom Sheet
- Fully customizable — theme, categories, labels, button order
- WebView bridge —
__duckcmpJS interface for hybrid apps - Zero dependencies — no third-party libraries beyond platform frameworks
// 1. Add GitHub Packages repo (settings.gradle.kts)
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://maven.pkg.github.com/analytics-debugger/duckcmp")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
}
// app/build.gradle.kts
dependencies {
implementation("com.analytics.debugger:duck-cmp:0.0.1-alpha")
}// 2. Initialize
DuckCMP.initialize(context)
// 3. Show dialog
if (DuckCMP.shouldShowDialog()) {
DuckCMP.showConsentDialog(activity)
}
// 4. Check consent
val hasAnalytics = DuckCMP.hasConsent(2) // category ID 2 = Analytics
val status = DuckCMP.getConsentStatus() // UNKNOWN, ACCEPTED, REJECTED, PARTIAL// 1. Add via Swift Package Manager
// Xcode: File > Add Package Dependencies
// 2. Initialize
import DuckCMP
DuckCMP.initialize()
// 3. Show dialog (SwiftUI)
.fullScreenCover(isPresented: $showConsent) {
ConsentDialogView {
showConsent = false
}
}
// 4. Check consent
let hasAnalytics = DuckCMP.hasConsent(categoryId: 2)
let status = DuckCMP.getConsentStatus()| ID | Category | Description | Required |
|---|---|---|---|
| 1 | Necessary | Essential for the app to function | Yes (always on) |
| 2 | Analytics | Help us understand how you use the app | No |
| 3 | Marketing | Used to deliver personalized ads | No |
| 4 | Preferences | Remember your settings and choices | No |
All configuration is done through ConsentConfig. Every property has a sensible default.
DuckCMP.initialize(context, ConsentConfig(
// Labels
dialogTitle = "Your Privacy Matters",
dialogMessage = "Choose which data categories you allow.",
acceptAllLabel = "Allow All",
rejectAllLabel = "Deny All",
saveChoicesLabel = "Confirm My Choices",
manageLabel = "Manage Preferences",
// Behavior
requireConsent = true, // must choose before dismissing
showCategoryToggles = true,
// Layout: BANNER, POPUP, FULLSCREEN, BOTTOM_SHEET
layout = ConsentLayout.POPUP,
// Button order (omit to hide)
buttons = listOf(
ConsentButton.SAVE_CHOICES,
ConsentButton.ACCEPT_ALL,
ConsentButton.REJECT_ALL
),
// Theme
theme = ConsentTheme(
primaryColor = 0xFF6200EE,
backgroundColor = 0xFFFFFFFF,
textColor = 0xFF333333,
titleColor = 0xFF111111,
buttonTextColor = 0xFFFFFFFF,
secondaryButtonTextColor = 0xFF6200EE,
cornerRadius = 16f,
overlayColor = 0x99000000
),
// Custom categories
categories = listOf(
ConsentCategory(id = 1, name = "Essential", description = "...", isRequired = true),
ConsentCategory(id = 2, name = "Performance", description = "..."),
ConsentCategory(id = 3, name = "Targeting", description = "..."),
ConsentCategory(id = 4, name = "Social", description = "...")
)
))| Property | Type | Default | Description |
|---|---|---|---|
categories |
List<ConsentCategory> |
4 defaults | Consent categories shown to the user |
dialogTitle |
String |
"Privacy Settings" | Heading text |
dialogMessage |
String |
"We use cookies..." | Body text |
acceptAllLabel |
String |
"Accept All" | Accept button text |
rejectAllLabel |
String |
"Reject All" | Reject button text |
saveChoicesLabel |
String |
"Save Choices" | Save toggles button text |
manageLabel |
String |
"Manage Preferences" | Expand toggles link text |
showCategoryToggles |
Boolean |
true |
Show per-category toggles |
requireConsent |
Boolean |
false |
Block dismiss without choosing |
buttons |
List<ConsentButton> |
All 4 | Which buttons, in what order |
layout |
ConsentLayout |
POPUP |
Dialog presentation style |
theme |
ConsentTheme |
Default | Visual customization |
| Property | Type | Description |
|---|---|---|
id |
Int |
Unique identifier for hasConsent(id) |
name |
String |
Display name in the dialog |
description |
String |
Explanation shown to the user |
isRequired |
Boolean |
If true, toggle is always on and disabled |
| Property | Default | Description |
|---|---|---|
primaryColor |
0xFF1976D2 |
Buttons, toggles, active elements |
backgroundColor |
0xFFFFFFFF |
Dialog background |
textColor |
0xFF212121 |
Body text |
titleColor |
0xFF212121 |
Heading text |
buttonTextColor |
0xFFFFFFFF |
Primary button label |
secondaryButtonTextColor |
0xFF1976D2 |
Outlined button text |
cornerRadius |
12f |
Corner radius (dp) |
overlayColor |
0x80000000 |
Background dim overlay |
| Value | Style | Description |
|---|---|---|
ACCEPT_ALL |
Primary filled | Accept all categories |
REJECT_ALL |
Outlined | Reject all optional categories |
SAVE_CHOICES |
Primary filled | Save individual toggle selections |
MANAGE |
Text link | Expand to show category toggles |
Order in the list = order on screen. Omit a button to hide it.
| Value | Description |
|---|---|
BANNER |
Compact bottom bar. Expands on "Manage". |
POPUP |
Centered modal dialog (default). |
FULLSCREEN |
Full-screen overlay. Maximum attention. |
BOTTOM_SHEET |
Draggable sheet (~85% height). Scrollable. |
All methods are static on DuckCMP.
| Method | Returns | Description |
|---|---|---|
initialize(context, config?) |
void |
Initialize SDK. Call once at app start. |
showConsentDialog(activity) |
void |
Show dialog with configured layout. |
getConsentStatus() |
ConsentStatus |
UNKNOWN, ACCEPTED, REJECTED, or PARTIAL |
getConsentResult() |
ConsentResult? |
Full consent snapshot with per-category details |
hasConsent(categoryId) |
Boolean |
Check if a category is consented |
shouldShowDialog() |
Boolean |
true if no consent stored yet |
saveConsent(result) |
void |
Programmatically save consent |
reset() |
void |
Clear all stored consent |
getConfig() |
ConsentConfig |
Get current configuration |
updateConfig(config) |
void |
Update config at runtime |
addCallback(callback) |
void |
Register for consent events |
removeCallback(callback) |
void |
Unregister a listener |
attachToWebView(webView) |
void |
Inject __duckcmp JS bridge |
detachFromWebView(webView) |
void |
Remove JS bridge |
interface ConsentCallback {
fun onConsentGiven(result: ConsentResult) // user accepted/rejected/saved
fun onConsentRevoked() // reset() called
fun onDialogDismissed() // dialog closed
}| Value | Meaning |
|---|---|
UNKNOWN |
No consent stored yet |
ACCEPTED |
All categories consented |
REJECTED |
All optional categories rejected |
PARTIAL |
Some accepted, some rejected |
For hybrid apps, inject the __duckcmp JavaScript interface:
// Android
DuckCMP.attachToWebView(webView)// iOS
DuckCMP.attachToWebView(webView)Then in your web content:
if (window.__duckcmp) {
// Read
const status = window.__duckcmp.getStatus(); // "ACCEPTED"
const hasAds = window.__duckcmp.hasConsent(3); // true/false
const full = JSON.parse(window.__duckcmp.getConsent());
// Write
window.__duckcmp.setConsent(JSON.stringify({
categories: [
{ id: 1, consent: true },
{ id: 2, consent: true },
{ id: 3, consent: false },
{ id: 4, consent: true }
]
}));
// Show native dialog from JS
window.__duckcmp.showConsentDialog();
// Listen for native changes
window.__duckcmp.onConsentChanged = function(state) {
console.log("Consent updated:", state);
};
}| Platform | Mechanism |
|---|---|
| Android | @JavascriptInterface via addJavascriptInterface(). Synchronous reads. |
| iOS | WKScriptMessageHandler + JS polyfill. Reads from pre-populated _state. |
Native consent changes are automatically pushed to all attached WebViews. WebView changes persist to native storage and trigger callbacks.
Consent is persisted using platform-native storage:
| Platform | Backend |
|---|---|
| Android | SharedPreferences (named "duckcmp") |
| iOS | UserDefaults.standard |
duckcmp_result // Full JSON consent result
duckcmp_timestamp // Unix timestamp of last consent
duckcmp_status // "ACCEPTED" | "REJECTED" | "PARTIAL" | "UNKNOWN"
duckcmp_category_{id} // Per-category boolean (e.g. duckcmp_category_2 = true)
Per-category keys let you check consent from anywhere without initializing the SDK — useful in background services, content providers, or broadcast receivers.
open_source_cmp_sdk/
├── android/
│ ├── duck-cmp/ # Library module (AAR)
│ │ └── src/main/kotlin/com/analyticsdebugger/cmp/
│ │ ├── DuckCMP.kt # Public API singleton
│ │ ├── ConsentCallback.kt
│ │ ├── model/
│ │ │ ├── ConsentConfig.kt
│ │ │ ├── ConsentCategory.kt
│ │ │ ├── ConsentTheme.kt
│ │ │ ├── ConsentLayout.kt
│ │ │ ├── ConsentButton.kt
│ │ │ ├── ConsentResult.kt
│ │ │ └── ConsentStatus.kt
│ │ ├── storage/
│ │ │ ├── ConsentStorage.kt
│ │ │ └── StorageKeys.kt
│ │ ├── ui/
│ │ │ ├── ConsentDialog.kt
│ │ │ └── ConsentDialogActivity.kt
│ │ └── webview/
│ │ └── ConsentWebViewBridge.kt
│ └── sample-app/
├── ios/
│ ├── DuckCMP/ # Swift Package
│ │ └── Sources/DuckCMP/
│ │ ├── DuckCMP.swift
│ │ ├── ConsentConfig.swift
│ │ ├── ConsentCategory.swift
│ │ ├── ConsentTheme.swift
│ │ ├── ConsentLayout.swift
│ │ ├── ConsentResult.swift
│ │ ├── ConsentStatus.swift
│ │ ├── ConsentStorage.swift
│ │ ├── StorageKeys.swift
│ │ ├── UI/
│ │ │ └── ConsentDialogView.swift
│ │ └── WebView/
│ │ └── ConsentWebViewBridge.swift
│ └── SampleApp/
├── public_site/ # Documentation website
├── LICENSE
└── README.md
| Platform | Minimum |
|---|---|
| Android | API 24 (Android 7.0) |
| iOS | iOS 15.0 |
| Kotlin | 1.9+ |
| Swift | 5.9+ |
| JDK | 17 |
MIT License. See LICENSE.
