feat(CON-173) : [SDK] - Module d'exécution de code en fonction des consentements #6

Merged
l.zborowski merged 1 commits from feat(CON-173) into develop 2024-12-12 15:31:02 +01:00
22 changed files with 358 additions and 10 deletions

View File

@ -27,5 +27,4 @@ plugins {
// Google services
alias(libs.plugins.googleServices) apply false
}

View File

@ -0,0 +1,98 @@
package fr.openium.consentium.api
import android.content.Context
import dagger.hilt.android.EntryPointAccessors
import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.api.model.PurposeIdentifier
import fr.openium.consentium.api.model.PurposeStatus
import fr.openium.consentium.api.model.VendorIdentifier
import fr.openium.consentium.api.model.VendorStatus
import fr.openium.consentium.api.state.FetchConsentiumState
import fr.openium.consentium.api.state.SetConsentiumState
import fr.openium.consentium.data.di.RepositoryEntryPoint
import fr.openium.consentium.data.repository.ConsentiumRepository
import fr.openium.consentium.data.repository.ConsentiumRepositoryGetResponse
import fr.openium.consentium.data.repository.ConsentiumRepositorySetResponse
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class Consentium(
context: Context,
val applicationId: String,
) {
private val _fetchConsentState = MutableStateFlow<FetchConsentiumState>(FetchConsentiumState.Idle)
val fetchConsentState by lazy { _fetchConsentState.asStateFlow() }
private val _setConsentState = MutableStateFlow<SetConsentiumState>(SetConsentiumState.Idle)
val setConsentState by lazy { _setConsentState.asStateFlow() }
private val consentiumRepository: ConsentiumRepository by lazy {
val appContext = context.applicationContext
val entryPoint = EntryPointAccessors.fromApplication(appContext, RepositoryEntryPoint::class.java)
entryPoint.provideConsentiumRepository()
}
fun getConsentStatus(purpose: PurposeIdentifier): PurposeStatus {
return when (val state = fetchConsentState.value) {
is FetchConsentiumState.Valid -> {
val purposeChoice = state.purposes.find { it.identifier == purpose }
purposeChoice?.isAccepted ?: PurposeStatus.UNKNOWN
}
else -> PurposeStatus.UNKNOWN
}
}
fun getConsentStatus(purpose: PurposeIdentifier, vendorIdentifier: VendorIdentifier): VendorStatus {
return when (val state = fetchConsentState.value) {
is FetchConsentiumState.Valid -> {
val vendorChoice = state.purposes.find { it.identifier == purpose }
?.vendors?.find { it.identifier == vendorIdentifier }
when (vendorChoice?.isAccepted) {
true -> VendorStatus.ACCEPTED
false -> VendorStatus.REJECTED
else -> VendorStatus.UNKNOWN
}
}
else -> VendorStatus.UNKNOWN
}
}
suspend fun fetchConsents() {
_fetchConsentState.value = FetchConsentiumState.Loading
try {
when (val consentResponse = consentiumRepository.getConsents(applicationId)) {
ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error
is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> {
val areConsentsValid = consentResponse.isValid
if (areConsentsValid) {
_fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes)
} else {
_fetchConsentState.value = FetchConsentiumState.Invalid
}
}
}
} catch (e: Exception) {
_fetchConsentState.value = FetchConsentiumState.Error
}
}
suspend fun setConsents(
consent: List<PurposeChoice>,
) {
_setConsentState.value = SetConsentiumState.Loading
try {
when (consentiumRepository.setConsents(applicationId, consent)) {
ConsentiumRepositorySetResponse.Error -> _setConsentState.value = SetConsentiumState.Error
ConsentiumRepositorySetResponse.SetConsentsSuccess -> _setConsentState.value = SetConsentiumState.Success
}
} catch (e: Exception) {
_setConsentState.value = SetConsentiumState.Error
}
}
}

View File

@ -0,0 +1,12 @@
package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.Purpose
import fr.openium.consentium.api.model.PurposeIdentifier
import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.PurposeDTO.toPurpose() = Purpose(
identifier = PurposeIdentifier.fromString(identifier),
isRequired = isRequired,
isAccepted = isAccepted.toPurposeStatus(),
vendors = vendors?.map { it.toVendor() } ?: emptyList(),
)

View File

@ -0,0 +1,10 @@
package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.data.remote.model.PatchConsent
internal fun PurposeChoice.toPatchConsentPurposeDTO() = PatchConsent.PurposeDTO(
identifier = purposeIdentifier.toString(),
isAccepted = isAccepted,
vendors = vendors.map { it.toPatchConsentVendorDTO() }
)

View File

@ -0,0 +1,10 @@
package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.PurposeStatus
import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.PurposeStatusDTO.toPurposeStatus() = when (this) {
GetConsent.PurposeStatusDTO.ACCEPTED -> PurposeStatus.ACCEPTED
GetConsent.PurposeStatusDTO.REJECTED -> PurposeStatus.REJECTED
GetConsent.PurposeStatusDTO.NOT_DEFINED -> PurposeStatus.NOT_DEFINED
}

View File

@ -0,0 +1,11 @@
package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.Vendor
import fr.openium.consentium.api.model.VendorIdentifier
import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.VendorDTO.toVendor() = Vendor(
identifier = VendorIdentifier.fromString(identifier),
isAccepted = isAccepted,
isRequired = isRequired,
)

View File

@ -0,0 +1,9 @@
package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.VendorChoice
import fr.openium.consentium.data.remote.model.PatchConsent
internal fun VendorChoice.toPatchConsentVendorDTO() = PatchConsent.VendorDTO(
identifier = vendorIdentifier.toString(),
isAccepted = isAccepted,
)

View File

@ -0,0 +1,8 @@
package fr.openium.consentium.api.model
data class Purpose(
val identifier: PurposeIdentifier,
val isRequired: Boolean,
val isAccepted: PurposeStatus,
val vendors: List<Vendor>,
)

View File

@ -0,0 +1,7 @@
package fr.openium.consentium.api.model
data class PurposeChoice(
val purposeIdentifier: PurposeIdentifier,
val isAccepted: Boolean,
val vendors: List<VendorChoice>,
)

View File

@ -0,0 +1,24 @@
package fr.openium.consentium.api.model
enum class PurposeIdentifier {
REQUIRED,
AUDIENCE,
ADS,
UNKNOWN;
companion object {
fun fromString(string: String): PurposeIdentifier = when (string) {
"purpose-required" -> REQUIRED
"purpose-audience" -> AUDIENCE
"purpose-ads" -> ADS
else -> UNKNOWN
}
}
override fun toString(): String = when (this) {
REQUIRED -> "purpose-required"
AUDIENCE -> "purpose-audience"
ADS -> "purpose-ads"
else -> "purpose-unknown"
}
}

View File

@ -0,0 +1,8 @@
package fr.openium.consentium.api.model
enum class PurposeStatus {
ACCEPTED,
REJECTED,
NOT_DEFINED,
UNKNOWN,
}

View File

@ -0,0 +1,7 @@
package fr.openium.consentium.api.model
data class Vendor(
val identifier: VendorIdentifier,
val isAccepted: Boolean,
val isRequired: Boolean,
)

View File

@ -0,0 +1,6 @@
package fr.openium.consentium.api.model
data class VendorChoice(
val vendorIdentifier: VendorIdentifier,
val isAccepted: Boolean,
)

View File

@ -0,0 +1,27 @@
package fr.openium.consentium.api.model
enum class VendorIdentifier {
CRASHLYTICS,
l.zborowski marked this conversation as resolved Outdated

Crashlytics est requis pour chaque app, ce n'est pas vraiment un vendor

Crashlytics est requis pour chaque app, ce n'est pas vraiment un vendor
MATOMO,
GA4,
CLARITY,
l.zborowski marked this conversation as resolved Outdated

piano n'est plus dans la liste des vendors

piano n'est plus dans la liste des vendors
UNKNOWN;
companion object {
fun fromString(string: String): VendorIdentifier = when (string) {
"vendor-clarity" -> CLARITY
"vendor-crashlytics" -> CRASHLYTICS
"vendor-matomo" -> MATOMO
"vendor-ga4" -> GA4
else -> UNKNOWN
}
l.zborowski marked this conversation as resolved Outdated

pareil

pareil
}
override fun toString(): String = when (this) {
CLARITY -> "vendor-clarity"
CRASHLYTICS -> "vendor-crashlytics"
MATOMO -> "vendor-matomo"
GA4 -> "vendor-ga4"
else -> "vendor-unknown"
}
}
l.zborowski marked this conversation as resolved Outdated

pareil

pareil

View File

@ -0,0 +1,7 @@
package fr.openium.consentium.api.model
enum class VendorStatus {
ACCEPTED,
REJECTED,
UNKNOWN,
}

View File

@ -0,0 +1,17 @@
package fr.openium.consentium.api.state
import fr.openium.consentium.api.model.Purpose
sealed interface FetchConsentiumState {
data object Idle : FetchConsentiumState
data object Loading : FetchConsentiumState
data object Invalid : FetchConsentiumState
data class Valid(val purposes: List<Purpose>) : FetchConsentiumState
data object Error : FetchConsentiumState
}

View File

@ -0,0 +1,13 @@
package fr.openium.consentium.api.state
sealed interface SetConsentiumState {
data object Idle : SetConsentiumState
data object Loading : SetConsentiumState
data object Success : SetConsentiumState
data object Error : SetConsentiumState
}

View File

@ -0,0 +1,12 @@
package fr.openium.consentium.data.di
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.repository.ConsentiumRepository
@EntryPoint
@InstallIn(SingletonComponent::class)
internal interface RepositoryEntryPoint {
fun provideConsentiumRepository(): ConsentiumRepository
}

View File

@ -2,6 +2,7 @@ package fr.openium.consentium.data.remote
import fr.openium.consentium.data.remote.model.GetConsent
import fr.openium.consentium.data.remote.model.PatchConsent
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
@ -13,13 +14,12 @@ internal interface ConsentiumApi {
suspend fun getConsents(
applicationId: String,
installationId: String,
): GetConsent.GetConsentPayloadDTO
): Response<GetConsent.GetConsentPayloadDTO>
@PATCH("/consents")
suspend fun setConsents(
applicationId: String,
installationId: String,
@Body patchConsent: PatchConsent.PatchConsentPayloadDTO,
): Any
): Response<Any>
}

View File

@ -16,7 +16,7 @@ internal sealed interface GetConsent {
data class PurposeDTO(
@SerialName("identifier") val identifier: String,
@SerialName("isRequired") val isRequired: Boolean,
@SerialName("isAccepted") val isAccepted: PurposeStatus,
@SerialName("isAccepted") val isAccepted: PurposeStatusDTO,
@SerialName("vendors") val vendors: List<VendorDTO>? = null,
)
@ -28,7 +28,7 @@ internal sealed interface GetConsent {
)
@Serializable
enum class PurposeStatus {
enum class PurposeStatusDTO {
@SerialName("ACCEPTED")
ACCEPTED,

View File

@ -1,7 +1,12 @@
package fr.openium.consentium.data.repository
import fr.openium.consentium.api.adapter.toPatchConsentPurposeDTO
import fr.openium.consentium.api.adapter.toPurpose
import fr.openium.consentium.api.model.Purpose
import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.data.local.ConsentiumDataStore
import fr.openium.consentium.data.remote.ConsentiumApi
import fr.openium.consentium.data.remote.model.PatchConsent
import javax.inject.Inject
internal class ConsentiumRepository @Inject constructor(
@ -9,5 +14,63 @@ internal class ConsentiumRepository @Inject constructor(
private val consentiumDataStore: ConsentiumDataStore,
) {
suspend fun getConsents(applicationId: String): ConsentiumRepositoryGetResponse {
val installationId = consentiumDataStore.getInstallationUniqueId()
val consentsResponse = consentiumApi.getConsents(applicationId, installationId)
return try {
val consentsBody = if (consentsResponse.isSuccessful) {
consentsResponse.body() ?: throw Exception()
} else {
throw Exception()
}
return ConsentiumRepositoryGetResponse.GetConsentsSuccess(
isValid = consentsBody.isValid,
purposes = consentsBody.purposes?.map { purpose -> purpose.toPurpose() } ?: emptyList()
)
} catch (e: Exception) {
ConsentiumRepositoryGetResponse.Error
}
}
suspend fun setConsents(
applicationId: String,
consents: List<PurposeChoice>,
): ConsentiumRepositorySetResponse {
val installationId = consentiumDataStore.getInstallationUniqueId()
val setConsentResponse = consentiumApi.setConsents(
applicationId = applicationId,
patchConsent = PatchConsent.PatchConsentPayloadDTO(
installationId = installationId,
purposes = consents.map { it.toPatchConsentPurposeDTO() },
)
)
return if (setConsentResponse.isSuccessful) {
ConsentiumRepositorySetResponse.SetConsentsSuccess
} else {
ConsentiumRepositorySetResponse.Error
}
}
}
internal interface ConsentiumRepositoryGetResponse {
data object Error : ConsentiumRepositoryGetResponse
data class GetConsentsSuccess(
val isValid: Boolean,
val purposes: List<Purpose>,
) : ConsentiumRepositoryGetResponse
}
internal interface ConsentiumRepositorySetResponse {
data object Error : ConsentiumRepositorySetResponse
data object SetConsentsSuccess : ConsentiumRepositorySetResponse
}

View File

@ -27,8 +27,8 @@ timber = "5.0.1"
material = "1.12.0"
# Serialization
serialization = "2.0.21"
jsonSerialization = "1.7.3"
serialization = "2.0.0"
jsonSerialization = "1.7.1"
# Retrofit
retrofit = "2.11.0"
@ -48,7 +48,7 @@ preferencesDataStore = "1.1.1"
# Plugins
agp = "8.7.3"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.23"
ksp = "2.0.0-1.0.24"
junitVersion = "1.2.1"
googleServicesPlugin = "4.4.2"
@ -99,7 +99,7 @@ compose-material3 = { group = "androidx.compose.material3", name = "material3" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
# Kotlin serizalization
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jsonSerialization" }
# Compose navigation
androidx-navigation-common-ktx = { group = "androidx.navigation", name = "navigation-common-ktx", version.ref = "navigationCompose" }