diff --git a/build.gradle.kts b/build.gradle.kts index a47fc9a..986ddcd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,5 +27,4 @@ plugins { // Google services alias(libs.plugins.googleServices) apply false - } \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/Consentium.kt b/consentium/src/main/java/fr/openium/consentium/api/Consentium.kt new file mode 100644 index 0000000..9427663 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/Consentium.kt @@ -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.Idle) + val fetchConsentState by lazy { _fetchConsentState.asStateFlow() } + + private val _setConsentState = MutableStateFlow(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, + ) { + _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 + } + } +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeAdapter.kt b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeAdapter.kt new file mode 100644 index 0000000..f60776b --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeAdapter.kt @@ -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(), +) diff --git a/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeChoiceAdapter.kt b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeChoiceAdapter.kt new file mode 100644 index 0000000..2babdd8 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeChoiceAdapter.kt @@ -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() } +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeStatusAdapter.kt b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeStatusAdapter.kt new file mode 100644 index 0000000..fc63bb5 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeStatusAdapter.kt @@ -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 +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorAdapter.kt b/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorAdapter.kt new file mode 100644 index 0000000..b9c3bba --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorAdapter.kt @@ -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, +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorChoiceAdapter.kt b/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorChoiceAdapter.kt new file mode 100644 index 0000000..ec76012 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorChoiceAdapter.kt @@ -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, +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/Purpose.kt b/consentium/src/main/java/fr/openium/consentium/api/model/Purpose.kt new file mode 100644 index 0000000..6d9caab --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/Purpose.kt @@ -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, +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/PurposeChoice.kt b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeChoice.kt new file mode 100644 index 0000000..bbd4c4a --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeChoice.kt @@ -0,0 +1,7 @@ +package fr.openium.consentium.api.model + +data class PurposeChoice( + val purposeIdentifier: PurposeIdentifier, + val isAccepted: Boolean, + val vendors: List, +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/PurposeIdentifier.kt b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeIdentifier.kt new file mode 100644 index 0000000..3e3ce8f --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeIdentifier.kt @@ -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" + } +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/PurposeStatus.kt b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeStatus.kt new file mode 100644 index 0000000..6175e2a --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeStatus.kt @@ -0,0 +1,8 @@ +package fr.openium.consentium.api.model + +enum class PurposeStatus { + ACCEPTED, + REJECTED, + NOT_DEFINED, + UNKNOWN, +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/Vendor.kt b/consentium/src/main/java/fr/openium/consentium/api/model/Vendor.kt new file mode 100644 index 0000000..3499ae3 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/Vendor.kt @@ -0,0 +1,7 @@ +package fr.openium.consentium.api.model + +data class Vendor( + val identifier: VendorIdentifier, + val isAccepted: Boolean, + val isRequired: Boolean, +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/VendorChoice.kt b/consentium/src/main/java/fr/openium/consentium/api/model/VendorChoice.kt new file mode 100644 index 0000000..98681ab --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/VendorChoice.kt @@ -0,0 +1,6 @@ +package fr.openium.consentium.api.model + +data class VendorChoice( + val vendorIdentifier: VendorIdentifier, + val isAccepted: Boolean, +) \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/VendorIdentifier.kt b/consentium/src/main/java/fr/openium/consentium/api/model/VendorIdentifier.kt new file mode 100644 index 0000000..65b1b70 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/VendorIdentifier.kt @@ -0,0 +1,27 @@ +package fr.openium.consentium.api.model + +enum class VendorIdentifier { + CRASHLYTICS, + MATOMO, + GA4, + CLARITY, + UNKNOWN; + + companion object { + fun fromString(string: String): VendorIdentifier = when (string) { + "vendor-clarity" -> CLARITY + "vendor-crashlytics" -> CRASHLYTICS + "vendor-matomo" -> MATOMO + "vendor-ga4" -> GA4 + else -> UNKNOWN + } + } + + override fun toString(): String = when (this) { + CLARITY -> "vendor-clarity" + CRASHLYTICS -> "vendor-crashlytics" + MATOMO -> "vendor-matomo" + GA4 -> "vendor-ga4" + else -> "vendor-unknown" + } +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/model/VendorStatus.kt b/consentium/src/main/java/fr/openium/consentium/api/model/VendorStatus.kt new file mode 100644 index 0000000..43be282 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/model/VendorStatus.kt @@ -0,0 +1,7 @@ +package fr.openium.consentium.api.model + +enum class VendorStatus { + ACCEPTED, + REJECTED, + UNKNOWN, +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/state/FetchConsentiumState.kt b/consentium/src/main/java/fr/openium/consentium/api/state/FetchConsentiumState.kt new file mode 100644 index 0000000..2f08b0d --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/state/FetchConsentiumState.kt @@ -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) : FetchConsentiumState + + data object Error : FetchConsentiumState + +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/api/state/SetConsentiumState.kt b/consentium/src/main/java/fr/openium/consentium/api/state/SetConsentiumState.kt new file mode 100644 index 0000000..9f795b3 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/api/state/SetConsentiumState.kt @@ -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 + +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/data/di/RepositoryEntryPoint.kt b/consentium/src/main/java/fr/openium/consentium/data/di/RepositoryEntryPoint.kt new file mode 100644 index 0000000..f39a140 --- /dev/null +++ b/consentium/src/main/java/fr/openium/consentium/data/di/RepositoryEntryPoint.kt @@ -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 +} \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/data/remote/ConsentiumApi.kt b/consentium/src/main/java/fr/openium/consentium/data/remote/ConsentiumApi.kt index 4f3847d..8b70b47 100644 --- a/consentium/src/main/java/fr/openium/consentium/data/remote/ConsentiumApi.kt +++ b/consentium/src/main/java/fr/openium/consentium/data/remote/ConsentiumApi.kt @@ -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 @PATCH("/consents") suspend fun setConsents( applicationId: String, - installationId: String, @Body patchConsent: PatchConsent.PatchConsentPayloadDTO, - ): Any + ): Response } \ No newline at end of file diff --git a/consentium/src/main/java/fr/openium/consentium/data/remote/model/GetConsentPayloadDTO.kt b/consentium/src/main/java/fr/openium/consentium/data/remote/model/GetConsentPayloadDTO.kt index 3206996..2bb7169 100644 --- a/consentium/src/main/java/fr/openium/consentium/data/remote/model/GetConsentPayloadDTO.kt +++ b/consentium/src/main/java/fr/openium/consentium/data/remote/model/GetConsentPayloadDTO.kt @@ -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? = null, ) @@ -28,7 +28,7 @@ internal sealed interface GetConsent { ) @Serializable - enum class PurposeStatus { + enum class PurposeStatusDTO { @SerialName("ACCEPTED") ACCEPTED, diff --git a/consentium/src/main/java/fr/openium/consentium/data/repository/ConsentiumRepository.kt b/consentium/src/main/java/fr/openium/consentium/data/repository/ConsentiumRepository.kt index f553d83..1674025 100644 --- a/consentium/src/main/java/fr/openium/consentium/data/repository/ConsentiumRepository.kt +++ b/consentium/src/main/java/fr/openium/consentium/data/repository/ConsentiumRepository.kt @@ -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, + ): 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, + ) : ConsentiumRepositoryGetResponse + +} + +internal interface ConsentiumRepositorySetResponse { + + data object Error : ConsentiumRepositorySetResponse + + data object SetConsentsSuccess : ConsentiumRepositorySetResponse } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c44f2a..97650ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }