feat(CON-223) : Ajouter l'auth

feat(CON-262) : Aligner les flux avec les nouveaux dev back
This commit is contained in:
Louis Legrand 2025-02-11 14:48:19 +01:00
parent 5303e594da
commit c57c9b7d05
25 changed files with 146 additions and 60 deletions

View File

@ -72,6 +72,10 @@ android {
dimension = "version" dimension = "version"
} }
create("dev") {
dimension = "version"
}
create("demo") { create("demo") {
dimension = "version" dimension = "version"
} }

View File

@ -14,7 +14,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import fr.openium.consentium.R
import fr.openium.consentium.api.Consentium import fr.openium.consentium.api.Consentium
import fr.openium.consentium.api.state.FetchConsentiumState import fr.openium.consentium.api.state.FetchConsentiumState
@ -25,14 +27,17 @@ fun SplashScreen(
) { ) {
// Property // Property
val context = LocalContext.current val context = LocalContext.current
val consentium = remember { Consentium(context = context, applicationId = "DemoApplicationId") } val consentiumKey = stringResource(R.string.consentium_api_key)
val consentium = remember { Consentium(context = context, applicationId = consentiumKey) }
// Effect // Effect
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
consentium.fetchConsentState.collect { consentState -> consentium.fetchConsentState.collect { consentState ->
when (consentState) { when (consentState) {
FetchConsentiumState.Idle, FetchConsentiumState.Idle,
FetchConsentiumState.Loading -> {} FetchConsentiumState.Loading,
-> {
}
FetchConsentiumState.Error -> { FetchConsentiumState.Error -> {
// Handle error // Handle error

View File

@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">Consentium</string> <string name="app_name">Consentium</string>
<string translatable="false" name="consentium_api_key">01938ce4-331a-7592-9e90-f09201ff4f36</string>
</resources> </resources>

View File

@ -34,6 +34,10 @@ android {
dimension = "version" dimension = "version"
} }
create("dev") {
dimension = "version"
}
create("demo") { create("demo") {
dimension = "version" dimension = "version"
} }

View File

@ -34,6 +34,10 @@ android {
dimension = "version" dimension = "version"
} }
create("dev") {
dimension = "version"
}
create("demo") { create("demo") {
dimension = "version" dimension = "version"
} }

View File

@ -2,6 +2,7 @@ package fr.openium.consentium.api
import android.content.Context import android.content.Context
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import fr.openium.consentium.api.model.ConsentState
import fr.openium.consentium.api.model.PurposeChoice import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.api.state.FetchConsentiumState import fr.openium.consentium.api.state.FetchConsentiumState
import fr.openium.consentium.api.state.SetConsentiumState import fr.openium.consentium.api.state.SetConsentiumState
@ -35,7 +36,7 @@ class Consentium(
ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error
is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> { is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> {
val areConsentsValid = consentResponse.isValid val areConsentsValid = consentResponse.state == ConsentState.VALID
if (areConsentsValid) { if (areConsentsValid) {
_fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes) _fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes)
} else { } else {

View File

@ -0,0 +1,11 @@
package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.ConsentState
import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.ConsentStateDTO.toConsentState(): ConsentState = when (this) {
GetConsent.ConsentStateDTO.UNSET -> ConsentState.UNSET
GetConsent.ConsentStateDTO.VALID -> ConsentState.VALID
GetConsent.ConsentStateDTO.EXPIRED -> ConsentState.EXPIRED
GetConsent.ConsentStateDTO.NEW_VERSION -> ConsentState.NEW_VERSION
}

View File

@ -5,7 +5,6 @@ import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.PurposeDTO.toPurpose() = Purpose( internal fun GetConsent.PurposeDTO.toPurpose() = Purpose(
identifier = identifier, identifier = identifier,
isRequired = isRequired, choice = choice.toPurposeStatus(),
isAccepted = isAccepted.toPurposeStatus(),
vendors = vendors?.map { it.toVendor() } ?: emptyList(), vendors = vendors?.map { it.toVendor() } ?: emptyList(),
) )

View File

@ -4,7 +4,7 @@ import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.data.remote.model.PatchConsent import fr.openium.consentium.data.remote.model.PatchConsent
internal fun PurposeChoice.toPatchConsentPurposeDTO() = PatchConsent.PurposeDTO( internal fun PurposeChoice.toPatchConsentPurposeDTO() = PatchConsent.PurposeDTO(
identifier = purposeIdentifier.toString(), identifier = purposeIdentifier,
isAccepted = isAccepted, choice = choice.toPurposeStatusDTO(),
vendors = vendors.map { it.toPatchConsentVendorDTO() } vendors = vendors.map { it.toPatchConsentVendorDTO() }
) )

View File

@ -2,9 +2,16 @@ package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.PurposeStatus import fr.openium.consentium.api.model.PurposeStatus
import fr.openium.consentium.data.remote.model.GetConsent import fr.openium.consentium.data.remote.model.GetConsent
import fr.openium.consentium.data.remote.model.PatchConsent
internal fun GetConsent.PurposeStatusDTO.toPurposeStatus() = when (this) { internal fun GetConsent.PurposeStatusDTO.toPurposeStatus() = when (this) {
GetConsent.PurposeStatusDTO.ACCEPTED -> PurposeStatus.ACCEPTED GetConsent.PurposeStatusDTO.ACCEPTED -> PurposeStatus.ACCEPTED
GetConsent.PurposeStatusDTO.REJECTED -> PurposeStatus.REJECTED GetConsent.PurposeStatusDTO.REJECTED -> PurposeStatus.REJECTED
GetConsent.PurposeStatusDTO.NOT_DEFINED -> PurposeStatus.NOT_DEFINED GetConsent.PurposeStatusDTO.PARTIAL -> PurposeStatus.PARTIAL
}
internal fun PurposeStatus.toPurposeStatusDTO() = when (this) {
PurposeStatus.ACCEPTED -> PatchConsent.PurposeStatusDTO.ACCEPTED
PurposeStatus.REJECTED -> PatchConsent.PurposeStatusDTO.REJECTED
PurposeStatus.PARTIAL -> PatchConsent.PurposeStatusDTO.PARTIAL
} }

View File

@ -4,6 +4,6 @@ import fr.openium.consentium.api.model.VendorChoice
import fr.openium.consentium.data.remote.model.PatchConsent import fr.openium.consentium.data.remote.model.PatchConsent
internal fun VendorChoice.toPatchConsentVendorDTO() = PatchConsent.VendorDTO( internal fun VendorChoice.toPatchConsentVendorDTO() = PatchConsent.VendorDTO(
identifier = vendorIdentifier.toString(), identifier = vendorIdentifier,
isAccepted = isAccepted, isAccepted = isAccepted,
) )

View File

@ -0,0 +1,8 @@
package fr.openium.consentium.api.model
enum class ConsentState {
UNSET,
VALID,
EXPIRED,
NEW_VERSION,
}

View File

@ -2,7 +2,6 @@ package fr.openium.consentium.api.model
data class Purpose( data class Purpose(
val identifier: String, val identifier: String,
val isRequired: Boolean, val choice: PurposeStatus,
val isAccepted: PurposeStatus,
val vendors: List<Vendor>, val vendors: List<Vendor>,
) )

View File

@ -2,6 +2,6 @@ package fr.openium.consentium.api.model
data class PurposeChoice( data class PurposeChoice(
val purposeIdentifier: String, val purposeIdentifier: String,
val isAccepted: Boolean, val choice: PurposeStatus,
val vendors: List<VendorChoice>, val vendors: List<VendorChoice>,
) )

View File

@ -3,6 +3,5 @@ package fr.openium.consentium.api.model
enum class PurposeStatus { enum class PurposeStatus {
ACCEPTED, ACCEPTED,
REJECTED, REJECTED,
NOT_DEFINED, PARTIAL,
UNKNOWN,
} }

View File

@ -5,20 +5,20 @@ import fr.openium.consentium.data.remote.model.PatchConsent
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.PATCH import retrofit2.http.Header
import retrofit2.http.POST
internal interface ConsentiumApi { internal interface ConsentiumApi {
@GET("/consents") @GET("/consents")
suspend fun getConsents( suspend fun getConsents(
applicationId: String, @Header("Authorization") token: String,
installationId: String,
): Response<GetConsent.GetConsentPayloadDTO> ): Response<GetConsent.GetConsentPayloadDTO>
@PATCH("/consents") @POST("/consents")
suspend fun setConsents( suspend fun setConsents(
applicationId: String, @Header("Authorization") token: String,
@Body patchConsent: PatchConsent.PatchConsentPayloadDTO, @Body patchConsent: PatchConsent.PatchConsentPayloadDTO,
): Response<Any> ): Response<Any>

View File

@ -10,41 +10,29 @@ import java.util.UUID
internal object ConsentiumMockApi : ConsentiumApi { internal object ConsentiumMockApi : ConsentiumApi {
private val consents = GetConsent.GetConsentPayloadDTO( private val consents = GetConsent.GetConsentPayloadDTO(
id = UUID.randomUUID().toString(),
installationId = UUID.randomUUID().toString(), installationId = UUID.randomUUID().toString(),
purposes = listOf( purposes = listOf(
GetConsent.PurposeDTO( GetConsent.PurposeDTO(
identifier = "purpose-audience", identifier = "purpose-audience",
isRequired = true, vendors = null,
isAccepted = GetConsent.PurposeStatusDTO.ACCEPTED, choice = GetConsent.PurposeStatusDTO.ACCEPTED,
vendors = listOf(
GetConsent.VendorDTO(
identifier = "vendor-clarity",
isAccepted = true,
isRequired = true,
),
GetConsent.VendorDTO(
identifier = "vendor-matomo",
isAccepted = true,
isRequired = false,
)
)
), ),
GetConsent.PurposeDTO( GetConsent.PurposeDTO(
identifier = "purpose-required", identifier = "purpose-required",
isRequired = true, vendors = null,
isAccepted = GetConsent.PurposeStatusDTO.REJECTED, choice = GetConsent.PurposeStatusDTO.REJECTED,
vendors = null
), ),
), ),
isValid = false, state = GetConsent.ConsentStateDTO.VALID,
) )
override suspend fun getConsents(applicationId: String, installationId: String): Response<GetConsent.GetConsentPayloadDTO> { override suspend fun getConsents(token: String): Response<GetConsent.GetConsentPayloadDTO> {
delay(2000) delay(2000)
return Response.success(consents) return Response.success(consents)
} }
override suspend fun setConsents(applicationId: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response<Any> { override suspend fun setConsents(token: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response<Any> {
delay(2000) delay(2000)
return Response.success(Unit) return Response.success(Unit)
} }

View File

@ -7,16 +7,17 @@ internal sealed interface GetConsent {
@Serializable @Serializable
data class GetConsentPayloadDTO( data class GetConsentPayloadDTO(
@SerialName("id") val id: String,
@SerialName("installationId") val installationId: String, @SerialName("installationId") val installationId: String,
@SerialName("state") val state: ConsentStateDTO? = null,
@SerialName("acceptedDate") val acceptedData: Long? = null,
@SerialName("purposes") val purposes: List<PurposeDTO>? = null, @SerialName("purposes") val purposes: List<PurposeDTO>? = null,
@SerialName("isValid") val isValid: Boolean,
) )
@Serializable @Serializable
data class PurposeDTO( data class PurposeDTO(
@SerialName("identifier") val identifier: String, @SerialName("identifier") val identifier: String,
@SerialName("isRequired") val isRequired: Boolean, @SerialName("choice") val choice: PurposeStatusDTO,
@SerialName("isAccepted") val isAccepted: PurposeStatusDTO,
@SerialName("vendors") val vendors: List<VendorDTO>? = null, @SerialName("vendors") val vendors: List<VendorDTO>? = null,
) )
@ -29,16 +30,30 @@ internal sealed interface GetConsent {
@Serializable @Serializable
enum class PurposeStatusDTO { enum class PurposeStatusDTO {
@SerialName("ACCEPTED") @SerialName("accepted")
ACCEPTED, ACCEPTED,
@SerialName("REJECTED") @SerialName("refused")
REJECTED, REJECTED,
@SerialName("NOT_DEFINED") @SerialName("partial")
NOT_DEFINED, PARTIAL,
} }
@Serializable
enum class ConsentStateDTO {
@SerialName("unset")
UNSET,
@SerialName("valid")
VALID,
@SerialName("expired")
EXPIRED,
@SerialName("new_version")
NEW_VERSION,
}
} }

View File

@ -7,14 +7,13 @@ internal sealed interface PatchConsent {
@Serializable @Serializable
data class PatchConsentPayloadDTO( data class PatchConsentPayloadDTO(
@SerialName("installationId") val installationId: String, @SerialName("consentPurposes") val purposes: List<PurposeDTO>? = null,
@SerialName("purposes") val purposes: List<PurposeDTO>? = null,
) )
@Serializable @Serializable
data class PurposeDTO( data class PurposeDTO(
@SerialName("identifier") val identifier: String, @SerialName("identifier") val identifier: String,
@SerialName("isAccepted") val isAccepted: Boolean, @SerialName("choice") val choice: PurposeStatusDTO,
@SerialName("vendors") val vendors: List<VendorDTO>? = null, @SerialName("vendors") val vendors: List<VendorDTO>? = null,
) )
@ -24,4 +23,16 @@ internal sealed interface PatchConsent {
@SerialName("isAccepted") val isAccepted: Boolean, @SerialName("isAccepted") val isAccepted: Boolean,
) )
@Serializable
enum class PurposeStatusDTO {
@SerialName("accepted")
ACCEPTED,
@SerialName("refused")
REJECTED,
@SerialName("partial")
PARTIAL,
}
} }

View File

@ -1,22 +1,24 @@
package fr.openium.consentium.domain.repository package fr.openium.consentium.domain.repository
import fr.openium.consentium.api.adapter.toConsentState
import fr.openium.consentium.api.adapter.toPatchConsentPurposeDTO import fr.openium.consentium.api.adapter.toPatchConsentPurposeDTO
import fr.openium.consentium.api.adapter.toPurpose import fr.openium.consentium.api.adapter.toPurpose
import fr.openium.consentium.api.model.ConsentState
import fr.openium.consentium.api.model.Purpose import fr.openium.consentium.api.model.Purpose
import fr.openium.consentium.api.model.PurposeChoice 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.ConsentiumApi
import fr.openium.consentium.data.remote.model.PatchConsent import fr.openium.consentium.data.remote.model.PatchConsent
import fr.openium.consentium.domain.useCase.GetAuthTokenUseCase
import javax.inject.Inject import javax.inject.Inject
internal class ConsentiumRepository @Inject constructor( internal class ConsentiumRepository @Inject constructor(
private val consentiumApi: ConsentiumApi, private val consentiumApi: ConsentiumApi,
private val consentiumDataStore: ConsentiumDataStore, private val getAuthTokenUseCase: GetAuthTokenUseCase,
) { ) {
suspend fun getConsents(applicationId: String): ConsentiumRepositoryGetResponse { suspend fun getConsents(applicationId: String): ConsentiumRepositoryGetResponse {
val installationId = consentiumDataStore.getInstallationUniqueId() val authToken = getAuthTokenUseCase(applicationId)
val consentsResponse = consentiumApi.getConsents(applicationId, installationId) val consentsResponse = consentiumApi.getConsents(authToken)
return try { return try {
val consentsBody = if (consentsResponse.isSuccessful) { val consentsBody = if (consentsResponse.isSuccessful) {
consentsResponse.body() ?: throw Exception() consentsResponse.body() ?: throw Exception()
@ -25,7 +27,7 @@ internal class ConsentiumRepository @Inject constructor(
} }
return ConsentiumRepositoryGetResponse.GetConsentsSuccess( return ConsentiumRepositoryGetResponse.GetConsentsSuccess(
isValid = consentsBody.isValid, state = consentsBody.state?.toConsentState() ?: ConsentState.UNSET,
purposes = consentsBody.purposes?.map { purpose -> purpose.toPurpose() } ?: emptyList() purposes = consentsBody.purposes?.map { purpose -> purpose.toPurpose() } ?: emptyList()
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -37,12 +39,11 @@ internal class ConsentiumRepository @Inject constructor(
applicationId: String, applicationId: String,
consents: List<PurposeChoice>, consents: List<PurposeChoice>,
): ConsentiumRepositorySetResponse { ): ConsentiumRepositorySetResponse {
val installationId = consentiumDataStore.getInstallationUniqueId() val authToken = getAuthTokenUseCase(applicationId)
val setConsentResponse = consentiumApi.setConsents( val setConsentResponse = consentiumApi.setConsents(
applicationId = applicationId, token = authToken,
patchConsent = PatchConsent.PatchConsentPayloadDTO( patchConsent = PatchConsent.PatchConsentPayloadDTO(
installationId = installationId,
purposes = consents.map { it.toPatchConsentPurposeDTO() }, purposes = consents.map { it.toPatchConsentPurposeDTO() },
) )
) )
@ -61,7 +62,7 @@ internal interface ConsentiumRepositoryGetResponse {
data object Error : ConsentiumRepositoryGetResponse data object Error : ConsentiumRepositoryGetResponse
data class GetConsentsSuccess( data class GetConsentsSuccess(
val isValid: Boolean, val state: ConsentState,
val purposes: List<Purpose>, val purposes: List<Purpose>,
) : ConsentiumRepositoryGetResponse ) : ConsentiumRepositoryGetResponse

View File

@ -0,0 +1,23 @@
package fr.openium.consentium.domain.useCase
import javax.inject.Inject
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
interface GetAuthTokenUseCase {
suspend operator fun invoke(applicationId: String): String
}
class GetAuthTokenUseCaseImpl @Inject internal constructor(
private val getConsentiumUniqueInstallationIdUseCase: GetConsentiumUniqueInstallationIdUseCase,
) : GetAuthTokenUseCase {
@OptIn(ExperimentalEncodingApi::class)
override suspend operator fun invoke(applicationId: String): String {
val uniqueInstallationId = getConsentiumUniqueInstallationIdUseCase()
val clearToken = "$applicationId.$uniqueInstallationId"
val cipheredToken = Base64.Default.encode(clearToken.toByteArray())
return "Bearer $cipheredToken"
}
}

View File

@ -4,14 +4,14 @@ import fr.openium.consentium.data.local.ConsentiumDataStore
import javax.inject.Inject import javax.inject.Inject
interface GetConsentiumUniqueInstallationIdUseCase { interface GetConsentiumUniqueInstallationIdUseCase {
suspend fun invoke(): String suspend operator fun invoke(): String
} }
class GetConsentiumUniqueInstallationIdUseCaseImpl @Inject internal constructor( class GetConsentiumUniqueInstallationIdUseCaseImpl @Inject internal constructor(
private val consentiumDataStore: ConsentiumDataStore, private val consentiumDataStore: ConsentiumDataStore,
) : GetConsentiumUniqueInstallationIdUseCase { ) : GetConsentiumUniqueInstallationIdUseCase {
override suspend fun invoke(): String { override suspend operator fun invoke(): String {
return consentiumDataStore.getInstallationUniqueId() return consentiumDataStore.getInstallationUniqueId()
} }

View File

@ -4,6 +4,8 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.domain.useCase.GetAuthTokenUseCase
import fr.openium.consentium.domain.useCase.GetAuthTokenUseCaseImpl
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCase import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCase
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCaseImpl import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCaseImpl
@ -16,4 +18,8 @@ interface ConsentiumUseCaseModule {
getUniqueInstallationIdUseCaseImpl: GetConsentiumUniqueInstallationIdUseCaseImpl, getUniqueInstallationIdUseCaseImpl: GetConsentiumUniqueInstallationIdUseCaseImpl,
): GetConsentiumUniqueInstallationIdUseCase ): GetConsentiumUniqueInstallationIdUseCase
@Binds
fun bindGetAuthTokenUseCase(
getAuthTokenUseCaseImpl: GetAuthTokenUseCaseImpl,
): GetAuthTokenUseCase
} }