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

Repository files navigation

SpotDraft Logo

SpotDraft Clickwrap Android Java Utils

A lightweight, headless, and modern Java/Kotlin library for integrating legally-binding SpotDraft Clickwrap agreements into any Android application. This guide focuses on Java implementation.

Gradle 8.2+ Java 11+ Android SDK 21+ License: MIT Version 1.0.0


Overview

The SpotDraft Clickwrap Android Utils offers a clean, secure, and scalable way to embed legally-compliant consent flows directly into your native Android applications.
Built as a headless Utils, it manages backend communication, data normalization, and state handling — allowing you to focus on delivering seamless user experiences 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 Utils 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 Utils?

Integrating legal agreements can be time-consuming, error-prone, and repetitive.
This Utils 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 Utils handles logic and data, while you retain full control over UI implementation.
Modern & Asynchronous Built using Kotlin Coroutines, but provides a Java-friendly API for safe, performant, and non-blocking execution.
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 class for predictable, structured, and efficient error management.
Minimal Dependencies Relies on androidx.core, kotlinx-coroutines, Gson, and OkHttp for core functionality, ensuring a focused and efficient footprint.
Re-Acceptance Logic Automatically validates if returning users must re-accept updated terms, ensuring compliance with evolving legal requirements.

System Requirements

Requirement Minimum Version
Android SDK 21+
Java 11+
Gradle 8.1+

Compatibility Notes

This Utils is designed to work seamlessly with:

  • Native Android projects (View-based UI supported)
  • Gradle-based build systems
  • Modern Android architectures (MVVM, MVI, Clean Architecture)

It relies on a minimal set of well-established third-party libraries, ensuring compatibility and stability across enterprise-level applications.

Integrating SpotDraft's Android Clickwrap Utils: A Step-by-Step Guide (Java)

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


1. Add the SpotDraftClickwrap Utility to Your Project

  1. Download: Get the spotdraftclickwrap utility.
  2. Unzip the spotdraftclickwrap zip file.
  3. Your project's settings.gradle.kts and app/build.gradle.kts files already correctly include the spotdraftclickwrap module. No further action is needed here.

settings.gradle.kts (already configured):

// ...
rootProject.name = "SpotDraftClickwrapJava"
include(":app")
include(":spotdraftclickwrap") // This line ensures the module is included

app/build.gradle.kts (already configured):

// ...
dependencies {
    // ... other dependencies
    implementation(project(":spotdraftclickwrap")) // This line adds the module as a dependency
    // ...
}

Kotlin Interoperability Setup (already configured): Your app/build.gradle.kts also correctly includes the Kotlin Android plugin and sets the JVM target, which is essential for seamless interoperability between your Java code and the Kotlin-based spotdraftclickwrap library.

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android) // Ensures Kotlin support
    alias(libs.plugins.kotlin.serialization)
}

android {
    // ...
    kotlinOptions {
        jvmTarget = "11" // Specifies JVM target for Kotlin compilation
    }
}

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 Utils

The SpotDraftManager is a singleton that manages the entire clickwrap lifecycle. Initialize it once when your application launches, typically in your Application class's onCreate() method or directly define in screen or viewmodel where you want to load agreements.

// In your Application class (e.g., SpotDraftApp.java)
package com.app.spotdraft; // Adjust package name as needed

import android.app.Application;
import android.util.Log;

import com.app.android.clickwrap.config.ClickwrapConfig;
import com.app.android.clickwrap.core.SpotDraftManager;

import java.net.MalformedURLException;

public class SpotDraftApp extends Application {
    private static final String TAG = "SpotDraftApp";

    @Override
    public void onCreate() {
        super.onCreate();
        // It's recommended to load configuration from a secure source
        // or user preferences, not hardcode it.
        String clickwrapId = "YOUR_CLICKWRAP_ID"; // Replace with your actual ID
        String baseURL = "https://api.spotdraft.com";
        String domain = "your-app-domain.com"; // A unique domain for your application.

        try {
            // ClickwrapConfig is a Kotlin data class, but its constructor is accessible from Java.
            ClickwrapConfig config = new ClickwrapConfig(
                clickwrapId,
                getPackageName(), // Or a unique identifier for your app
                baseURL,
                domain,
                true // enableLogging: Set to false for production
            );
            
            // SpotDraftManager is a Kotlin object (singleton), accessible via its INSTANCE field from Java.
            SpotDraftManager.INSTANCE.initialize(
                this, // Pass the Application instance
                config
            );
        } catch (MalformedURLException e) {
            Log.e(TAG, "Failed to initialize SpotDraftManager: Invalid Base URL", e);
            // Handle this critical error, e.g., show a user-friendly message
        } catch (Exception e) {
            Log.e(TAG, "Failed to initialize SpotDraftManager", e);
            // Handle other initialization errors
        }
    }
}

Important Note on Kotlin Interoperability:

  • Kotlin object declarations (like SpotDraftManager) are accessed from Java using INSTANCE.
  • Kotlin data class constructors (like ClickwrapConfig) are directly usable from Java.

4. Load Clickwrap Agreements

4.1. Set the Event Listener

To receive data and events from the Utils, your ViewModel or Activity/Fragment must implement the ClickwrapListener interface.

// Make your class implement the listener interface.
package com.app.spotdraft.viewmodel; // Adjust package name as needed

import android.app.Application;
import android.util.Log;

import androidx.lifecycle.ViewModel;

import com.app.android.clickwrap.callback.ClickwrapListener;
import com.app.android.clickwrap.core.SpotDraftManager;
import com.app.android.clickwrap.domain.models.Clickwrap;
import com.app.android.clickwrap.domain.models.ReAcceptanceResult;

// Example ViewModel in Java
public class YourViewModel extends ViewModel implements ClickwrapListener {

    private static final String TAG = "YourViewModel";
    private Application appContext;

    public YourViewModel(Application appContext) {
        this.appContext = appContext;
        // Register this class as the listener for clickwrap events.
        // This should be done after SpotDraftManager has been initialized.
        SpotDraftManager.INSTANCE.setClickwrapListener(this);
    }

    // MARK: - ClickwrapListener Callbacks

    /// Called when the clickwrap data is successfully loaded and ready.
    @Override
    public void onReady(Clickwrap clickwrap) {
        Log.d(TAG, "Clickwrap data ready: " + clickwrap.getClickwrapId());
        // Update UI state, e.g., via LiveData or other observable patterns
        // _clickwrapLiveData.postValue(clickwrap);
    }

    /// Triggered when a user accepts or un-accepts a specific agreement.
    @Override
    public void onAcceptanceChange(int policyId, boolean isAccepted) {
        Log.d(TAG, "Policy " + policyId + " acceptance changed: " + isAccepted);
        // Update UI state
    }

    /// Fired when the state of all required policies changes.
    /// Use this to enable/disable your submit button.
    @Override
    public void onAllAcceptedChange(boolean allAccepted) {
        Log.d(TAG, "All policies accepted status: " + allAccepted);
        // Update UI state, e.g., enable/disable submit button
        // _canSubmitLiveData.postValue(allAccepted);
    }

    /// Called when a user views a legal agreement.
    @Override
    public void onAgreementViewed(int agreementId) {
        Log.d(TAG, "Agreement " + agreementId + " viewed.");
        // Update UI state
    }

    /// Fired after consent is successfully submitted to the server.
    @Override
    public void onSubmitSuccessful(String submissionPublicId) {
        Log.d(TAG, "Submission successful with ID: " + submissionPublicId);
        // Handle success, e.g., navigate to next screen
    }

    /// Called when any error occurs within the Utils.
    @Override
    public void onError(Throwable error) {
        Log.e(TAG, "Clickwrap error: " + error.getMessage(), error);
        // Handle error, e.g., show a toast or dialog
    }

    /// Provides the result of a re-acceptance check for a returning user.
    @Override
    public void onPolicyReAcceptanceStatus(ReAcceptanceResult result) {
        Log.d(TAG, "Re-acceptance status: " + result.getClass().getSimpleName());
        // Handle re-acceptance status
    }
}

4.2. Fetch the Clickwrap Data

Call loadClickwrap() to asynchronously fetch the agreement data from the server.

// In your ViewModel or Activity/Fragment
// Triggers the asynchronous loading of clickwrap data.
// The result will be delivered to the `onReady` or `onError` callback.
SpotDraftManager.INSTANCE.loadClickwrap();

Important Note on Kotlin Interoperability:

  • Kotlin functions that are suspend functions (like loadClickwrap()) are exposed to Java as methods that return Object and take a Continuation as the last parameter. However, SpotDraftManager provides Java-friendly wrappers for these methods that handle the coroutine execution, allowing you to call them directly without dealing with Continuation.

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.

// In your ViewModel (as shown in 4.1)
@Override
public void onReady(Clickwrap clickwrap) {
    // The clickwrap data is now available.
    // Update your view's state to render the UI.
    // For example, using LiveData:
    // _clickwrapLiveData.postValue(clickwrap);
}

5. Render the User Interface

The Utils is headless, giving you complete control over the UI. Use the getDisplayType() property of the Clickwrap object to determine how to render the agreements.

// In your Activity or Fragment (e.g., in HomeActivity.java)
package com.app.spotdraft.screen; // Adjust package name as needed

import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;

import com.app.android.clickwrap.domain.models.Clickwrap;
import com.app.android.clickwrap.domain.models.Policy;
import com.app.android.clickwrap.enums.DisplayType;
import com.app.spotdraft.R; // Assuming your R file is here
import com.app.spotdraft.viewmodel.YourViewModel; // Your ViewModel

import java.util.List;

public class HomeActivity extends AppCompatActivity {

    private YourViewModel viewModel;
    private LinearLayout policiesContainer; // Assuming a LinearLayout to add policies dynamically
    private TextView inlinePolicyTextView; // For DisplayType.INLINE

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home); // Assuming you have an activity_home.xml layout

        policiesContainer = findViewById(R.id.policies_container); // Add this ID to your layout
        inlinePolicyTextView = findViewById(R.id.inline_policy_text_view); // Add this ID to your layout

        viewModel = new ViewModelProvider(this).get(YourViewModel.class);

        // Observe LiveData from ViewModel to update UI
        // Example: if your ViewModel exposes a LiveData<Clickwrap>
        // viewModel.getClickwrapLiveData().observe(this, this::renderClickwrapContent);

        // Manually trigger load if not done in ViewModel init
        // SpotDraftManager.INSTANCE.loadClickwrap();
    }

    private void renderClickwrapContent(Clickwrap clickwrap) {
        if (clickwrap == null) {
            return;
        }

        policiesContainer.removeAllViews(); // Clear previous views
        inlinePolicyTextView.setVisibility(View.GONE); // Hide inline text by default

        DisplayType displayType = clickwrap.getDisplayType();
        List<Policy> policies = clickwrap.getPolicies();

        if (policies == null || policies.isEmpty()) {
            // Handle no policies case
            TextView noPoliciesText = new TextView(this);
            noPoliciesText.setText("No policies to display.");
            policiesContainer.addView(noPoliciesText);
            return;
        }

        if (displayType == DisplayType.SINGLE_CHECKBOX || displayType == DisplayType.MULTIPLE_CHECKBOXES) {
            for (Policy policy : policies) {
                // Create a custom view for each policy, similar to PolicyRow in Kotlin
                View policyView = LayoutInflater.from(this).inflate(R.layout.item_policy, policiesContainer, false); // Assuming item_policy.xml
                CheckBox policyCheckbox = policyView.findViewById(R.id.policy_checkbox); // Add this ID to item_policy.xml
                TextView policyTitle = policyView.findViewById(R.id.policy_title); // Add this ID to item_policy.xml
                TextView policyContent = policyView.findViewById(R.id.policy_content); // Add this ID to item_policy.xml

                policyTitle.setText(policy.getTitle());
                // For HTML content, use Html.fromHtml
                policyContent.setText(Html.fromHtml(policy.getContent(), Html.FROM_HTML_MODE_COMPACT));
                policyCheckbox.setChecked(policy.isAccepted());

                policyCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
                    // Notify the manager of the change
                    viewModel.togglePolicyAcceptance(policy.getPolicyId());
                });

                // Optionally, handle marking agreement as viewed when content is displayed
                // viewModel.markAgreementAsViewed(policy.getAgreementId());

                policiesContainer.addView(policyView);
            }
        } else if (displayType == DisplayType.INLINE) {
            // For implied consent, render the inline text.
            Policy firstPolicy = policies.get(0);
            if (firstPolicy != null) {
                inlinePolicyTextView.setVisibility(View.VISIBLE);
                inlinePolicyTextView.setText(Html.fromHtml(firstPolicy.getContent(), Html.FROM_HTML_MODE_COMPACT));
                // Mark as viewed if it's an inline policy
                // viewModel.markAgreementAsViewed(firstPolicy.getAgreementId());
            }
        } else if (displayType == DisplayType.UNKNOWN) {
            TextView unsupportedText = new TextView(this);
            unsupportedText.setText("Unsupported clickwrap format.");
            policiesContainer.addView(unsupportedText);
        }
    }
}

Important Note on UI Control

The SpotDraft Clickwrap Utils is headless — it provides only core functionality for managing agreements and handling events.
It does not include any UI components, allowing you to build a custom experience that fits your app’s design.
Example: You would create your own layout files (e.g., item_policy.xml and PolicyAdapter.java) and inflate them.

6. Handle User Interactions

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

// In your ViewModel (e.g., YourViewModel.java)
package com.app.spotdraft.viewmodel; // Adjust package name as needed

import android.util.Log;

import androidx.lifecycle.ViewModel;

import com.app.android.clickwrap.core.SpotDraftManager;

import kotlin.Unit;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.CoroutineContext;
import kotlin.coroutines.EmptyCoroutineContext;

public class YourViewModel extends ViewModel implements ClickwrapListener {
    // ... (constructor and other methods as above)

    public void togglePolicyAcceptance(int policyId) {
        // SpotDraftManager.INSTANCE.togglePolicyAcceptance is a suspend function.
        // It's exposed to Java as a method taking a Continuation.
        // For simplicity, you can often call it directly if it's designed with Java interop in mind
        // or wrap it in a coroutine scope if you need to handle its completion.
        // The SpotDraftManager methods are designed to be callable directly from Java.
        try {
            SpotDraftManager.INSTANCE.togglePolicyAcceptance(policyId);
        } catch (Exception e) {
            Log.e(TAG, "Error toggling policy acceptance: " + e.getMessage(), e);
            // Handle error
        }
    }

    public void markAgreementAsViewed(int agreementId) {
        try {
            SpotDraftManager.INSTANCE.markAgreementAsViewed(agreementId);
        } catch (Exception e) {
            Log.e(TAG, "Error marking agreement as viewed: " + e.getMessage(), e);
            // Handle error
        }
    }
}

Important Note on Kotlin Interoperability:

  • Kotlin suspend functions are often exposed to Java with a Continuation parameter. However, the SpotDraftManager methods like togglePolicyAcceptance and markAgreementAsViewed are likely designed to be directly callable from Java, handling the coroutine execution internally for convenience. If you encounter issues, you might need to wrap them in a CoroutineScope if the library doesn't provide direct Java wrappers.

7. Submit User Consent

7.1. Manage Submit Button State

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

// In your ViewModel (e.g., YourViewModel.java)
package com.app.spotdraft.viewmodel; // Adjust package name as needed

import androidx.lifecycle.MutableLiveData;

public class YourViewModel extends ViewModel implements ClickwrapListener {
    // ... (constructor and other methods)

    private MutableLiveData<Boolean> canSubmitLiveData = new MutableLiveData<>();

    public MutableLiveData<Boolean> getCanSubmitLiveData() {
        return canSubmitLiveData;
    }

    @Override
    public void onAllAcceptedChange(boolean allAccepted) {
        // Update a LiveData variable bound to the submit button's enabled property.
        canSubmitLiveData.postValue(allAccepted);
    }
}

Then, in your Activity/Fragment:

// In your Activity or Fragment (e.g., HomeActivity.java)
// ...
import android.widget.Button;

public class HomeActivity extends AppCompatActivity {
    // ...
    private Button submitButton; // Add this ID to your layout

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ...
        submitButton = findViewById(R.id.submit_button); // Add this ID to your layout

        viewModel.getCanSubmitLiveData().observe(this, canSubmit -> {
            submitButton.setEnabled(canSubmit);
        });

        submitButton.setOnClickListener(v -> {
            // Replace with actual user identifier
            viewModel.submitConsent("user@example.com");
        });
    }
    // ...
}

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.

// In your ViewModel (e.g., YourViewModel.java)
package com.app.spotdraft.viewmodel; // Adjust package name as needed

import android.util.Log;

import com.app.android.clickwrap.core.SpotDraftManager;

public class YourViewModel extends ViewModel implements ClickwrapListener {
    // ... (constructor and other methods)

    public void submitConsent(String userIdentifier) {
        try {
            SpotDraftManager.INSTANCE.submitAcceptance(userIdentifier);
        } catch (Exception e) {
            Log.e(TAG, "Error submitting consent: " + e.getMessage(), e);
            // Handle error
        }
    }

    // The listener method below will be called upon a successful submission.
    @Override
    public void onSubmitSuccessful(String submissionPublicId) {
        // Consent has been recorded.
        // You can now navigate to the next screen or complete the action.
        Log.d(TAG, "Submission successful with ID: " + submissionPublicId);
        // _submissionSuccessLiveData.postValue(submissionPublicId);
    }
}

8. Handle Re-acceptance for Returning Users

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

// In your ViewModel (e.g., YourViewModel.java)
package com.app.spotdraft.viewmodel; // Adjust package name as needed

import android.util.Log;

import androidx.lifecycle.MutableLiveData;

import com.app.android.clickwrap.core.SpotDraftManager;
import com.app.android.clickwrap.domain.models.ReAcceptanceResult;

public class YourViewModel extends ViewModel implements ClickwrapListener {
    // ... (constructor and other methods)

    private MutableLiveData<Clickwrap> clickwrapLiveData = new MutableLiveData<>();
    private MutableLiveData<Boolean> showReAcceptanceDialogLiveData = new MutableLiveData<>();
    private MutableLiveData<String> errorMessageLiveData = new MutableLiveData<>();

    public MutableLiveData<Clickwrap> getClickwrapLiveData() { return clickwrapLiveData; }
    public MutableLiveData<Boolean> getShowReAcceptanceDialogLiveData() { return showReAcceptanceDialogLiveData; }
    public MutableLiveData<String> getErrorMessageLiveData() { return errorMessageLiveData; }


    public void checkReAcceptance(String userIdentifier) {
        try {
            SpotDraftManager.INSTANCE.checkForPolicyReAcceptance(userIdentifier);
        } catch (Exception e) {
            Log.e(TAG, "Error checking re-acceptance: " + e.getMessage(), e);
            errorMessageLiveData.postValue(e.getMessage());
            // Handle error
        }
    }

    // The listener method below will handle the result.
    @Override
    public void onPolicyReAcceptanceStatus(ReAcceptanceResult result) {
        if (result instanceof ReAcceptanceResult.Required) {
            // The user must re-accept.
            // Show the clickwrap UI with the new `clickwrap` object.
            ReAcceptanceResult.Required requiredResult = (ReAcceptanceResult.Required) result;
            clickwrapLiveData.postValue(requiredResult.getClickwrap());
            showReAcceptanceDialogLiveData.postValue(true);
        } else if (result instanceof ReAcceptanceResult.NotRequired) {
            // The user is up-to-date. No action needed.
            Log.d(TAG, "User has already accepted the latest policies.");
            // Proceed directly into the app.
            showReAcceptanceDialogLiveData.postValue(false); // Hide any dialog
        } else if (result instanceof ReAcceptanceResult.Error) {
            // An error occurred during the check.
            ReAcceptanceResult.Error errorResult = (ReAcceptanceResult.Error) result;
            errorMessageLiveData.postValue(errorResult.getError().getMessage());
        }
    }
}

Important Note on Kotlin Interoperability:

  • Kotlin sealed class hierarchies (like ReAcceptanceResult) are exposed to Java as abstract classes with concrete subclasses. You use instanceof to check the type and then cast to the specific subclass to access its properties (e.g., ReAcceptanceResult.Required.getClickwrap()).

API Reference (Java Perspective)

SpotDraftManager

The main singleton for interacting with the utility. Accessed via SpotDraftManager.INSTANCE.

Method Description
initialize(application: Application, config: ClickwrapConfig) (Required) Configures and initializes the manager. Must be called once before any other method.
setClickwrapListener(listener: ClickwrapListener) Sets the object that will receive callbacks for clickwrap events.
loadClickwrap() Asynchronously fetches the clickwrap configuration from the SpotDraft API.
togglePolicyAcceptance(policyId: int) Toggles the acceptance state of a specific policy.
markAgreementAsViewed(agreementId: int) Marks a legal agreement as having been viewed by the user.
submitAcceptance(userIdentifier: String) Submits the collected consent to the SpotDraft API.
checkForPolicyReAcceptance(userIdentifier: String) Checks if a returning user needs to re-accept updated policies.
getClickwrap(): Clickwrap Synchronously returns the currently loaded Clickwrap object, if available. Returns null if not loaded.
isAllAccepted(): boolean Synchronously returns true if all required policies are currently in an accepted state.
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: Clickwrap) Called when the Clickwrap data has been successfully loaded.
onAcceptanceChange(policyId: int, isAccepted: boolean) Called when a single policy's acceptance state changes.
onAllAcceptedChange(allAccepted: boolean) Called when the overall acceptance status of all required policies changes.
onAgreementViewed(agreementId: int) Called when an agreement has been marked as viewed.
onSubmitSuccessful(submissionPublicId: String) Called after consent has been successfully submitted to the API.
onError(error: Throwable) Called when any error occurs within the utility.
onPolicyReAcceptanceStatus(result: ReAcceptanceResult) Called with the result of a checkForPolicyReAcceptance call.

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. The demo app is written in Kotlin, but the flow and concepts are directly applicable to a Java implementation.

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.

How to Run the Demo

  1. Clone this repository.
  2. Open the project in Android Studio (version Hedgehog | 2023.1.1+ recommended).
  3. Ensure your local.properties file (in the project root) is correctly configured for your Android SDK location.
  4. Select the app module and a device/emulator.
  5. Build and Run (Shift+F10 or Run button).

Architecture Overview

The utility follows a clean, unidirectional data flow, ensuring a predictable and maintainable state management lifecycle. The diagram below illustrates the flow, which remains consistent whether your host app is in Kotlin or Java.

sequenceDiagram
    participant User
    box Host App
        participant HostAppUI as Host App UI (Activity/Fragment)
        participant HostAppLogic as Host App Logic (ViewModel)
    end
    box SpotDraft Clickwrap Utils
        participant Manager as SpotDraftManager
        participant Service as ClickwrapService
        participant Repository as ClickwrapRepository
    end
    participant API as SpotDraft API

    Note over User, API: Phase 1: Initialization & Loading
    HostAppLogic->>Manager: initialize(application, config)
    HostAppLogic->>Manager: setClickwrapListener(this)
    User->>HostAppUI: Navigates to screen
    HostAppUI->>HostAppLogic: Calls 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: Calls checkForReAcceptance(userIdentifier)
    HostAppLogic->>Manager: checkForPolicyReAcceptance(userIdentifier)
    Manager->>Service: checkForReAcceptance(...)
    Service->>Repository: GET /re-acceptance-status?user={id}
    Repository->>API: Returns Re-acceptance JSON
    API-->>Repository: Mapped ReAcceptanceResult
    Repository-->>Service: Returns 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 (Java Perspective)

Proper error handling is crucial for a good user experience. The Utils provides a typed ClickwrapError class to make this easy. You will handle these errors in the onError callback and try-catch blocks.

Error When It Occurs How to Handle
notInitialized Calling an Utils 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 to the user 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 (check Logcat). 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. This usually indicates an issue with the Utils or API. Log the error (check Logcat) and show a generic failure message.

Best Practices (Java Perspective)

  • Initialize Once: Call SpotDraftManager.INSTANCE.initialize() only once when your application starts. Your Application class's 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 Utils directly from your Activity/Fragment. Use a ViewModel to manage state, implement ClickwrapListener, and handle all interactions with the SpotDraftManager. Use LiveData or other observable patterns to communicate state changes to your UI.
  • Provide Clear Feedback: Always show loading indicators 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 Gradle buildConfigField or a secrets plugin to store sensitive credentials.

Troubleshooting & FAQ (Java Perspective)

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 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 enable/disable your submit button.
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 HTML, such as a WebView or a TextView with Html.fromHtml(). You might need to configure TextView to handle link clicks.
Kotlin interoperability issues (e.g., Continuation errors). Ensure your app/build.gradle.kts includes alias(libs.plugins.kotlin.android) and kotlinOptions { jvmTarget = "11" }. Most SpotDraftManager methods are designed for direct Java calls, but for complex suspend functions, you might need to wrap them in a CoroutineScope if direct calls don't work as expected.

Contact & Support

  • For issues with the Utils, please open a GitHub issue.
  • For questions about your SpotDraft account, please contact [support @spotdraft.com](mailto:support @spotdraft.com).

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

Releases

No releases published

Packages

No packages published