Skip to content

The SpotDraft Clickwrap Android Utility is a native Kotlin/Java library designed to provide a robust and seamless bridge between any Android application and SpotDraft's powerful Clickwrap backend.

Notifications You must be signed in to change notification settings

SpotDraft/spotdraft-clickthrough-utils-android-kotlin

Repository files navigation

SpotDraft Logo

SpotDraft Clickwrap Android Kotlin Utility

A lightweight, headless, and modern Kotlin library for integrating legally-binding SpotDraft Clickwrap agreements into any Android application.

Jetpack Compose Kotlin 1.8+ API 21+ License: MIT Version 1.0.0


Overview

The SpotDraft Clickwrap Android Utility offers a clean, secure, and scalable way to embed legally-compliant consent flows directly into your native Android apps. Built as a headless Utility, it manages backend communication, data normalization, and state handling — allowing you to focus on delivering seamless user experiences with Jetpack Compose or traditional Views while maintaining airtight legal compliance.

What is SpotDraft Clickwrap?

A clickwrap agreement is a legally binding digital contract where users provide consent by tapping a button or selecting a checkbox (e.g., “I agree to the Terms of Service”).

SpotDraft empowers you to create, publish, and track these agreements from a centralized dashboard. This Utility acts as the bridge between your dashboard configuration and your app UI — enabling you to present legal terms and capture user consent effortlessly.

Why This Utility?

Integrating legal agreements can be time-consuming, error-prone, and repetitive. This Utility abstracts the complexity by providing:

  • Event-driven consent flow
  • Automated backend communication
  • Built-in data and state management
  • Minimal UI constraints — fully customizable

Simply integrate, bind events, and you're ready to go — no legal boilerplate or custom backend logic required.


Key Features

Feature Description
Dynamic & Headless Retrieves clickwrap configuration directly from your SpotDraft dashboard. The Utility handles logic and data, while you retain full control over UI implementation.
Modern & Asynchronous Built using Kotlin Coroutines for safe, performant, and non-blocking execution on Android.
Event-Driven Architecture Utilizes a ClickwrapListener interface to stream real-time events such as state changes, success callbacks, and error updates for seamless UI integration.
Typed Error Handling Offers a clearly-defined ClickwrapError sealed class for predictable, structured, and efficient error management.
Zero Dependencies Uses only native Android and Kotlin libraries to ensure minimal footprint and zero third-party conflicts.
Re-Acceptance Logic Automatically validates if returning users must re-accept updated terms, ensuring compliance with evolving legal requirements.

System Requirements

Requirement Minimum Version
Min SDK 21+ (Lollipop)
Android Studio Flamingo+
Kotlin 1.8.0+
Jetpack Compose 1.4.0+

Compatibility Notes

This Utility is designed to work seamlessly with:

  • Native Android projects (Jetpack Compose and XML Views supported)
  • Modern Android architectures (MVVM, MVI, Clean Architecture)

It does not rely on third-party libraries, ensuring compatibility and stability across enterprise-level applications.

Integrating SpotDraft's Android Clickwrap Utility: A Step-by-Step Guide

This guide provides a clear, step-by-step walkthrough for integrating the SpotDraft Clickwrap Utility into your Android application. Follow these instructions to manage legal agreements and capture user consent seamlessly.


1. Add the SpotDraftClickwrap Utility to Your Project

This project is distributed as a local utility module.

  1. Download: Get the SpotDraftClickwrap utility.
  2. Place the folder: Copy the spotdraftclickwrap folder into the root directory of your Android Studio project, alongside your app module.
  3. Configure settings.gradle.kts: In your project's settings.gradle.kts file, include the utility as a module:
    // settings.gradle.kts
    include(":app", ":spotdraftclickwrap")
  4. Add Project Dependency: In your app-level build.gradle.kts file, add a dependency on the newly included project module.
    // app/build.gradle.kts
    dependencies {
        // ... other dependencies
        implementation(project(":spotdraftclickwrap"))
    }
  5. Sync Project: Sync your project with the updated Gradle files.

2. Obtain Your Clickwrap ID

  1. Log In to your SpotDraft account.
  2. Navigate to the Clickthrough section via the side menu.
  3. Select an existing "Clickthrough Package" or create a new one.
  4. Inside the package, create and Publish at least one legal agreement.
  5. Go to the Clickthrough Settings tab to find your Clickwrap ID.

3. Initialize the Utility

The SpotDraftManager is a singleton that manages the entire clickwrap lifecycle. Initialize it once when your application launches, typically in your custom Application class's onCreate method.

// In src/main/com/your/package/MainApplication.kt
import android.app.Application
import android.util.Log
import com.app.android.clickwrap.config.ClickwrapConfig
import com.app.android.clickwrap.core.SpotDraftManager
import com.app.android.clickwrap.helper.errors.ClickwrapError

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        try {
            // 1. Configure the Utility with your specific details.
            val config = ClickwrapConfig(
                // Replace with the actual ID from your SpotDraft account.
                clickwrapId = "YOUR_CLICKWRAP_ID",
                // The base URL for the SpotDraft API.
                baseURL = "https://api.spotdraft.com",
                // Optional: A unique domain for your application.
                domain = "your-app-domain.com"
            )

            // 2. Initialize the manager with the configuration.
            // This sets up the singleton for use throughout your app.
            SpotDraftManager.initialize(this, config)

        } catch (e: ClickwrapError.InvalidConfig) {
            // If initialization fails, it's a critical error.
            Log.e("MainApplication", "Failed to initialize SpotDraftManager", e)
        }
    }
}

4. Load Clickwrap Agreements

4.1. Set the Event Listener

To receive data and events from the Utility, your screen's ViewModel (or another lifecycle-aware class) should implement the ClickwrapListener interface.

// Make your ViewModel implement the listener interface.
class YourViewModel : ViewModel(), ClickwrapListener {

    init {
        // Register this class as the listener for clickwrap events.
        SpotDraftManager.setClickwrapListener(this)
    }

    // MARK: - ClickwrapListener Callbacks

    /// Called when the clickwrap data is successfully loaded and ready.
    override fun onReady(clickwrap: Clickwrap) { }

    /// Triggered when a user accepts or un-accepts a specific agreement.
    override fun onAcceptanceChange(policyId: Int, isAccepted: Boolean) { }

    /// Fired when the state of all required policies changes.
    /// Use this to enable/disable your submit button.
    override fun onAllAcceptedChange(allAccepted: Boolean) { }

    /// Called when a user views a legal agreement.
    override fun onAgreementViewed(agreementId: Int) { }

    /// Fired after consent is successfully submitted to the server.
    override fun onSubmitSuccessful(submissionPublicId: String?) { }

    /// Called when any error occurs within the Utility.
    override fun onError(error: ClickwrapError) { }

    /// Provides the result of a re-acceptance check for a returning user.
    override fun onPolicyReAcceptanceStatus(result: ReAcceptanceResult) { }
    
    // Clean up the listener when the ViewModel is destroyed to prevent memory leaks.
    override fun onCleared() {
        super.onCleared()
        SpotDraftManager.setClickwrapListener(null)
    }
}

4.2. Fetch the Clickwrap Data

Call loadClickwrap() to asynchronously fetch the agreement data from the server. This requires the ACCESS_NETWORK_STATE permission.

// Triggers the asynchronous loading of clickwrap data.
// The result will be delivered to the `onReady` or `onError` callback.
SpotDraftManager.loadClickwrap()

Add the required permission to your AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

4.3. Handle the onReady Callback

When the data is loaded, the onReady method of your listener will be called with the Clickwrap data. This is your cue to update the UI.

override fun onReady(clickwrap: Clickwrap) {
    // The clickwrap data is now available.
    // Update your view's state to render the UI.
    _uiState.value = _uiState.value.copy(clickwrapData = clickwrap, isLoading = false)
}

5. Render the User Interface

The Utility is headless, giving you complete control over the UI. Use the displayType property of the Clickwrap object to determine how to render the agreements.

Below is a sample PolicyRow Composable you can add to your project to display a single policy.

// You can add this Composable to your project's UI components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.app.android.clickwrap.domain.models.Policy

@Composable
fun PolicyRow(
    policy: Policy,
    onToggle: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = policy.isAccepted,
            onCheckedChange = { onToggle(policy.id) }
        )
        Spacer(Modifier.width(8.dp))
        // The policy.content is HTML. For a production app, use a component
        // that can render HTML to handle links and formatting.
        Text(text = policy.content)
    }
}

You can then use this PolicyRow in your screen:

// In your Composable screen
@Composable
private fun RenderClickwrapContent(clickwrap: Clickwrap, viewModel: YourViewModel) {
    // Determine the UI based on the display type from the server.
    when (clickwrap.displayType) {
        DisplayType.SINGLE_CHECKBOX, DisplayType.MULTIPLE_CHECKBOXES -> {
            // Render a list of policies with checkboxes.
            clickwrap.policies.forEach { policy ->
                PolicyRow(
                    policy = policy,
                    onToggle = { policyId -> viewModel.togglePolicy(policyId) }
                )
            }
        }
        // ... other cases
    }
}

Important Note on UI Control

The SpotDraft Clickwrap Utility is headless — it provides only core functionality for managing agreements and handling events. For advanced UI, such as rendering clickable links within the policy text, refer to the PolicyRow implementation in the accompanying demo application, which contains a more sophisticated example.

6. Handle User Interactions

Notify the SpotDraftManager whenever a user interacts with a policy or views an agreement.

/// Call this when a user taps a checkbox or toggle.
fun togglePolicy(policyId: Int) {
    SpotDraftManager.togglePolicyAcceptance(policyId = policyId)
}

/// Call this when a user navigates to view the full legal text of an agreement.
fun viewAgreement(agreementId: Int) {
    SpotDraftManager.markAgreementAsViewed(agreementId = agreementId)
}

7. Submit User Consent

7.1. Manage Submit Button State

Use the onAllAcceptedChange callback to dynamically enable or disable your form's submit button.

override fun onAllAcceptedChange(allAccepted: Boolean) {
    // Update a state variable bound to the submit button's enabled property.
    _uiState.value = _uiState.value.copy(canSubmit = allAccepted)
}

7.2. Submit and Handle the Response

When the user is ready to proceed, call submitAcceptance(userIdentifier:) to record their consent.

What is a userIdentifier? This must be a stable and unique string that identifies the user, such as their email address or a permanent user ID from your database (e.g., a UUID). This is crucial for associating the consent record with the correct user.

/// Call this when the user taps the final submit/continue button.
fun submitConsent(userIdentifier: String) {
    SpotDraftManager.submitAcceptance(userIdentifier = userIdentifier)
}

// The listener method below will be called upon a successful submission.
override fun onSubmitSuccessful(submissionPublicId: String?) {
    // Consent has been recorded.
    // You can now navigate to the next screen or complete the action.
    Log.d("ViewModel", "Submission successful with ID: ${submissionPublicId ?: "N/A"}")
}

8. Handle Re-acceptance for Returning Users

For users who have previously given consent, check if they need to re-accept updated policies.

/// For a returning user, call this method to check their status.
fun checkReAcceptance(userIdentifier: String) {
    SpotDraftManager.checkForPolicyReAcceptance(userIdentifier = userIdentifier)
}

// The listener method below will handle the result.
override fun onPolicyReAcceptanceStatus(result: ReAcceptanceResult) {
    when (result) {
        is ReAcceptanceResult.Required -> {
            // The user must re-accept.
            // Show the clickwrap UI with the new `clickwrap` object.
            _uiState.value = _uiState.value.copy(
                clickwrapData = result.clickwrap,
                showReAcceptanceDialog = true
            )
        }

        is ReAcceptanceResult.NotRequired -> {
            // The user is up-to-date. No action needed.
            Log.d("ViewModel", "User has already accepted the latest policies.")
            // Proceed directly into the app.
        }

        is ReAcceptanceResult.Error -> {
            // An error occurred during the check.
            _uiState.value = _uiState.value.copy(error = result.error.localizedMessage)
        }
    }
}

Demo Tour

A complete walkthrough of the SpotDraft Clickwrap Utility demo app, showcasing the full consent lifecycle — from initial registration to post-login policy re-acceptance.

SpotDraft Clickwrap Android Utility Demo

Application Flow

1. Register Screen — New User Onboarding

  • Show Policies: Displays required agreements (Terms of Service, Privacy Policy) as interactive checkboxes.
  • Accept & Submit: "Sign Up" enabled only after consent; recorded using the user’s email.
  • Test Configurations: Config Panel allows switching clickwrapId and testing different policy setups instantly.

2. Login Screen — Authentication

  • Enter Credentials: Email and password login.
  • Navigate to Home: Successful login redirects to Home Screen.

3. Home Screen — Policy Re-Acceptance

  • Check for Updates: Automatically verifies if the user must accept updated policies.
  • Handle Results:
    • No updates → user continues normally.
    • Updates available → dialog prompts acceptance before proceeding.

API Reference

SpotDraftManager

The main singleton for interacting with the Utility.

Method Description
initialize(application, config) (Required) Configures and initializes the manager. Must be called once.
setClickwrapListener(listener) Sets the object that will receive callbacks for clickwrap events.
loadClickwrap() Asynchronously fetches the clickwrap configuration from the SpotDraft API.
togglePolicyAcceptance(policyId) Toggles the acceptance state of a specific policy.
markAgreementAsViewed(agreementId) Marks a legal agreement as having been viewed by the user.
submitAcceptance(userIdentifier) Submits the collected consent to the SpotDraft API.
checkForPolicyReAcceptance(userIdentifier) Checks if a returning user needs to re-accept updated policies.
getClickwrap() -> Clickwrap? Synchronously returns the currently loaded Clickwrap object, if available.
isAllAccepted() -> Bool Synchronously returns true if all required policies are currently accepted.
shutdown() Shuts down the manager and releases all resources. Call this when the Utility is no longer needed.

ClickwrapListener

An interface for receiving events from the SpotDraftManager. All methods are called on the Main thread.

Method Description
onReady(clickwrap) Called when the Clickwrap data has been successfully loaded.
onAcceptanceChange(policyId, isAccepted) Called when a single policy's acceptance state changes.
onAllAcceptedChange(allAccepted) Called when the overall acceptance status of all required policies changes.
onAgreementViewed(agreementId) Called when an agreement has been marked as viewed.
onSubmitSuccessful(submissionPublicId) Called after consent has been successfully submitted to the API.
onError(error: ClickwrapError) Called when any error occurs within the Utility.
onPolicyReAcceptanceStatus(result) Called with the result of a checkForPolicyReAcceptance call.

Architecture Overview

The Utility follows a clean, unidirectional data flow, ensuring a predictable and maintainable state management lifecycle.

sequenceDiagram
    participant User
    box Host App
        participant HostAppUI as Host App UI (Composable)
        participant HostAppLogic as Host App Logic (ViewModel)
    end
    box SpotDraftUtility
        participant Manager as SpotDraftManager
        participant Service
        participant Repository
    end
    participant API as SpotDraft API

    Note over User, API: Phase 1: Initialization & Loading
    HostAppLogic->>Manager: initialize(config)
    HostAppLogic->>Manager: setClickwrapListener(self)
    User->>HostAppUI: Navigates to screen
    HostAppUI->>HostAppLogic: onAppear -> loadClickwrap()
    HostAppLogic->>Manager: loadClickwrap()
    Manager->>Service: loadClickwrap()
    Service->>Repository: fetchClickwrapData()
    Repository->>API: GET /clickwrap/{id}
    API-->>Repository: Returns Clickwrap JSON
    Repository-->>Service: Returns mapped Clickwrap model
    Service-->>Manager: Returns Clickwrap model
    Manager->>HostAppLogic: onReady(clickwrap)
    HostAppLogic->>HostAppUI: Updates UI with policies

    Note over User, API: Phase 2: User Interaction
    User->>HostAppUI: Taps a policy checkbox
    HostAppUI->>HostAppLogic: togglePolicy(policyId)
    HostAppLogic->>Manager: togglePolicyAcceptance(policyId)
    Manager->>Service: Updates policy state
    Manager->>HostAppLogic: onAcceptanceChange(...)
    Manager->>HostAppLogic: onAllAcceptedChange(...)
    HostAppLogic->>HostAppUI: Updates checkbox and submit button state

    Note over User, API: Phase 3: Consent Submission
    User->>HostAppUI: Taps 'Submit' button
    HostAppUI->>HostAppLogic: submitConsent(userIdentifier)
    HostAppLogic->>Manager: submitAcceptance(userIdentifier)
    alt Submission Succeeded
        Manager->>Service: submitAcceptance(...)
        Service->>Repository: POST /execute with consent data
        Repository-->>API: Returns Success (200 OK)
        API-->>Repository: Returns submissionPublicId
        Repository-->>Service: Returns submissionPublicId
        Service-->>Manager: Returns submissionPublicId
        Manager->>HostAppLogic: onSubmitSuccessful(submissionPublicId)
        HostAppLogic->>HostAppUI: Navigates to next screen
    else Submission Failed
        Manager->>Service: submitAcceptance(...)
        Service->>Repository: Throws ClickwrapError
        Manager->>HostAppLogic: onError(error)
        HostAppLogic->>HostAppUI: Displays error message
    end

    Note over User, API: Phase 4: Re-acceptance Check (Returning User)
    User->>HostAppUI: Logs in or returns to app
    HostAppUI->>HostAppLogic: onAppear -> checkForReAcceptance(userIdentifier)
    HostAppLogic->>Manager: checkForPolicyReAcceptance(userIdentifier)
    Manager->>Service: checkForReAcceptance(...)
    Service->>Repository: GET /re-acceptance-status?user={id}
    API-->>Repository: Returns Re-acceptance JSON
    Repository-->>Service: Mapped ReAcceptanceResult
    Service-->>Manager: Returns ReAcceptanceResult
    Manager->>HostAppLogic: onPolicyReAcceptanceStatus(result)
    alt Re-acceptance Required
        HostAppLogic->>HostAppUI: Shows re-acceptance view with new policies
        Note over HostAppUI, Manager: Flow continues to Phase 2 & 3
    else Re-acceptance Not Required
        HostAppLogic->>HostAppUI: Allows user to proceed
    else Error
        HostAppLogic->>HostAppUI: Displays error message
    end
Loading

Error Handling and Edge Cases

Proper error handling is crucial for a good user experience. The Utility provides a typed ClickwrapError sealed class to make this easy.

Error When It Occurs How to Handle
NotInitialized Calling a Utility method before initialize(). Ensure initialize() is called successfully at app launch. This is a programmer error.
NetworkUnavailable The device has no internet connection. Display a "No Internet" message and provide a "Retry" button that calls loadClickwrap() again.
ApiError The SpotDraft API returned an error (e.g., 404, 500). Log the error for debugging. If it's a 404, your clickwrapId is likely wrong. Otherwise, show a generic error.
PoliciesNotAccepted Calling submitAcceptance() before all required policies are accepted. This should be prevented by disabling the submit button. Use the onAllAcceptedChange listener for this.
DecodingError / InvalidResponse The data from the API was malformed or missing fields. This usually indicates an issue with the Utility or API. Log the error and show a generic failure message.

Best Practices

  • Initialize Once: Call SpotDraftManager.initialize() only once when your application starts. Your Application class onCreate() is the perfect place.
  • Use a Stable userIdentifier: When calling submitAcceptance or checkForPolicyReAcceptance, use a persistent and unique identifier for the user, such as their email address or a database UUID. Avoid using temporary or changing values.
  • Centralize Logic in a ViewModel: Do not call the Utility directly from your Composable functions. Use a ViewModel to manage state, implement ClickwrapListener, and handle all interactions with the SpotDraftManager.
  • Provide Clear Feedback: Always show loading indicators (CircularProgressIndicator) during network operations and display clear error messages to the user when something goes wrong.
  • Secure Your Configuration: Avoid hardcoding your clickwrapId directly in your source code. Use a secure method like storing it in local.properties and accessing it via BuildConfig fields.

Troubleshooting & FAQ

Question / Issue Suggested Solution
onReady callback is never fired. 1. Set enableLogging: true in ClickwrapConfig and check Logcat for errors.
2. Verify your clickwrapId and baseURL are correct.
3. Check the device's network connectivity.
4. Ensure your domain (if used) is whitelisted in your SpotDraft dashboard.
ClickwrapError.PoliciesNotAccepted on submit. This error means submitAcceptance() was called before all required policies were accepted. Use the onAllAcceptedChange callback to dynamically control your submit button's enabled state.
ClickwrapError.ApiError (e.g., 404 Not Found). This is almost always caused by an incorrect clickwrapId. Double-check the ID in your SpotDraft dashboard.
Links in policy text are not tappable. The policy.content is HTML. You must render it in a view that supports it. For Jetpack Compose, you will need a custom solution or a library to parse the HTML and handle clicks on <a> tags. The demo application contains a complete example of this.

Contact & Support

About

The SpotDraft Clickwrap Android Utility is a native Kotlin/Java library designed to provide a robust and seamless bridge between any Android application and SpotDraft's powerful Clickwrap backend.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages