A lightweight, headless, and modern Swift library for integrating legally-binding SpotDraft Clickwrap agreements into any iOS application.
The SpotDraft Clickwrap iOS Utils offers a clean, secure, and scalable way to embed legally-compliant consent flows directly into your native iOS apps.
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.
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.
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.
| 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 Swift's async/await for safe, performant, and non-blocking execution. |
| Event-Driven Architecture | Utilizes a ClickwrapListener protocol 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 enum for predictable, structured, and efficient error management. |
| Zero Dependencies | Uses only native Apple frameworks 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. |
| Requirement | Minimum Version |
|---|---|
| iOS | 15.0+ |
| Xcode | 14.0+ |
| Swift | 5.7+ |
This SDK is designed to work seamlessly with:
- Native iOS projects (UIKit and SwiftUI supported)
- Swift Package Manager integrations
- Modern iOS architectures (MVC, MVVM, VIPER, Clean Architecture)
It does not rely on third-party libraries, ensuring compatibility and stability across enterprise-level applications.
This guide provides a clear, step-by-step walkthrough for integrating the SpotDraft Clickwrap SDK into your iOS application. Follow these instructions to manage legal agreements and capture user consent seamlessly.
- Download: Get the
SpotDraftClickwraputility. - Import: Drag the downloaded
SpotDraftClickwrapfolder into your Xcode project. - Configure: In the import dialog, ensure "Copy items if needed" and "Create groups" are selected. Add the files to your main app target.
- Log In to your SpotDraft account.
- Navigate to the Clickthrough section via the side menu.
- Select an existing "Clickthrough Package" or create a new one.
- Inside the package, create and Publish at least one legal agreement.
- Go to the Clickthrough Settings tab to find your Clickwrap ID.
The SpotDraftManager is a singleton that manages the entire clickwrap lifecycle. Initialize it once when your application launches, typically in your App's init() method or class where you want to get agreements.
@main
struct YourApp: App {
init() {
do {
// 1. Configure the Utils with your specific details.
let 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",
// 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.
try SpotDraftManager.initialize(config: config)
} catch {
// If initialization fails, it's a critical error.
fatalError("Failed to initialize SpotDraftManager: \(error.localizedDescription)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}To receive data and events from the SDK, your screen or ViewModel must conform to the ClickwrapListener protocol.
// Make your class conform to the listener protocol.
class YourViewModel: ObservableObject, ClickwrapListener {
init() {
// Register this class as the listener for clickwrap events.
SpotDraftManager.setClickwrapListener(listener: self)
}
// MARK: - ClickwrapListener Callbacks
/// Called when the clickwrap data is successfully loaded and ready.
func onReady(clickwrap: Clickwrap) { }
/// Triggered when a user accepts or un-accepts a specific agreement.
func onAcceptanceChange(policyId: Int, isAccepted: Bool) { }
/// Fired when the state of all required policies changes.
/// Use this to enable/disable your submit button.
func onAllAcceptedChange(allAccepted: Bool) { }
/// Called when a user views a legal agreement.
func onAgreementViewed(agreementId: Int) { }
/// Fired after consent is successfully submitted to the server.
func onSubmitSuccessful(submissionPublicId: String?) { }
/// Called when any error occurs within the SDK.
func onError(error: ClickwrapError) { }
/// Provides the result of a re-acceptance check for a returning user.
func onPolicyReAcceptanceStatus(result: ReAcceptanceResult) { }
}Call loadClickwrap() to asynchronously fetch the agreement data from the server. and provide agreements
// Triggers the asynchronous loading of clickwrap data.
// The result will be delivered to the `onReady` or `onError` callback.
SpotDraftManager.loadClickwrap()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.
func onReady(clickwrap: Clickwrap) {
// The clickwrap data is now available.
// Update your view's state to render the UI.
self.clickwrapData = clickwrap
}The SDK is headless, giving you complete control over the UI. Use the displayType property of the Clickwrap object to determine how to render the agreements.
// In your SwiftUI View
@ViewBuilder
private func renderClickwrapContent(for clickwrap: Clickwrap) -> some View {
// Determine the UI based on the display type from the server.
switch clickwrap.displayType {
case .singleCheckbox, .multipleCheckboxes:
// Render a list of policies with checkboxes.
ForEach(clickwrap.policies) { policy in
// `PolicyRow` is your custom view for displaying a single policy.
PolicyRow(policy: policy)
}
case .inline:
// For implied consent, render the inline text.
if let policy = clickwrap.policies.first {
// Use a helper to strip HTML for simple text display.
Text(policy.content.htmlStripped())
}
case .unknown:
// Handle cases where the format is not supported.
Text("Unsupported clickwrap format.")
}
}The SpotDraft Clickwrap SDK 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: Refer to thePolicyRowin theClickwrapDemoAppfor a sample implementation of a single policy display.
Notify the SpotDraftManager whenever a user interacts with a policy or views an agreement.
/// Call this when a user taps a checkbox or toggle.
func togglePolicy(_ policyId: Int) {
SpotDraftManager.togglePolicyAcceptance(policyId: policyId)
}
/// Call this when a user navigates to view the full legal text of an agreement.
func viewAgreement(agreementId: Int) {
SpotDraftManager.markAgreementAsViewed(agreementId: agreementId)
}Use the onAllAcceptedChange callback to dynamically enable or disable your form's submit button.
func onAllAcceptedChange(allAccepted: Bool) {
// Update a state variable bound to the submit button's disabled property.
self.canSubmit = allAccepted
}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.
func submitConsent(userIdentifier: String) {
SpotDraftManager.submitAcceptance(userIdentifier: userIdentifier)
}
// The listener method below will be called upon a successful submission.
func onSubmitSuccessful(submissionPublicId: String?) {
// Consent has been recorded.
// You can now navigate to the next screen or complete the action.
print("Submission successful with ID: \(submissionPublicId ?? "N/A")")
}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.
func checkReAcceptance(userIdentifier: String) {
SpotDraftManager.checkForPolicyReAcceptance(userIdentifier: userIdentifier)
}
// The listener method below will handle the result.
func onPolicyReAcceptanceStatus(result: ReAcceptanceResult) {
switch result {
case .required(let clickwrap, _):
// The user must re-accept.
// Show the clickwrap UI with the new `clickwrap` object.
self.clickwrapData = clickwrap
self.showReAcceptanceDialog = true
case .notRequired:
// The user is up-to-date. No action needed.
print("User has already accepted the latest policies.")
// Proceed directly into the app.
case .error(let error):
// An error occurred during the check.
self.errorMessage = error.localizedDescription
}
}The main singleton for interacting with the utility.
| Method | Description |
|---|---|
initialize(config:) |
(Required) Configures and initializes the manager. Must be called once before any other method. |
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 in an accepted state. |
shutdown() |
Shuts down the manager and releases all resources. Call this when the utility is no longer needed. |
A protocol 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:) |
Called when any error occurs within the utility. |
onPolicyReAcceptanceStatus(result:) |
Called with the result of a checkForPolicyReAcceptance call. |
A complete walkthrough of the SpotDraft Clickwrap SDK demo app, showcasing the full consent lifecycle — from initial registration to post-login policy re-acceptance.
- Clone this repository.
- Open
ClickwrapDemoApp.xcodeprojin Xcode. - Select the
ClickwrapDemoAppscheme and a simulator. - Build and Run (Cmd+R).
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 (View)
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}
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
Proper error handling is crucial for a good user experience. The Utils provides a typed ClickwrapError enum to make this easy.
| 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. 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 and show a generic failure message. |
- Initialize Once: Call
SpotDraftManager.initialize()only once when your application starts. YourApp'sinit()is the perfect place.
- Use a Stable
userIdentifier: When callingsubmitAcceptanceorcheckForPolicyReAcceptance, 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 SwiftUI
View. Use aViewModelto manage state, conform toClickwrapListener, and handle all interactions with theSpotDraftManager.
- Provide Clear Feedback: Always show loading indicators (
ProgressView) during network operations and display clear error messages to the user when something goes wrong.
- Secure Your Configuration: Avoid hardcoding your
clickwrapIddirectly in your source code. Use a secure method like a.xcconfigfile or an obfuscation tool to store sensitive credentials.
| Question / Issue | Suggested Solution |
|---|---|
onReady callback is never fired. |
1. Set enableLogging: true in ClickwrapConfig and check the Xcode console 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 attributed strings, like a UITextView. The demo app contains a HTMLStringView helper for this exact purpose. |
- For issues with the Utils, please open a GitHub issue.
- For questions about your SpotDraft account, please contact support@spotdraft.com.
