diff --git a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/ClickwrapService.kt b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/ClickwrapService.kt index 1b22524..3035a7a 100644 --- a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/ClickwrapService.kt +++ b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/ClickwrapService.kt @@ -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 @@ -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)) { @@ -98,7 +99,8 @@ internal class ClickwrapService( userIdentifier = userIdentifier, clickwrapPublicId = data.publicId, policies = data.policies, - agreements = data.agreements + agreements = data.agreements, + metadata = metadata ) } diff --git a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/SpotDraftManager.kt b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/SpotDraftManager.kt index 3a51f28..98718ed 100644 --- a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/SpotDraftManager.kt +++ b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/core/SpotDraftManager.kt @@ -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 @@ -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 { @@ -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) { diff --git a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/domain/models/ClickwrapSubmitMetadata.kt b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/domain/models/ClickwrapSubmitMetadata.kt new file mode 100644 index 0000000..e3977a0 --- /dev/null +++ b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/domain/models/ClickwrapSubmitMetadata.kt @@ -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? = null, + val keyPointerInformation: Map? = null, + val externalMetadata: Map? = null +) diff --git a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/ConsentStatusRequest.kt b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/ConsentStatusRequest.kt index 0341bf8..f7ddfe2 100644 --- a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/ConsentStatusRequest.kt +++ b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/ConsentStatusRequest.kt @@ -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? = null, - @SerializedName("key_pointer_information") - val keyPointerInformation: Map? = null, - @SerializedName("agreements") - val agreements: List? = null -) - -data class AgreementConsentStatus( - @SerializedName("id") - val id: Int, - @SerializedName("version_id") - val versionId: Int, - @SerializedName("has_clicked") - val hasClicked: Boolean + val userIdentifier: String ) diff --git a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/SubmissionRequestModel.kt b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/SubmissionRequestModel.kt index e3a93cf..9f56603 100644 --- a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/SubmissionRequestModel.kt +++ b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/networking/models/request/SubmissionRequestModel.kt @@ -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, + @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? = null, + @SerializedName("key_pointer_information") val keyPointerInformation: Map? = null, + @SerializedName("external_metadata") val externalMetadata: Map? = null, ) \ No newline at end of file diff --git a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/repository/ClickwrapRepository.kt b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/repository/ClickwrapRepository.kt index 4ca92c0..74e7750 100644 --- a/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/repository/ClickwrapRepository.kt +++ b/spotdraftclickwrap/src/main/java/com/app/android/clickwrap/repository/ClickwrapRepository.kt @@ -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 @@ -63,7 +64,8 @@ internal interface ClickwrapRepository { userIdentifier: String, clickwrapPublicId: String?, policies: List, - agreements: List + agreements: List, + metadata: ClickwrapSubmitMetadata? = null ): String? /** @@ -158,7 +160,8 @@ internal class DefaultClickwrapRepository( userIdentifier: String, clickwrapPublicId: String?, policies: List, - agreements: List + agreements: List, + metadata: ClickwrapSubmitMetadata? = null ): String? { Logger.log( Logger.LogLevel.INFO, "${UtilsConstants.LogCore.TAG_CLICKWRAP_REPO}: ${ @@ -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.