SDK documentation
Learn how to integrate Digital Anumati with web, Android, and iOS experiences using platform-appropriate patterns.
Overview
Every SDK provides the same consent-management flow: initialize once with your site key and application ID, show the consent screen when the user hasn't decided yet, read the stored status, and let the user change or revoke consent at any time.
Web SDK
Use the JavaScript SDK to present a consent banner and capture user interactions on the web.
Android SDK (demo)
Kotlin and Jetpack Compose integration for consent capture, status checks, and revocation.
iOS SDK (demo)
Swift, SwiftUI, and UIKit integration with async/await and completion-handler APIs.
Flutter SDK (demo)
A thin Flutter plugin over the native SDKs, using MethodChannel and EventChannel.
React Native SDK (demo)
A thin React Native module over the native SDKs, with Promises and NativeEventEmitter.
Web SDK
Add the Web SDK inside the <head> of your site so the widget is ready before any trigger fires. Pick your stack below and replace YOUR_SITE_KEY and YOUR_APPLICATION_ID with the values from your application in the dashboard.
Mount the consent SDK inside <head> using next/script with strategy="beforeInteractive" so the widget is ready before the first paint.
// Next.js — add to app/layout.tsximport Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> {/* Digital Anumati Consent SDK */} <Script src="https://cdn.digitalanumati.com/widget/uat/v1/anumati-dpdp-consent-v1.js" data-site-key="YOUR_SITE_KEY" application-id="YOUR_APPLICATION_ID" strategy="beforeInteractive" /> </head> <body>{children}</body> </html> );}Next, mark the elements that should open a consent notice and verify each submission on your server — see Usage in the Developer Guide.
Android SDK (demo)
DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK
Requirements: Android Studio, Kotlin, Gradle Kotlin DSL, Jetpack Compose, minSdk 24 or higher, and targetSdk 35. The examples below make up a complete sample app, AnumatiDemoApp, with a home screen that shows the SDK initialization state and consent status, plus Manage Consent and Revoke Consent buttons.
Installation
Register the repositories and the app module.
pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() }} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // the demo SDK would be published here }} rootProject.name = "AnumatiDemoApp"include(":app")plugins { id("com.android.application") version "8.7.3" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false}Add the SDK dependency and expose the site key and application ID through BuildConfig.
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose")} android { namespace = "com.example.anumatidemo" compileSdk = 35 defaultConfig { applicationId = "com.example.anumatidemo" minSdk = 24 targetSdk = 35 versionCode = 1 versionName = "1.0" // Demo values. In a real app, inject these per build type or flavor from CI — // never hard-code production values in source. buildConfigField("String", "ANUMATI_SITE_KEY", "\"demo-site-key-123\"") buildConfigField("String", "ANUMATI_APPLICATION_ID", "\"demo-mobile-app-123\"") } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } buildFeatures { compose = true buildConfig = true }} kotlin { jvmToolchain(17)} dependencies { // DEMO / FICTIONAL SDK coordinates implementation("com.digitalanumati:consent-sdk:1.0.0") implementation(platform("androidx.compose:compose-bom:2024.12.01")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") implementation("androidx.activity:activity-compose:1.9.3") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")}The SDK needs network access to sync consent with the service.
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- The SDK syncs consent with the Digital Anumati service over HTTPS. --> <uses-permission android:name="android.permission.INTERNET" /> <application android:name=".AnumatiDemoApplication" android:label="AnumatiDemoApp" android:theme="@android:style/Theme.Material.Light.NoActionBar"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>Initialization
Initialize once, in your Application class. initialize() throws a ConsentError with code INVALID_CONFIGURATION, INVALID_SITE_KEY, or INVALID_APPLICATION_ID for a missing or malformed value; catch it so the app still starts.
package com.example.anumatidemo import android.app.Applicationimport android.util.Logimport com.digitalanumati.consent.ConsentErrorimport com.digitalanumati.consent.DigitalAnumatiimport com.digitalanumati.consent.Environment class AnumatiDemoApplication : Application() { override fun onCreate() { super.onCreate() try { DigitalAnumati.initialize( context = applicationContext, siteKey = BuildConfig.ANUMATI_SITE_KEY, applicationId = BuildConfig.ANUMATI_APPLICATION_ID, environment = if (BuildConfig.DEBUG) Environment.UAT else Environment.PRODUCTION, ) } catch (error: ConsentError) { // Keep the app usable. Consent actions stay disabled until initialization succeeds. Log.w(TAG, "Consent SDK failed to initialize: " + error.message) } } private companion object { const val TAG = "AnumatiDemo" }}Show consent & status
The activity reads the stored status on launch, opens the consent screen when the user hasn't decided yet, and receives the ConsentResult through ConsentListener. Revoking runs in a coroutine because it syncs with the service.
package com.example.anumatidemo import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Buttonimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.OutlinedButtonimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.lifecycleScopeimport com.digitalanumati.consent.ConsentErrorimport com.digitalanumati.consent.ConsentErrorCodeimport com.digitalanumati.consent.ConsentListenerimport com.digitalanumati.consent.ConsentResultimport com.digitalanumati.consent.ConsentStatusimport com.digitalanumati.consent.DigitalAnumatiimport kotlinx.coroutines.launch class MainActivity : ComponentActivity() { private var consentStatus by mutableStateOf(ConsentStatus.NOT_DETERMINED) private var errorMessage by mutableStateOf<String?>(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (DigitalAnumati.isInitialized()) { consentStatus = DigitalAnumati.getConsentStatus() // First launch (or after a revoke): ask before continuing. if (consentStatus == ConsentStatus.NOT_DETERMINED) showConsent() } setContent { MaterialTheme { HomeScreen( sdkReady = DigitalAnumati.isInitialized(), status = consentStatus, error = errorMessage, onManageConsent = ::showConsent, onRevokeConsent = ::revokeConsent, ) } } } private fun showConsent() { DigitalAnumati.showConsent( activity = this, listener = object : ConsentListener { override fun onConsentCompleted(result: ConsentResult) { consentStatus = result.status errorMessage = null } override fun onError(error: ConsentError) { errorMessage = when (error.code) { ConsentErrorCode.USER_CANCELLED -> null // closed without choosing; keep the previous state ConsentErrorCode.NETWORK_ERROR -> "Couldn't reach the consent service. Check your connection and try again." else -> error.message } } }, ) } private fun revokeConsent() { lifecycleScope.launch { try { DigitalAnumati.revokeConsent() consentStatus = DigitalAnumati.getConsentStatus() // NOT_DETERMINED after a revoke errorMessage = null } catch (error: ConsentError) { errorMessage = error.message } } }} @Composablefun HomeScreen( sdkReady: Boolean, status: ConsentStatus, error: String?, onManageConsent: () -> Unit, onRevokeConsent: () -> Unit,) { Scaffold { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .padding(24.dp) .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text("AnumatiDemoApp", style = MaterialTheme.typography.headlineMedium) Text("SDK initialized: " + if (sdkReady) "Yes" else "No") Text("Consent status: " + status.name) if (error != null) { Text(error, color = MaterialTheme.colorScheme.error) } Button(onClick = onManageConsent, enabled = sdkReady) { Text("Manage Consent") } OutlinedButton( onClick = onRevokeConsent, enabled = sdkReady && status != ConsentStatus.NOT_DETERMINED, ) { Text("Revoke Consent") } } }}For release builds with R8 enabled, keep the SDK's public API:
# app/proguard-rules.pro# A real SDK normally ships these as consumer rules inside its AAR; they're shown# here so release (R8) builds of the demo keep the SDK's public API intact.-keep class com.digitalanumati.consent.** { *; }-keepclassmembers enum com.digitalanumati.consent.** { *; }-dontwarn com.digitalanumati.consent.**SDK types used above, for reference:
// Public types exposed by the DEMO SDK (package com.digitalanumati.consent).// Shown for reference — you don't write these; they ship inside the SDK.// The Flutter plugin and React Native module call exactly this API.package com.digitalanumati.consent enum class Environment { UAT, PRODUCTION } enum class ConsentStatus { NOT_DETERMINED, // the user hasn't answered yet (or consent was revoked) GRANTED, // every optional category accepted DENIED, // every optional category rejected PARTIAL, // some optional categories accepted} enum class ConsentCategory { NECESSARY, ANALYTICS, PERSONALIZATION, ADVERTISING } data class ConsentPreferences( val analytics: Boolean = false, val personalization: Boolean = false, val advertising: Boolean = false,) { val necessary: Boolean get() = true // always on; not user-editable} data class ConsentResult( val status: ConsentStatus, val preferences: ConsentPreferences, val timestamp: Long, // epoch millis) enum class ConsentErrorCode { INITIALIZATION_FAILED, // initialize() failed, or a call was made before initialize() INVALID_CONFIGURATION, INVALID_SITE_KEY, INVALID_APPLICATION_ID, NETWORK_ERROR, CONSENT_LOAD_FAILED, CONSENT_SAVE_FAILED, USER_CANCELLED, UNKNOWN_ERROR,} class ConsentError( val code: ConsentErrorCode, message: String, cause: Throwable? = null,) : Exception(message, cause) interface ConsentListener { fun onConsentCompleted(result: ConsentResult) fun onError(error: ConsentError)} enum class ConsentEventType { SDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED, CONSENT_UPDATED, CONSENT_REVOKED, CONSENT_ERROR,} data class ConsentEvent( val type: ConsentEventType, val result: ConsentResult? = null, // set for ACCEPTED, REJECTED and UPDATED val error: ConsentError? = null, // set for CONSENT_ERROR) object DigitalAnumati { // Throws ConsentError: INVALID_CONFIGURATION, INVALID_SITE_KEY, INVALID_APPLICATION_ID, INITIALIZATION_FAILED fun initialize(context: Context, siteKey: String, applicationId: String, environment: Environment) fun isInitialized(): Boolean // Presents the SDK-owned consent screen; the result or error arrives through the listener. fun showConsent(activity: Activity, listener: ConsentListener) fun getConsentStatus(): ConsentStatus // reads local storage, no network call fun getConsentPreferences(): ConsentPreferences // reads local storage, no network call suspend fun updateConsent(preferences: ConsentPreferences): ConsentResult suspend fun revokeConsent() // One listener; events are delivered on the main thread. fun setEventListener(listener: ((ConsentEvent) -> Unit)?)}Folder structure of the sample app:
AnumatiDemoApp/├── app/│ ├── src/main/│ │ ├── java/com/example/anumatidemo/│ │ │ ├── AnumatiDemoApplication.kt # SDK initialization│ │ │ └── MainActivity.kt # Home screen, consent status, manage / revoke│ │ └── AndroidManifest.xml│ ├── build.gradle.kts # SDK dependency + BuildConfig values│ └── proguard-rules.pro├── build.gradle.kts└── settings.gradle.ktsAPI reference
| Method | Parameters | Returns | Purpose | Possible errors |
|---|---|---|---|---|
| initialize() | context, siteKey, applicationId, environment | Unit | Configures the SDK. Call once in Application.onCreate(). | INVALID_CONFIGURATION, INVALID_SITE_KEY, INVALID_APPLICATION_ID, INITIALIZATION_FAILED |
| isInitialized() | — | Boolean | Whether initialize() succeeded; use it to enable consent actions. | — |
| showConsent() | activity, listener: ConsentListener | Unit (result via listener) | Opens the SDK-owned consent screen; delivers a ConsentResult to onConsentCompleted. | INITIALIZATION_FAILED, NETWORK_ERROR, CONSENT_SAVE_FAILED, USER_CANCELLED |
| getConsentStatus() | — | ConsentStatus | Reads the stored status without any network call. | INITIALIZATION_FAILED, CONSENT_LOAD_FAILED |
| getConsentPreferences() | — | ConsentPreferences | Reads the stored per-category choices without any network call. | INITIALIZATION_FAILED, CONSENT_LOAD_FAILED |
| updateConsent() | preferences: ConsentPreferences (suspend) | ConsentResult | Saves new choices without showing the screen, e.g. from your own settings toggle. | INITIALIZATION_FAILED, NETWORK_ERROR, CONSENT_SAVE_FAILED |
| revokeConsent() | — (suspend) | Unit | Withdraws consent; status returns to NOT_DETERMINED. | INITIALIZATION_FAILED, NETWORK_ERROR, CONSENT_SAVE_FAILED |
| setEventListener() | listener: ((ConsentEvent) -> Unit)? | Unit | Receives SDK events on the main thread; used by the Flutter and React Native bridges. | — |
if (DigitalAnumati.isInitialized() && DigitalAnumati.getConsentStatus() == ConsentStatus.NOT_DETERMINED) { DigitalAnumati.showConsent(activity = this, listener = consentListener)}iOS SDK (demo)
DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK
Requirements: Xcode, Swift, iOS 15 or later. The SDK offers async/await APIs and a completion-handler variant for UIKit code. The examples below make up the iOS version of AnumatiDemoApp.
Installation
Swift Package Manager in Xcode: choose File → Add Package Dependencies…, enter https://github.com/example/digital-anumati-ios-sdk, select version 1.0.0 (“Up to Next Major”), and add the DigitalAnumatiConsent product to your app target. If your code lives in a local Swift package, declare it in Package.swift instead:
// swift-tools-version:5.9// Package.swift — for teams that keep app code in a local Swift package.// Most apps instead add the SDK in Xcode (see "Swift Package Manager in Xcode").import PackageDescription let package = Package( name: "AnumatiDemoApp", platforms: [.iOS(.v15)], products: [ .library(name: "AnumatiDemoApp", targets: ["AnumatiDemoApp"]), ], dependencies: [ // DEMO / FICTIONAL package URL .package(url: "https://github.com/example/digital-anumati-ios-sdk", from: "1.0.0"), ], targets: [ .target( name: "AnumatiDemoApp", dependencies: [ .product(name: "DigitalAnumatiConsent", package: "digital-anumati-ios-sdk"), ], path: "AnumatiDemoApp" ), ])Or, with CocoaPods:
# Podfile — optional, if your project uses CocoaPods instead of SPMplatform :ios, '15.0' target 'AnumatiDemoApp' do use_frameworks! # DEMO / FICTIONAL pod pod 'DigitalAnumatiConsent', '~> 1.0'endConfiguration & initialization
Keep per-environment values in .xcconfig files and expose them through Info.plist, so switching between UAT and production is a build-configuration change rather than a code change.
// Config/UAT.xcconfig — one file per environment, selected by build configuration.// Keep production values out of source control; inject them from CI.ANUMATI_SITE_KEY = demo-site-key-123ANUMATI_APPLICATION_ID = demo-mobile-app-123ANUMATI_ENVIRONMENT = uat<!-- Info.plist — values come from the active .xcconfig --><key>DigitalAnumatiSiteKey</key><string>$(ANUMATI_SITE_KEY)</string><key>DigitalAnumatiApplicationId</key><string>$(ANUMATI_APPLICATION_ID)</string><key>DigitalAnumatiEnvironment</key><string>$(ANUMATI_ENVIRONMENT)</string>// AnumatiDemoApp/Models/SDKConfiguration.swiftimport Foundationimport DigitalAnumatiConsent struct SDKConfiguration { let siteKey: String let applicationId: String let environment: DigitalAnumati.Environment enum ConfigurationError: LocalizedError { case missingKey(String) var errorDescription: String? { switch self { case .missingKey(let key): return "Missing \(key) in Info.plist." } } } static func fromInfoPlist(_ bundle: Bundle = .main) throws -> SDKConfiguration { func value(_ key: String) throws -> String { guard let value = bundle.object(forInfoDictionaryKey: key) as? String, !value.isEmpty else { throw ConfigurationError.missingKey(key) } return value } return SDKConfiguration( siteKey: try value("DigitalAnumatiSiteKey"), applicationId: try value("DigitalAnumatiApplicationId"), environment: try value("DigitalAnumatiEnvironment") == "production" ? .production : .uat ) }}The app creates a ConsentService at launch, which initializes the SDK and publishes its state to the UI.
// AnumatiDemoApp/App/AnumatiDemoApp.swiftimport SwiftUI @mainstruct AnumatiDemoApp: App { // Creating the service initializes the SDK once, at launch. @StateObject private var consent = ConsentService() var body: some Scene { WindowGroup { HomeView() .environmentObject(consent) } }}// AnumatiDemoApp/Services/ConsentService.swiftimport UIKitimport DigitalAnumatiConsent @MainActorfinal class ConsentService: ObservableObject { @Published private(set) var isInitialized = false @Published private(set) var status: ConsentStatus = .notDetermined @Published var errorMessage: String? init() { do { let config = try SDKConfiguration.fromInfoPlist() try DigitalAnumati.initialize( siteKey: config.siteKey, applicationId: config.applicationId, environment: config.environment ) isInitialized = true status = DigitalAnumati.getConsentStatus() } catch { // Keep the app usable. Consent actions stay disabled until initialization succeeds. errorMessage = "Consent SDK failed to initialize: \(error.localizedDescription)" } } /// Presents the SDK's consent screen and records the result. func showConsent() async { guard let presenter = UIApplication.shared.topViewController else { return } do { let result = try await DigitalAnumati.showConsent(from: presenter) status = result.status errorMessage = nil } catch let error as ConsentError where error.code == .userCancelled { // Closed without choosing; keep the previous state. } catch { errorMessage = message(for: error) } } func revokeConsent() async { do { try await DigitalAnumati.revokeConsent() status = DigitalAnumati.getConsentStatus() // .notDetermined after a revoke errorMessage = nil } catch { errorMessage = message(for: error) } } private func message(for error: Error) -> String { switch (error as? ConsentError)?.code { case .networkError?: return "Couldn't reach the consent service. Check your connection and try again." case .initializationFailed?: return "The consent SDK isn't initialized." default: return error.localizedDescription } }}// AnumatiDemoApp/Services/UIApplication+TopViewController.swiftimport UIKit extension UIApplication { /// The view controller currently on screen — used to present the SDK's consent screen from SwiftUI. var topViewController: UIViewController? { let scene = connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive } var top = scene?.windows.first { $0.isKeyWindow }?.rootViewController while let presented = top?.presentedViewController { top = presented } return top }}Show consent & status
SwiftUI: the home screen shows the initialization state and consent status, and asks for consent on first launch.
// AnumatiDemoApp/Views/HomeView.swiftimport SwiftUIimport DigitalAnumatiConsent struct HomeView: View { @EnvironmentObject private var consent: ConsentService var body: some View { NavigationView { List { Section("SDK") { row("Initialized", consent.isInitialized ? "Yes" : "No") row("Consent status", consent.status.rawValue) } if let message = consent.errorMessage { Section { Text(message).foregroundStyle(.red) } } Section { Button("Manage Consent") { Task { await consent.showConsent() } } Button("Revoke Consent", role: .destructive) { Task { await consent.revokeConsent() } } .disabled(consent.status == .notDetermined) } .disabled(!consent.isInitialized) } .navigationTitle("AnumatiDemoApp") .task { // First launch (or after a revoke): ask before continuing. if consent.isInitialized && consent.status == .notDetermined { await consent.showConsent() } } } } private func row(_ title: String, _ value: String) -> some View { HStack { Text(title) Spacer() Text(value).foregroundStyle(.secondary) } }}UIKit: use the completion-handler variant, which delivers a Result.
// UIKit — completion-handler APIimport UIKitimport DigitalAnumatiConsent final class PrivacySettingsViewController: UIViewController { private let statusLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground let manageButton = UIButton(type: .system) manageButton.setTitle("Manage Consent", for: .normal) manageButton.addTarget(self, action: #selector(manageConsent), for: .touchUpInside) let stack = UIStackView(arrangedSubviews: [statusLabel, manageButton]) stack.axis = .vertical stack.spacing = 16 stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.centerXAnchor.constraint(equalTo: view.centerXAnchor), stack.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) statusLabel.text = "Consent status: \(DigitalAnumati.getConsentStatus().rawValue)" } @objc private func manageConsent() { DigitalAnumati.showConsent(from: self) { [weak self] result in switch result { case .success(let consent): self?.statusLabel.text = "Consent status: \(consent.status.rawValue)" case .failure(let error) where error.code == .userCancelled: break // closed without choosing case .failure(let error): self?.statusLabel.text = "Couldn't update consent: \(error.message)" } } }}SDK types used above, for reference:
// Public types exposed by the DEMO SDK (module DigitalAnumatiConsent).// Shown for reference — you don't write these; they ship inside the SDK.// The Flutter plugin and React Native module call exactly this API.import UIKit public enum ConsentStatus: String { case notDetermined = "NOT_DETERMINED" // no answer yet (or consent was revoked) case granted = "GRANTED" // every optional category accepted case denied = "DENIED" // every optional category rejected case partial = "PARTIAL" // some optional categories accepted} public enum ConsentCategory: String, CaseIterable { case necessary = "NECESSARY", analytics = "ANALYTICS" case personalization = "PERSONALIZATION", advertising = "ADVERTISING"} public struct ConsentPreferences: Equatable { public var necessary: Bool { true } // always on; not user-editable public var analytics: Bool public var personalization: Bool public var advertising: Bool public init(analytics: Bool = false, personalization: Bool = false, advertising: Bool = false)} public struct ConsentResult { public let status: ConsentStatus public let preferences: ConsentPreferences public let timestamp: Date} public struct ConsentError: LocalizedError { public enum Code: String { case initializationFailed = "INITIALIZATION_FAILED" // also: called before initialize() case invalidConfiguration = "INVALID_CONFIGURATION" case invalidSiteKey = "INVALID_SITE_KEY" case invalidApplicationId = "INVALID_APPLICATION_ID" case networkError = "NETWORK_ERROR" case consentLoadFailed = "CONSENT_LOAD_FAILED" case consentSaveFailed = "CONSENT_SAVE_FAILED" case userCancelled = "USER_CANCELLED" case unknownError = "UNKNOWN_ERROR" } public let code: Code public let message: String public var errorDescription: String? { message }} public enum ConsentEventType: String { case sdkInitialized = "SDK_INITIALIZED", consentShown = "CONSENT_SHOWN" case consentAccepted = "CONSENT_ACCEPTED", consentRejected = "CONSENT_REJECTED" case consentUpdated = "CONSENT_UPDATED", consentRevoked = "CONSENT_REVOKED" case consentError = "CONSENT_ERROR"} public struct ConsentEvent { public let type: ConsentEventType public let result: ConsentResult? // set for accepted, rejected and updated public let error: ConsentError? // set for consentError} public enum DigitalAnumati { // Nested so it doesn't clash with SwiftUI's @Environment property wrapper. public enum Environment { case uat, production } // Throws ConsentError: invalidConfiguration, invalidSiteKey, invalidApplicationId, initializationFailed public static func initialize(siteKey: String, applicationId: String, environment: Environment) throws public static var isInitialized: Bool { get } // Presents the SDK-owned consent screen. @MainActor public static func showConsent( from viewController: UIViewController, completion: @escaping (Result<ConsentResult, ConsentError>) -> Void ) @MainActor public static func showConsent(from viewController: UIViewController) async throws -> ConsentResult public static func getConsentStatus() -> ConsentStatus // local storage, no network call public static func getConsentPreferences() -> ConsentPreferences // local storage, no network call public static func updateConsent(_ preferences: ConsentPreferences) async throws -> ConsentResult public static func revokeConsent() async throws // One handler; events are delivered on the main thread. public static func setEventHandler(_ handler: ((ConsentEvent) -> Void)?)}Folder structure of the sample app:
AnumatiDemoApp/├── AnumatiDemoApp/│ ├── App/│ │ └── AnumatiDemoApp.swift # @main, creates ConsentService│ ├── Views/│ │ ├── HomeView.swift # status, Manage / Revoke buttons│ │ └── PrivacySettingsViewController.swift # UIKit example│ ├── Services/│ │ ├── ConsentService.swift # SDK init, show / revoke, errors│ │ └── UIApplication+TopViewController.swift│ ├── Models/│ │ └── SDKConfiguration.swift # reads Info.plist values│ └── Info.plist├── Config/│ ├── UAT.xcconfig│ └── Production.xcconfig└── Package.swiftAPI reference
| Method | Parameters | Returns | Purpose | Possible errors |
|---|---|---|---|---|
| initialize(siteKey:applicationId:environment:) | String, String, DigitalAnumati.Environment | Void (throws) | Configures the SDK. Call once at launch. | .invalidConfiguration, .invalidSiteKey, .invalidApplicationId, .initializationFailed |
| isInitialized | — (static property) | Bool | Whether initialization succeeded. | — |
| showConsent(from:) async | UIViewController | ConsentResult (throws) | Presents the SDK-owned consent screen and returns the user's choice. | .initializationFailed, .networkError, .consentSaveFailed, .userCancelled |
| showConsent(from:completion:) | UIViewController, (Result<ConsentResult, ConsentError>) -> Void | Void | Completion-handler variant for UIKit. | Same as above, via .failure |
| getConsentStatus() | — | ConsentStatus | Reads the stored status without any network call. | — (returns .notDetermined before initialization) |
| getConsentPreferences() | — | ConsentPreferences | Reads the stored per-category choices without any network call. | — (returns defaults before initialization) |
| updateConsent(_:) async | ConsentPreferences | ConsentResult (throws) | Saves new choices without showing the screen. | .initializationFailed, .networkError, .consentSaveFailed |
| revokeConsent() async | — | Void (throws) | Withdraws consent; status returns to .notDetermined. | .initializationFailed, .networkError, .consentSaveFailed |
| setEventHandler(_:) | ((ConsentEvent) -> Void)? | Void | Receives SDK events on the main thread; used by the Flutter and React Native bridges. | — |
if DigitalAnumati.getConsentStatus() == .notDetermined { let result = try await DigitalAnumati.showConsent(from: viewController) print("Consent status: \(result.status)")}Cross-platform SDKs (demo)
DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK
Flutter and React Native apps use the same native SDKs through thin bridges. The bridges only handle serialization, method calls, callbacks, events, error mapping, and type conversion; consent storage, rules, validation, backend communication, and the consent screen stay in the native SDK, so every app behaves the same way.
Consent Backend | +--------------+--------------+ | | Android Native SDK iOS Native SDK (consent logic, storage, (consent logic, storage, consent UI, networking) consent UI, networking) | | +-------+-------+ +-------+-------+ | | | | Flutter React Native Flutter React Native plugin native module plugin native module (Kotlin) (Kotlin) (Swift) (Swift) | | | | +-------+-------+-------------+-------+-------+ | | Dart API (Flutter) TypeScript API (React Native) | | +--------- Host application --+Flutter SDK (demo)
digital_anumati_consent — MethodChannel for commands, EventChannel for events.
React Native SDK (demo)
@digitalanumati/react-native-consent — Promise API with NativeEventEmitter.
Data type mapping
| Concept | Android | iOS | Flutter | React Native |
|---|---|---|---|---|
| ConsentStatus | Kotlin enum | Swift enum (String raw values) | Dart enum (wire value) | TypeScript string union |
| ConsentCategory | Kotlin enum | Swift enum | Dart enum | TypeScript string union |
| ConsentPreferences | Kotlin data class | Swift struct | Dart class | TypeScript interface |
| ConsentResult | Kotlin data class (timestamp: Long ms) | Swift struct (timestamp: Date) | Dart class (DateTime) | TypeScript interface (timestamp: number ms) |
| ConsentError | Kotlin exception with code | Swift Error with code | Dart exception (ConsentError) | JS Error with code |
| Environment | Environment.UAT / PRODUCTION | DigitalAnumati.Environment .uat / .production | Environment.uat / .production | 'uat' | 'production' |
| Events | setEventListener callback | setEventHandler closure | Stream<ConsentEvent> (EventChannel) | addListener (NativeEventEmitter) |
| Async calls | suspend functions / listener | async/await / completion handler | Future | Promise |
Error contract
Every platform reports the same code strings. Flutter receives them as ConsentError.code (from PlatformException); React Native as error.code on the rejected Promise.
| Code | Meaning | Typical cause |
|---|---|---|
| INITIALIZATION_FAILED | The SDK isn't ready | initialize() failed, or a call ran before it completed |
| INVALID_CONFIGURATION | Configuration is incomplete or inconsistent | Missing values or an unknown environment |
| INVALID_SITE_KEY | The site key was rejected | Wrong key, or a key from the other environment |
| INVALID_APPLICATION_ID | The application ID was rejected | Wrong ID, or an ID from the other environment |
| NETWORK_ERROR | The consent service couldn't be reached | No connectivity or a TLS failure |
| CONSENT_LOAD_FAILED | Stored consent couldn't be read | Corrupted or inaccessible local storage |
| CONSENT_SAVE_FAILED | Consent couldn't be saved | Storage or service write failure |
| USER_CANCELLED | The user closed the consent screen without choosing | Not a failure — keep the previous state |
| UNKNOWN_ERROR | Anything else | Unexpected native exception; no presenting activity / view controller |
Events (SDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED, CONSENT_UPDATED, CONSENT_REVOKED, CONSENT_ERROR) use the same names everywhere — see the event contract.
Consent screen
Both mobile SDKs present the same screen. Necessary is always on; the optional categories can be toggled. Accept All and Reject All submit immediately; Save Preferences submits the current toggles. Try it:
Privacy & Consent
AnumatiDemoApp asks for your permission to process some of your data. Choose what you're comfortable with — you can change this any time in Settings.
Necessary
Needed for sign-in, security and core features. Always on.
Analytics
Helps us understand how the app is used so we can improve it.
Personalization
Remembers your preferences to tailor content.
Advertising
Shows ads that are more relevant to you.
ConsentResult returned to the app
Toggle categories and press a button to see the result the host app receives in onConsentCompleted (Android), the completion handler (iOS), or the resolved showConsent() call (Flutter and React Native).
| Status | When |
|---|---|
| GRANTED / .granted | Every optional category accepted |
| DENIED / .denied | Every optional category rejected |
| PARTIAL / .partial | Some optional categories accepted |
| NOT_DETERMINED / .notDetermined | No answer yet, or consent was revoked |
Consent lifecycle
Application starts ↓SDK initialization ↓Check existing consent (getConsentStatus) ↓If consent exists ──────────────→ Continue application ↓If consent does not exist ↓Display consent UI (showConsent) ↓User selects preferences ↓SDK stores consent ↓Return ConsentResult ↓Application continues| Scenario | What happens |
|---|---|
| Changing consent | The user taps Manage Consent → showConsent() opens with their current choices pre-selected → the new ConsentResult replaces the stored one. |
| Revoking consent | revokeConsent() withdraws consent on the service and clears the stored result → status becomes NOT_DETERMINED → the app asks again on next launch. |
| Application restart | initialize() restores the stored result, so getConsentStatus() returns the last decision immediately — no network call, and no consent screen if a decision exists. |
| Initialization failure | initialize() throws (e.g. missing key) → isInitialized is false → the app keeps running with consent actions disabled and treats optional processing as not consented. |
UAT & production
| Environment | Android | iOS | Flutter | React Native | Base URL (placeholder) |
|---|---|---|---|---|---|
| UAT | Environment.UAT | .uat | Environment.uat | 'uat' | https://api.uat.example.digitalanumati.com |
| Production | Environment.PRODUCTION | .production | Environment.production | 'production' | https://api.example.digitalanumati.com |
Demo configuration: site key demo-site-key-123, application ID demo-mobile-app-123, environment UAT. These URLs are fictional placeholders — the SDK selects the endpoint from the environment, and real production endpoints must be supplied by the actual SDK provider. Select the environment per build (debug → UAT, release → production) rather than switching it in code — in Flutter with kReleaseMode, in React Native with __DEV__.
Security
- Don't hard-code production values in source; inject them per build from CI.
- Treat site keys and application IDs according to the actual SDK provider's security model — they identify your app, they aren't secrets.
- Never put private API credentials (such as the server
x-api-key) in a mobile app. Server-side verification belongs on your backend. - All SDK traffic must use HTTPS.
- Don't log consent data or identifiers unnecessarily, especially in release builds.
- Protect configuration files and keep production xcconfig / Gradle properties out of source control.
- Handle SDK and network errors gracefully — the app should keep working if consent can't sync.
- Never put private server credentials in Flutter or React Native code either — bundled Dart and JavaScript are as easy to extract as native code.
- Consent is stored locally by the native SDK in protected storage (Android Keystore-backed encrypted storage, iOS Keychain); the Flutter and React Native layers never persist it.
- Keep the platform's certificate validation on: don't disable App Transport Security or allow cleartext traffic, and don't add custom trust managers.
- Treat reference IDs, emails, and phone numbers as PII — don't send them to analytics or logs.
Testing checklist
- Fresh install shows the consent screen; a second launch doesn't.
- Accept All, Reject All, and Save Preferences return GRANTED, DENIED, and PARTIAL respectively.
- Closing the screen without choosing keeps the previous status (USER_CANCELLED).
- Revoke Consent resets the status to NOT_DETERMINED and the screen appears on next launch.
- Airplane mode: showConsent / revokeConsent report a network error and the app stays usable.
- Missing or empty site key: initialization fails gracefully and consent actions are disabled.
- Release build with R8 (Android) still initializes and shows the consent screen.
- UAT and production builds point at the right environment.
Troubleshooting
Web SDK doesn't load
| Symptom | Likely cause | Fix |
|---|---|---|
| INITIALIZATION_FAILED | A call ran before initialize(), or initialize() failed | Initialize in Application.onCreate() / at app launch and check isInitialized first. |
| INVALID_CONFIGURATION / INVALID_SITE_KEY at launch | Missing, empty or wrong site key or application ID | Check BuildConfig fields (Android) or the Info.plist / xcconfig values (iOS). |
| Network error on showConsent | No connectivity, or wrong environment | Retry when online; confirm UAT vs production for the build. |
| Consent screen never appears | A decision is already stored | Expected — call showConsent() from a Manage Consent action, or revoke to reset. |
| Crash or missing classes in release (Android) | R8 removed SDK classes | Add the ProGuard rules above. |
| Package resolution fails | The demo SDK isn't published | Expected for this demo — use the real coordinates once the SDK is released. |