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. InitializeDuckCMP.initialize(context)
// 3. Show dialogif (DuckCMP.shouldShowDialog()) {
DuckCMP.showConsentDialog(activity)
}
// 4. Check consentval hasAnalytics =DuckCMP.hasConsent(2) // category ID 2 = Analyticsval 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
lethasAnalytics=DuckCMP.hasConsent(categoryId:2)letstatus=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 |
interfaceConsentCallback {
funonConsentGiven(result:ConsentResult) // user accepted/rejected/saved
funonConsentRevoked() // reset() called
funonDialogDismissed() // 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:
// AndroidDuckCMP.attachToWebView(webView)// iOS
DuckCMP.attachToWebView(webView)Then in your web content:
if(window.__duckcmp){// Readconststatus=window.__duckcmp.getStatus();// "ACCEPTED"consthasAds=window.__duckcmp.hasConsent(3);// true/falseconstfull=JSON.parse(window.__duckcmp.getConsent());// Writewindow.__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 JSwindow.__duckcmp.showConsentDialog();// Listen for native changeswindow.__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.
