DuckCMP mascot
Open Source · MIT License

Open Source Consent
Management for Apps

A lightweight CMP SDK for Android & iOS. Native Kotlin + Swift. Beautiful dialogs, full customization, WebView bridge. Zero dependencies.

Get Started Read the Docs
DuckCMP consent dialog screenshot
Kotlin + Compose
Swift + SwiftUI
4 Layout Options
WebView Bridge
No Dependencies

Everything you need

Built for real apps. No bloat, no third-party dependencies, just clean native code.

🎨

4 Layout Options

Banner, popup, fullscreen, or bottom sheet. Pick the presentation that fits your UX.

🌈

Full Theming

Colors, corner radius, overlay, button styles. Match your brand perfectly with ConsentTheme.

🛠

Custom Categories

Define your own consent categories with names, descriptions, and required flags. 4 sensible defaults included.

🌐

WebView Bridge

The __duckcmp JS interface lets web content read and write consent. Full bidirectional sync.

🔒

Require Consent

Force users to make a choice before proceeding. Blocks back button, overlay dismiss, and swipe-to-close.

Two Native SDKs

Pure Kotlin for Android, pure Swift for iOS. Compose & SwiftUI. Zero cross-platform overhead.

Default Categories

4 built-in consent categories. Rename, reorder, add, or remove them via ConsentConfig.

IDCategoryDescriptionRequired
1NecessaryEssential for the app to function. Cannot be disabled.Required
2AnalyticsHelp us understand how you use the app.Optional
3MarketingUsed to deliver personalized ads.Optional
4PreferencesRemember your settings and choices.Optional

Layout Options

Set via ConsentConfig(layout: ConsentLayout.POPUP)

Banner

Compact bottom bar. Non-intrusive. Expands on "Manage".

Popup

Classic centered modal. The default. Balanced visibility.

Fullscreen

Takes over the screen. Maximum attention. Great with requireConsent.

Bottom Sheet

Draggable sheet covering ~85%. Scrollable. Buttons pinned.

Up and running in minutes

Three steps to add consent management to your app

1

Add the dependency

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")
}
2

Initialize & configure

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
            )
        ))
    }
}
3

Show the dialog & check consent

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 */ }
})
1

Add the package

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")
2

Initialize & configure

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
            )
        ))
    }
}
3

Show the dialog & check consent

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

Configuration Reference

Every property you can configure, with types and defaults

ConsentConfig

Main configuration object passed to DuckCMP.initialize(). All properties have sensible defaults.

PropertyTypeDefaultDescription
categoriesList<ConsentCategory>4 defaultsConsent categories shown to the user.
dialogTitleString"Privacy Settings"Heading displayed at the top of the consent dialog.
dialogMessageString"We use cookies and similar…"Body text explaining what consent means.
acceptAllLabelString"Accept All"Text for the accept-all button.
rejectAllLabelString"Reject All"Text for the reject-all button.
saveChoicesLabelString"Save Choices"Text for saving individual toggle choices.
manageLabelString"Manage Preferences"Text for expanding to show category toggles (Banner layout).
showCategoryTogglesBooleantrueWhether to show per-category toggle switches.
requireConsentBooleanfalseIf true, dialog cannot be dismissed without choosing. Blocks back button and overlay tap.
buttonsList<ConsentButton>[ACCEPT_ALL, REJECT_ALL, SAVE_CHOICES, MANAGE]Which buttons to show and in what order. Omit to hide.
layoutConsentLayoutPOPUPPresentation style: BANNER, POPUP, FULLSCREEN, BOTTOM_SHEET.
themeConsentThemeDefault themeVisual customization: colors, corner radius, overlay.

ConsentCategory

Defines a single consent category shown to the user.

PropertyTypeDescription
idIntUnique identifier. Used to query consent with hasConsent(id).
nameStringDisplay name shown in the dialog (e.g. "Analytics").
descriptionStringExplanation shown under the category name.
isRequiredBooleanIf true, toggle is always on and disabled. Default: false.

ConsentTheme

Visual customization for the consent dialog.

PropertyDefaultDescription
primaryColor 0xFF1976D2Buttons, toggles, active elements.
backgroundColor0xFFFFFFFFDialog background color.
textColor0xFF212121Body text color.
titleColor0xFF212121Dialog heading color.
buttonTextColor0xFFFFFFFFPrimary button label color.
secondaryButtonTextColor 0xFF1976D2Outlined/secondary button text.
cornerRadius12fCorner radius for dialog and buttons (dp).
overlayColor0x80000000Background dimming overlay (ARGB).

ConsentButton

Controls which buttons appear and their visual order.

ValueStyleBehavior
ACCEPT_ALLPrimary filledGrants consent for all categories. Saves and dismisses.
REJECT_ALLOutlinedRejects all optional categories (required stay on).
SAVE_CHOICESPrimary filledSaves individual toggle selections.
MANAGEText linkExpands to show per-category toggles.

Order in the list = order on screen. Omit a button to hide it entirely.

ConsentLayout

Dialog presentation style.

ValueDescription
BANNERCompact bottom bar with accept/reject. "Manage" expands to show toggles.
POPUPCentered modal dialog. The default. Balanced visibility.
FULLSCREENFull-screen overlay. Maximum attention. Ideal with requireConsent.
BOTTOM_SHEETDraggable bottom sheet (~85% height). Scrollable with pinned buttons.

ConsentStatus

Returned by getConsentStatus().

ValueMeaning
UNKNOWNNo consent stored yet. User hasn't interacted with the dialog.
ACCEPTEDAll categories consented (including optional ones).
REJECTEDAll optional categories rejected. Only required ones active.
PARTIALSome optional categories accepted, some rejected.

Custom Categories Example

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."
        )
    ]
))

Theme Example

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
    )
))

Button Ordering

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
    )
)

Public API

Identical interface on both platforms. All methods are static on DuckCMP.

MethodReturnsDescription
initialize(context, config?)voidInitialize the SDK. Call once at app start. Config is optional (uses defaults).
showConsentDialog(activity)voidPresent the consent dialog using the configured layout.
getConsentStatus()ConsentStatusReturns UNKNOWN, ACCEPTED, REJECTED, or PARTIAL.
getConsentResult()ConsentResult?Full consent snapshot with per-category details and timestamp.
hasConsent(categoryId)BooleanCheck if a specific category has been granted consent.
shouldShowDialog()BooleanReturns true if no consent has been stored yet.
saveConsent(result)voidProgrammatically save a consent result. Triggers callbacks and WebView sync.
reset()voidClear all stored consent. Triggers onConsentRevoked callback.
getConfig()ConsentConfigReturns the current configuration.
updateConfig(config)voidUpdate configuration at runtime (e.g., switch layout or theme).
addCallback(callback)voidRegister a listener for consent changes.
removeCallback(callback)voidUnregister a listener.
attachToWebView(webView)voidInject the __duckcmp JS bridge into a WebView.
detachFromWebView(webView)voidRemove the JS bridge from a WebView.

ConsentCallback

Interface/protocol with three methods. Implement only what you need.

MethodCalled When
onConsentGiven(result)User accepts, rejects, or saves individual choices. Receives the full ConsentResult.
onConsentRevoked()reset() is called. All stored consent is cleared.
onDialogDismissed()The consent dialog is closed (by user choice or programmatically).

The __duckcmp Interface

Bidirectional consent sync between native and web content

1. Attach to WebView (native side)

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)

2. Use from JavaScript

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);
    };
}

How it works

Android
@JavascriptInterface class injected via addJavascriptInterface(). Synchronous reads.
iOS
WKScriptMessageHandler + injected JS polyfill. Reads from pre-populated _state for synchronous access.
Sync
Native consent changes are automatically pushed to all attached WebViews. WebView changes persist to native storage and trigger callbacks.

How consent is persisted

Platform-native storage. Queryable from anywhere in your app.

Android

SharedPreferences named "duckcmp"

iOS

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.