Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import androidx.annotation.RequiresPermission
import com.app.android.clickwrap.constant.UtilsConstants
import com.app.android.clickwrap.domain.models.Clickwrap
import com.app.android.clickwrap.domain.models.ClickwrapSubmitMetadata
import com.app.android.clickwrap.domain.models.Policy
import com.app.android.clickwrap.enums.DisplayType
import com.app.android.clickwrap.networking.models.response.ConsentStatusResponse
Expand Down Expand Up @@ -82,7 +83,7 @@ internal class ClickwrapService(
* @throws ClickwrapError.DecodingError If the submission response cannot be parsed.
*/
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
suspend fun submitAcceptance(userIdentifier: String): String? {
suspend fun submitAcceptance(userIdentifier: String, metadata: ClickwrapSubmitMetadata? = null): String? {
Logger.info(UtilsConstants.LogCore.CLICKWRAP_SERVICE_SUBMITTING_ACCEPTANCE.format(userIdentifier))
val appContext = SpotDraftManager.getAppContext()
if (!NetworkUtils.isNetworkAvailable(appContext)) {
Expand All @@ -98,7 +99,8 @@ internal class ClickwrapService(
userIdentifier = userIdentifier,
clickwrapPublicId = data.publicId,
policies = data.policies,
agreements = data.agreements
agreements = data.agreements,
metadata = metadata
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.app.android.clickwrap.config.ClickwrapConfig
import com.app.android.clickwrap.constant.UtilsConstants
import com.app.android.clickwrap.core.SpotDraftManager.Companion.initialize
import com.app.android.clickwrap.domain.models.Clickwrap
import com.app.android.clickwrap.domain.models.ClickwrapSubmitMetadata
import com.app.android.clickwrap.domain.models.ReAcceptanceResult
import com.app.android.clickwrap.enums.ConsentStatus
import com.app.android.clickwrap.enums.DisplayType
Expand Down Expand Up @@ -204,15 +205,19 @@ class SpotDraftManager private constructor() {
* on the listener is called. If validation fails or an error occurs during submission,
* the `onError` method is called.
*
* @param Context The Android [Context] required for the underlying service operations.
* @param userIdentifier A unique string identifier for the user performing the submission.
* @param metadata Optional metadata to enrich the consent record stored on the backend
* (first name, last name, email, custom information, key pointer information,
* and external metadata). All fields within [metadata] are nullable. Defaults to `null`.
* @throws ClickwrapError.NotInitialized If the util has not been initialized.
* @throws ClickwrapError.PoliciesNotAccepted If not all policies are accepted before a submission attempt.
*/
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
@JvmStatic
@JvmOverloads
fun submitAcceptance(
@NonNull userIdentifier: String
@NonNull userIdentifier: String,
@Nullable metadata: ClickwrapSubmitMetadata? = null
) {
Logger.info(UtilsConstants.LogCore.MANAGER_SUBMITTING_ACCEPTANCE.format(userIdentifier))
val currentService = actualServiceInstance ?: run {
Expand All @@ -224,7 +229,7 @@ class SpotDraftManager private constructor() {
if (!currentService.validateAllAccepted()) {
throw ClickwrapError.PoliciesNotAccepted
}
val submissionId = currentService.submitAcceptance(userIdentifier)
val submissionId = currentService.submitAcceptance(userIdentifier, metadata)
actualListenerInstance?.onSubmitSuccessful(submissionId)
currentService.clearClickwrapData()
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.app.android.clickwrap.domain.models

/**
* Optional metadata that can be attached to a clickwrap acceptance submission.
*
* Pass an instance of this class to [com.app.android.clickwrap.core.SpotDraftManager.submitAcceptance]
* to enrich the consent record stored on the SpotDraft backend. All fields are nullable —
* supply only what is available in your application context.
*
* ## Example
* ```kotlin
* val metadata = ClickwrapSubmitMetadata(
* firstName = "Jane",
* lastName = "Doe",
* userEmail = "jane@example.com",
* additionalCustomInformation = mapOf("plan" to "enterprise"),
* keyPointerInformation = mapOf("contractRef" to "MSA-2024"),
* externalMetadata = mapOf("crmId" to "SF-00001")
* )
* SpotDraftManager.submitAcceptance(userIdentifier = userId, metadata = metadata)
* ```
*
* @property firstName The user's first name.
* @property lastName The user's last name.
* @property userEmail The user's email address.
* @property additionalCustomInformation Arbitrary key–value pairs to attach as custom
* information on the consent record. Values must be JSON-serialisable types.
* @property keyPointerInformation Structured metadata used to populate contract field
* pointers on the consent record. Values must be JSON-serialisable types.
* @property externalMetadata External-system context to store alongside the consent record
* (e.g. CRM IDs, order references). Values must be JSON-serialisable types.
*/
data class ClickwrapSubmitMetadata(
val firstName: String? = null,
val lastName: String? = null,
val userEmail: String? = null,
val additionalCustomInformation: Map<String, Any>? = null,
val keyPointerInformation: Map<String, Any>? = null,
val externalMetadata: Map<String, Any>? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ package com.app.android.clickwrap.networking.models.request

import com.google.gson.annotations.SerializedName

/**
* Request body for `POST .../consent_status/`.
*
* The backend only consumes [userIdentifier]; the previous iteration of this model
* included `additional_custom_information`, `key_pointer_information`, and `agreements`
* which the API ignores entirely. Those fields have been removed to keep the wire
* format accurate and the model self-documenting.
*
* @property userIdentifier A unique identifier for the user whose consent status is being checked.
*/
data class ConsentStatusRequest(
@SerializedName("user_identifier")
val userIdentifier: String,
@SerializedName("additional_custom_information")
val additionalCustomInformation: Map<String, Any>? = null,
@SerializedName("key_pointer_information")
val keyPointerInformation: Map<String, Any>? = null,
@SerializedName("agreements")
val agreements: List<AgreementConsentStatus>? = null
)

data class AgreementConsentStatus(
@SerializedName("id")
val id: Int,
@SerializedName("version_id")
val versionId: Int,
@SerializedName("has_clicked")
val hasClicked: Boolean
val userIdentifier: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,30 @@ internal data class AgreementBody(
* Represents the complete request body structure for submitting clickwrap acceptance.
* This model is serialized into JSON and sent to the `/execute` API endpoint.
*
* All fields except [userIdentifier] and [agreements] are optional. When a field is `null`,
* Gson omits it from the JSON payload (using `@SerializedName` with Gson's default behaviour)
* keeping the wire format backward-compatible.
*
* @property userIdentifier A unique string identifying the user performing the acceptance.
* @property clickwrapPublicId The public ID (UUID) of the clickwrap configuration being executed.
* @property agreements A list of [AgreementBody] objects, each detailing an agreement's acceptance status.
* @property firstName The user's first name (optional).
* @property lastName The user's last name (optional).
* @property userEmail The user's email address (optional).
* @property additionalCustomInformation Arbitrary key–value pairs stored as custom information
* on the consent record (optional).
* @property keyPointerInformation Structured contract-field pointer metadata (optional).
* @property externalMetadata External-system context data (optional).
*/

internal data class RequestBody(
@SerializedName("user_identifier") val userIdentifier: String,
@SerializedName("clickwrap_public_id") val clickwrapPublicId: String?,
@SerializedName("agreements") val agreements: List<AgreementBody>,
@SerializedName("first_name") val firstName: String? = null,
@SerializedName("last_name") val lastName: String? = null,
@SerializedName("user_email") val userEmail: String? = null,
@SerializedName("additional_custom_information") val additionalCustomInformation: Map<String, Any>? = null,
@SerializedName("key_pointer_information") val keyPointerInformation: Map<String, Any>? = null,
@SerializedName("external_metadata") val externalMetadata: Map<String, Any>? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.app.android.clickwrap.constant.UtilsConstants
import com.app.android.clickwrap.domain.mappers.ClickwrapMapper
import com.app.android.clickwrap.domain.models.Agreement
import com.app.android.clickwrap.domain.models.Clickwrap
import com.app.android.clickwrap.domain.models.ClickwrapSubmitMetadata
import com.app.android.clickwrap.domain.models.Policy
import com.app.android.clickwrap.helper.errors.ClickwrapError
import com.app.android.clickwrap.helper.logger.Logger
Expand Down Expand Up @@ -63,7 +64,8 @@ internal interface ClickwrapRepository {
userIdentifier: String,
clickwrapPublicId: String?,
policies: List<Policy>,
agreements: List<Agreement>
agreements: List<Agreement>,
metadata: ClickwrapSubmitMetadata? = null
): String?

/**
Expand Down Expand Up @@ -158,7 +160,8 @@ internal class DefaultClickwrapRepository(
userIdentifier: String,
clickwrapPublicId: String?,
policies: List<Policy>,
agreements: List<Agreement>
agreements: List<Agreement>,
metadata: ClickwrapSubmitMetadata? = null
): String? {
Logger.log(
Logger.LogLevel.INFO, "${UtilsConstants.LogCore.TAG_CLICKWRAP_REPO}: ${
Expand Down Expand Up @@ -202,7 +205,13 @@ internal class DefaultClickwrapRepository(
val requestBody = RequestBody(
userIdentifier = userIdentifier,
clickwrapPublicId = clickwrapPublicId,
agreements = agreementPayloads
agreements = agreementPayloads,
firstName = metadata?.firstName,
lastName = metadata?.lastName,
userEmail = metadata?.userEmail,
additionalCustomInformation = metadata?.additionalCustomInformation,
keyPointerInformation = metadata?.keyPointerInformation,
externalMetadata = metadata?.externalMetadata
)

// Step 5: Encode the request body object to a JSON string using Gson.
Expand Down