A lightweight CMP SDK for Android & iOS. Native Kotlin + Swift. Beautiful dialogs, full customization, WebView bridge. Zero dependencies.
Built for real apps. No bloat, no third-party dependencies, just clean native code.
Banner, popup, fullscreen, or bottom sheet. Pick the presentation that fits your UX.
Colors, corner radius, overlay, button styles. Match your brand perfectly with ConsentTheme.
Define your own consent categories with names, descriptions, and required flags. 4 sensible defaults included.
The __duckcmp JS interface lets web content read and write consent. Full bidirectional sync.
Force users to make a choice before proceeding. Blocks back button, overlay dismiss, and swipe-to-close.
Pure Kotlin for Android, pure Swift for iOS. Compose & SwiftUI. Zero cross-platform overhead.
4 built-in consent categories. Rename, reorder, add, or remove them via ConsentConfig.
| ID | Category | Description | Required |
|---|---|---|---|
| 1 | Necessary | Essential for the app to function. Cannot be disabled. | Required |
| 2 | Analytics | Help us understand how you use the app. | Optional |
| 3 | Marketing | Used to deliver personalized ads. | Optional |
| 4 | Preferences | Remember your settings and choices. | Optional |
Set via ConsentConfig(layout: ConsentLayout.POPUP)
Compact bottom bar. Non-intrusive. Expands on "Manage".
Classic centered modal. The default. Balanced visibility.
Takes over the screen. Maximum attention. Great with requireConsent.
Draggable sheet covering ~85%. Scrollable. Buttons pinned.
Three steps to add consent management to your app
Gradle // settings.gradle.kts — add GitHub Packages repo 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") }
Kotlin class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Initialize with defaults (4 categories, popup layout) DuckCMP.initialize(this) // Or fully customize DuckCMP.initialize(this, ConsentConfig( dialogTitle = "Your Privacy Matters", dialogMessage = "Choose which data categories you allow.", layout = ConsentLayout.BOTTOM_SHEET, requireConsent = true, buttons = listOf( ConsentButton.SAVE_CHOICES, ConsentButton.ACCEPT_ALL, ConsentButton.REJECT_ALL ), theme = ConsentTheme( primaryColor = 0xFF6200EE, cornerRadius = 16f ) )) } }
Kotlin // Show the consent dialog DuckCMP.showConsentDialog(activity) // Check if a category is consented val hasAnalytics = DuckCMP.hasConsent(2) // Analytics val hasMarketing = DuckCMP.hasConsent(3) // Marketing // Get overall status val status = DuckCMP.getConsentStatus() // UNKNOWN, ACCEPTED, REJECTED, PARTIAL // Listen for changes DuckCMP.addCallback(object : ConsentCallback { override fun onConsentGiven(result: ConsentResult) { /* user chose */ } override fun onConsentRevoked() { /* consent reset */ } override fun onDialogDismissed() { /* dialog closed */ } })
Swift Package Manager // Xcode: File > Add Package Dependencies // URL: https://github.com/analytics-debugger/duckcmp // Or in Package.swift: .package(url: "https://github.com/analytics-debugger/duckcmp", from: "0.0.1-alpha")
Swift import DuckCMP @main struct MyApp: App { init() { // Initialize with defaults DuckCMP.initialize() // Or fully customize DuckCMP.initialize(config: ConsentConfig( dialogTitle: "Your Privacy Matters", layout: .bottomSheet, requireConsent: true, theme: ConsentTheme( primaryColor: .purple, cornerRadius: 16 ) )) } }
Swift // Show in SwiftUI Button("Privacy Settings") { showConsentDialog = true } .fullScreenCover(isPresented: $showConsentDialog) { ConsentDialogView { showConsentDialog = false } } // Check consent let hasAnalytics = DuckCMP.hasConsent(categoryId: 2) let status = DuckCMP.getConsentStatus() // .unknown, .accepted, .rejected, .partial
Every property you can configure, with types and defaults
ConsentConfigMain configuration object passed to DuckCMP.initialize(). All properties have sensible defaults.
| Property | Type | Default | Description |
|---|---|---|---|
| categories | List<ConsentCategory> | 4 defaults | Consent categories shown to the user. |
| dialogTitle | String | "Privacy Settings" | Heading displayed at the top of the consent dialog. |
| dialogMessage | String | "We use cookies and similar…" | Body text explaining what consent means. |
| acceptAllLabel | String | "Accept All" | Text for the accept-all button. |
| rejectAllLabel | String | "Reject All" | Text for the reject-all button. |
| saveChoicesLabel | String | "Save Choices" | Text for saving individual toggle choices. |
| manageLabel | String | "Manage Preferences" | Text for expanding to show category toggles (Banner layout). |
| showCategoryToggles | Boolean | true | Whether to show per-category toggle switches. |
| requireConsent | Boolean | false | If true, dialog cannot be dismissed without choosing. Blocks back button and overlay tap. |
| buttons | List<ConsentButton> | [ACCEPT_ALL, REJECT_ALL, SAVE_CHOICES, MANAGE] | Which buttons to show and in what order. Omit to hide. |
| layout | ConsentLayout | POPUP | Presentation style: BANNER, POPUP, FULLSCREEN, BOTTOM_SHEET. |
| theme | ConsentTheme | Default theme | Visual customization: colors, corner radius, overlay. |
ConsentCategoryDefines a single consent category shown to the user.
| Property | Type | Description |
|---|---|---|
| id | Int | Unique identifier. Used to query consent with hasConsent(id). |
| name | String | Display name shown in the dialog (e.g. "Analytics"). |
| description | String | Explanation shown under the category name. |
| isRequired | Boolean | If true, toggle is always on and disabled. Default: false. |
ConsentThemeVisual customization for the consent dialog.
| Property | Default | Description |
|---|---|---|
| primaryColor | 0xFF1976D2 | Buttons, toggles, active elements. |
| backgroundColor | 0xFFFFFFFF | Dialog background color. |
| textColor | 0xFF212121 | Body text color. |
| titleColor | 0xFF212121 | Dialog heading color. |
| buttonTextColor | 0xFFFFFFFF | Primary button label color. |
| secondaryButtonTextColor | 0xFF1976D2 | Outlined/secondary button text. |
| cornerRadius | 12f | Corner radius for dialog and buttons (dp). |
| overlayColor | 0x80000000 | Background dimming overlay (ARGB). |
ConsentButtonControls which buttons appear and their visual order.
| Value | Style | Behavior |
|---|---|---|
| ACCEPT_ALL | Primary filled | Grants consent for all categories. Saves and dismisses. |
| REJECT_ALL | Outlined | Rejects all optional categories (required stay on). |
| SAVE_CHOICES | Primary filled | Saves individual toggle selections. |
| MANAGE | Text link | Expands to show per-category toggles. |
Order in the list = order on screen. Omit a button to hide it entirely.
ConsentLayoutDialog presentation style.
| Value | Description |
|---|---|
| BANNER | Compact bottom bar with accept/reject. "Manage" expands to show toggles. |
| POPUP | Centered modal dialog. The default. Balanced visibility. |
| FULLSCREEN | Full-screen overlay. Maximum attention. Ideal with requireConsent. |
| BOTTOM_SHEET | Draggable bottom sheet (~85% height). Scrollable with pinned buttons. |
ConsentStatusReturned by getConsentStatus().
| Value | Meaning |
|---|---|
| UNKNOWN | No consent stored yet. User hasn't interacted with the dialog. |
| ACCEPTED | All categories consented (including optional ones). |
| REJECTED | All optional categories rejected. Only required ones active. |
| PARTIAL | Some optional categories accepted, some rejected. |
Kotlin DuckCMP.initialize(this, ConsentConfig( categories = listOf( ConsentCategory( id = 1, name = "Essential", description = "Required for core functionality.", isRequired = true ), ConsentCategory( id = 2, name = "Performance", description = "Crash reports and performance metrics." ), ConsentCategory( id = 3, name = "Targeting", description = "Personalized ad delivery." ), ConsentCategory( id = 4, name = "Social", description = "Share content to social networks." ) ) ))
Swift DuckCMP.initialize(config: ConsentConfig( categories: [ ConsentCategory( id: 1, name: "Essential", description: "Required for core functionality.", isRequired: true ), ConsentCategory( id: 2, name: "Performance", description: "Crash reports and performance metrics." ), ConsentCategory( id: 3, name: "Targeting", description: "Personalized ad delivery." ), ConsentCategory( id: 4, name: "Social", description: "Share content to social networks." ) ] ))
Kotlin DuckCMP.initialize(this, ConsentConfig( theme = ConsentTheme( primaryColor = 0xFF6200EE, // Purple accent backgroundColor = 0xFFFFFFFF, // White background textColor = 0xFF333333, // Dark gray body text titleColor = 0xFF111111, // Near-black headings buttonTextColor = 0xFFFFFFFF, // White button labels secondaryButtonTextColor = 0xFF6200EE, cornerRadius = 16f, // Rounded corners overlayColor = 0x99000000 // 60% black overlay ) ))
The order of buttons in the list determines their order on screen. Omit any button to hide it.
Kotlin // Show save first, then accept, then reject. Hide "Manage". ConsentConfig( buttons = listOf( ConsentButton.SAVE_CHOICES, ConsentButton.ACCEPT_ALL, ConsentButton.REJECT_ALL ) ) // Only show accept and reject ConsentConfig( buttons = listOf( ConsentButton.ACCEPT_ALL, ConsentButton.REJECT_ALL ) )
Identical interface on both platforms. All methods are static on DuckCMP.
Interface/protocol with three methods. Implement only what you need.
__duckcmp InterfaceBidirectional consent sync between native and web content
Kotlin // In your WebView setup DuckCMP.attachToWebView(webView) // Clean up when done DuckCMP.detachFromWebView(webView)
Swift // In your WKWebView setup DuckCMP.attachToWebView(webView) // Clean up when done DuckCMP.detachFromWebView(webView)
JavaScript if (window.__duckcmp) { // Read consent const consent = JSON.parse(window.__duckcmp.getConsent()); // { status: "ACCEPTED", categories: [{ id: 1, name: "Necessary", consent: true }, ...] } const status = window.__duckcmp.getStatus(); // "ACCEPTED" const hasAds = window.__duckcmp.hasConsent(3); // true / false // Write consent from JS window.__duckcmp.setConsent(JSON.stringify({ categories: [ { id: 1, consent: true }, { id: 2, consent: true }, { id: 3, consent: false }, { id: 4, consent: true } ] })); // Show native consent dialog from JS window.__duckcmp.showConsentDialog(); // Listen for native consent changes window.__duckcmp.onConsentChanged = function(state) { console.log("Consent updated:", state); }; }
@JavascriptInterface class injected via addJavascriptInterface(). Synchronous reads.WKScriptMessageHandler + injected JS polyfill. Reads from pre-populated _state for synchronous access.Platform-native storage. Queryable from anywhere in your app.
SharedPreferences named "duckcmp"
UserDefaults.standard
Storage Keys duckcmp_result // Full JSON consent result duckcmp_timestamp // Unix timestamp of last consent action 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.