Merge pull request 'feat(CON-206) : [UI] - Create Data / Domain layer to retrive the configuration.' (#8) from CON-206 into develop

Reviewed-on: #8
Reviewed-by: Lucas Zborowski <l.zborowski@openium.fr>
This commit is contained in:
Louis Legrand 2024-12-13 15:45:08 +01:00
commit ae61624eaf
31 changed files with 485 additions and 12 deletions

View File

@ -1,6 +1,8 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.serialization)
alias(libs.plugins.ksp)
} }
android { android {
@ -37,6 +39,21 @@ dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
// Serialization
implementation(libs.kotlin.serialization)
// Retrofit
api(libs.retrofit)
implementation(libs.retrofitConverter)
implementation(libs.logging.interceptor)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// Timber
implementation(libs.timber)
// Tests // Tests
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -0,0 +1,49 @@
package fr.openium.consentium_ui.data.di
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.di.ConsentiumUrl
import fr.openium.consentium.data.di.OkHttpClientDefault
import fr.openium.consentium_ui.data.remote.ConsentiumUIApi
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
private val json by lazy {
Json {
isLenient = true
ignoreUnknownKeys = true
}
}
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
@Reusable
@Provides
fun provideConsentiumUIApi(
@ConsentiumUrl url: HttpUrl,
@OkHttpClientDefault okHttpClient: Lazy<OkHttpClient>,
): ConsentiumUIApi =
createRetrofit(
url,
okHttpClient
).create(ConsentiumUIApi::class.java)
private fun createRetrofit(url: HttpUrl, okHttpClient: Lazy<OkHttpClient>): Retrofit =
Retrofit.Builder()
.callFactory { request ->
okHttpClient.get().newCall(request)
}.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.baseUrl(url)
.build()
}

View File

@ -0,0 +1,15 @@
package fr.openium.consentium_ui.data.remote
import fr.openium.consentium_ui.data.remote.model.GetConsentConfigDTO
import retrofit2.Response
import retrofit2.http.GET
internal interface ConsentiumUIApi {
@GET("consent-config")
suspend fun getConsentConfig(
applicationID: String,
installationID: String,
): Response<GetConsentConfigDTO>
}

View File

@ -0,0 +1,16 @@
package fr.openium.consentium_ui.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class GetConsentConfigDTO(
@SerialName("id") val installationId: String,
@SerialName("name") val appName: String,
@SerialName("icon") val icon: String,
@SerialName("primaryColor") val primaryColor: String,
@SerialName("secondaryColor") val secondaryColor: String,
@SerialName("textColor") val textColor: String,
@SerialName("translation") val consentMainTextTranslation: List<MainConsentTextTranslationDTO>,
@SerialName("purposes") val purposes: List<PurposeDTO>,
)

View File

@ -0,0 +1,13 @@
package fr.openium.consentium_ui.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class MainConsentTextTranslationDTO(
@SerialName("id") val id: String,
@SerialName("lang") val language: String,
@SerialName("consentPageUrl") val consentPageUrl: String,
@SerialName("mainConsentText") val mainConsentText: String,
@SerialName("durationText") val durationText: String,
)

View File

@ -0,0 +1,14 @@
package fr.openium.consentium_ui.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class PurposeDTO(
@SerialName("identifier") val id: String,
@SerialName("order") val order: Int,
@SerialName("isRequired") val isRequired: Boolean,
@SerialName("isAccepted") val isAccepted: Boolean,
@SerialName("translations") val translations: List<PurposeTranslationDTO>,
@SerialName("vendors") val vendors: List<VendorDTO>,
)

View File

@ -0,0 +1,11 @@
package fr.openium.consentium_ui.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class PurposeTranslationDTO(
@SerialName("id") val id: String,
@SerialName("lang") val language: String,
@SerialName("text") val text: String,
)

View File

@ -0,0 +1,13 @@
package fr.openium.consentium_ui.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class VendorDTO(
@SerialName("identifier") val id: String,
@SerialName("order") val order: Int,
@SerialName("isRequired") val isRequired: Boolean,
@SerialName("isAccepted") val isAccepted: Boolean,
@SerialName("translations") val translations: List<VendorTranslationDTO>,
)

View File

@ -0,0 +1,11 @@
package fr.openium.consentium_ui.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class VendorTranslationDTO(
@SerialName("id") val id: String,
@SerialName("lang") val language: String,
@SerialName("text") val text: String,
)

View File

@ -0,0 +1,12 @@
package fr.openium.consentium_ui.domain.adapter
import fr.openium.consentium_ui.data.remote.model.GetConsentConfigDTO
import fr.openium.consentium_ui.domain.model.ContentConfigData
internal fun GetConsentConfigDTO.toConsentConfigData() =
ContentConfigData(
applicationName = appName,
iconUrl = icon,
mainTextTranslation = consentMainTextTranslation.toMainConsentTextTranslationDataList(),
purposes = purposes.toPurposeDataList()
)

View File

@ -0,0 +1,15 @@
package fr.openium.consentium_ui.domain.adapter
import fr.openium.consentium_ui.data.remote.model.MainConsentTextTranslationDTO
import fr.openium.consentium_ui.domain.model.MainConsentTextTranslationData
internal fun MainConsentTextTranslationDTO.toMainConsentTextTranslationData() =
MainConsentTextTranslationData(
id = id,
language = language,
consentPageUrl = consentPageUrl,
mainConsentText = mainConsentText,
durationText = durationText,
)
internal fun List<MainConsentTextTranslationDTO>.toMainConsentTextTranslationDataList() = map { it.toMainConsentTextTranslationData() }

View File

@ -0,0 +1,16 @@
package fr.openium.consentium_ui.domain.adapter
import fr.openium.consentium_ui.data.remote.model.PurposeDTO
import fr.openium.consentium_ui.domain.model.PurposeData
internal fun PurposeDTO.toPurposeData() =
PurposeData(
identifier = id,
isRequired = isRequired,
isAccepted = isAccepted,
order = order,
vendors = vendors.toVendorDataList(),
translations = translations.toPurposeTranslationDataList(),
)
internal fun List<PurposeDTO>.toPurposeDataList() = map { it.toPurposeData() }

View File

@ -0,0 +1,13 @@
package fr.openium.consentium_ui.domain.adapter
import fr.openium.consentium_ui.data.remote.model.PurposeTranslationDTO
import fr.openium.consentium_ui.domain.model.PurposeTranslationData
internal fun PurposeTranslationDTO.toPurposeTranslationData() =
PurposeTranslationData(
id = id,
language = language,
text = text,
)
internal fun List<PurposeTranslationDTO>.toPurposeTranslationDataList() = map { it.toPurposeTranslationData() }

View File

@ -0,0 +1,15 @@
package fr.openium.consentium_ui.domain.adapter
import fr.openium.consentium_ui.data.remote.model.VendorDTO
import fr.openium.consentium_ui.domain.model.VendorData
internal fun VendorDTO.toVendorData() =
VendorData(
identifier = id,
order = order,
isRequired = isRequired,
isAccepted = isAccepted,
translations = translations.toVendorTranslationDataList(),
)
internal fun List<VendorDTO>.toVendorDataList() = map { it.toVendorData() }

View File

@ -0,0 +1,13 @@
package fr.openium.consentium_ui.domain.adapter
import fr.openium.consentium_ui.data.remote.model.VendorTranslationDTO
import fr.openium.consentium_ui.domain.model.VendorTranslationData
internal fun VendorTranslationDTO.toVendorTranslationData() =
VendorTranslationData(
id = id,
language = language,
text = text,
)
internal fun List<VendorTranslationDTO>.toVendorTranslationDataList() = map { it.toVendorTranslationData() }

View File

@ -0,0 +1,19 @@
package fr.openium.consentium_ui.domain.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCase
import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCaseImpl
@Module
@InstallIn(SingletonComponent::class)
internal interface ConsentiumUseCaseModule {
@Binds
fun bindsGetConfigTextForLanguageUseCase(
getConfigTextForLanguageUseCaseImpl: GetConfigTextForLanguageUseCaseImpl,
): GetConfigTextForLanguageUseCase
}

View File

@ -0,0 +1,8 @@
package fr.openium.consentium_ui.domain.model
internal data class ContentConfigData(
val applicationName: String,
val iconUrl : String,
val mainTextTranslation: List<MainConsentTextTranslationData>,
val purposes: List<PurposeData>
)

View File

@ -0,0 +1,9 @@
package fr.openium.consentium_ui.domain.model
internal data class MainConsentTextTranslationData(
val id: String,
val language: String,
val consentPageUrl: String,
val mainConsentText: String,
val durationText: String,
)

View File

@ -0,0 +1,10 @@
package fr.openium.consentium_ui.domain.model
internal data class PurposeData(
val identifier: String,
val order: Int,
val isRequired: Boolean,
val isAccepted: Boolean,
val translations: List<PurposeTranslationData>,
val vendors: List<VendorData>,
)

View File

@ -0,0 +1,7 @@
package fr.openium.consentium_ui.domain.model
internal data class PurposeTranslationData(
val id: String,
val language: String,
val text: String,
)

View File

@ -0,0 +1,9 @@
package fr.openium.consentium_ui.domain.model
internal data class VendorData(
val identifier: String,
val order: Int,
val isRequired: Boolean,
val isAccepted: Boolean,
val translations: List<VendorTranslationData>,
)

View File

@ -0,0 +1,7 @@
package fr.openium.consentium_ui.domain.model
internal data class VendorTranslationData(
val id: String,
val language: String,
val text: String,
)

View File

@ -0,0 +1,41 @@
package fr.openium.consentium_ui.domain.repository
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCase
import fr.openium.consentium_ui.data.remote.ConsentiumUIApi
import fr.openium.consentium_ui.domain.adapter.toConsentConfigData
import fr.openium.consentium_ui.domain.model.ContentConfigData
import javax.inject.Inject
internal class ConsentiumRepository @Inject constructor(
private val getConsentiumUniqueInstallationIdUseCase: GetConsentiumUniqueInstallationIdUseCase,
private val consentiumUIApi: ConsentiumUIApi,
) {
suspend fun getConsentiumConfig(
applicationId: String,
): ConsentiumUIRepositoryResponse {
val installationId = getConsentiumUniqueInstallationIdUseCase.invoke()
val consentsResponse = consentiumUIApi.getConsentConfig(applicationId, installationId)
return try {
val consentsBody = if (consentsResponse.isSuccessful) {
consentsResponse.body() ?: throw Exception()
} else {
throw Exception()
}
ConsentiumUIRepositoryResponse.Success(consentsBody.toConsentConfigData())
} catch (e: Exception) {
ConsentiumUIRepositoryResponse.Error
}
}
}
internal interface ConsentiumUIRepositoryResponse {
data object Error : ConsentiumUIRepositoryResponse
data class Success(val contentConfigData: ContentConfigData) : ConsentiumUIRepositoryResponse
}

View File

@ -0,0 +1,83 @@
package fr.openium.consentium_ui.domain.usecase
import fr.openium.consentium_ui.domain.model.ContentConfigData
import javax.inject.Inject
private const val FALLBACK_LANGUAGE = "en"
internal interface GetConfigTextForLanguageUseCase {
suspend fun invoke(
language: String = FALLBACK_LANGUAGE,
configData: ContentConfigData,
): GetConfigTextForLanguageUseCaseResponce
}
internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetConfigTextForLanguageUseCase {
override suspend fun invoke(
language: String,
configData: ContentConfigData,
): GetConfigTextForLanguageUseCaseResponce {
return try {
val canIUseTheTranslation = configData.mainTextTranslation.any { it.language == language } &&
configData.purposes.all { purposeData ->
purposeData.translations.any { it.language == language } &&
purposeData.vendors.all { vendorData ->
vendorData.translations.any { it.language == language }
}
}
val languageToUse = if (canIUseTheTranslation) {
language
} else {
val isThereAGoodFallbackLanguage = configData.mainTextTranslation.any { it.language == FALLBACK_LANGUAGE } &&
configData.purposes.all { purposeData ->
purposeData.translations.any { it.language == FALLBACK_LANGUAGE } &&
purposeData.vendors.all { vendorData ->
vendorData.translations.any { it.language == FALLBACK_LANGUAGE }
}
}
if (isThereAGoodFallbackLanguage) {
FALLBACK_LANGUAGE
} else {
throw Exception()
}
}
val filteredConfigData = configData.copy(
mainTextTranslation = configData.mainTextTranslation.filter { it.language == languageToUse },
purposes = configData.purposes.map { purposeData ->
purposeData.copy(
translations = purposeData.translations.filter { it.language == languageToUse },
vendors = purposeData.vendors.map { vendorData ->
vendorData.copy(
translations = vendorData.translations.filter { it.language == languageToUse }
)
}
)
}
)
if (languageToUse == FALLBACK_LANGUAGE) {
GetConfigTextForLanguageUseCaseResponce.DefaultLanguage(filteredConfigData)
} else {
GetConfigTextForLanguageUseCaseResponce.Success(filteredConfigData)
}
} catch (e: Exception) {
GetConfigTextForLanguageUseCaseResponce.Error
}
}
}
internal interface GetConfigTextForLanguageUseCaseResponce {
data object Error : GetConfigTextForLanguageUseCaseResponce
data class Success(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponce
data class DefaultLanguage(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponce
}

View File

@ -16,7 +16,7 @@ import timber.log.Timber
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class DebugNetworkModule { class ConsentiumDebugNetworkModule {
@Provides @Provides
fun provideOkHttpBuilder( fun provideOkHttpBuilder(

View File

@ -9,10 +9,10 @@ import fr.openium.consentium.api.model.VendorIdentifier
import fr.openium.consentium.api.model.VendorStatus import fr.openium.consentium.api.model.VendorStatus
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
import fr.openium.consentium.data.di.RepositoryEntryPoint import fr.openium.consentium.domain.di.RepositoryEntryPoint
import fr.openium.consentium.data.repository.ConsentiumRepository import fr.openium.consentium.domain.repository.ConsentiumRepository
import fr.openium.consentium.data.repository.ConsentiumRepositoryGetResponse import fr.openium.consentium.domain.repository.ConsentiumRepositoryGetResponse
import fr.openium.consentium.data.repository.ConsentiumRepositorySetResponse import fr.openium.consentium.domain.repository.ConsentiumRepositorySetResponse
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow

View File

@ -28,7 +28,7 @@ private val json by lazy {
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal object NetworkModule { object NetworkModule {
@OkHttpClientDefault @OkHttpClientDefault
@Provides @Provides
@ -51,7 +51,7 @@ internal object NetworkModule {
@Reusable @Reusable
@Provides @Provides
fun provideConsentiumApi( internal fun provideConsentiumApi(
@ConsentiumUrl url: HttpUrl, @ConsentiumUrl url: HttpUrl,
@OkHttpClientDefault okHttpClient: Lazy<OkHttpClient>, @OkHttpClientDefault okHttpClient: Lazy<OkHttpClient>,
): ConsentiumApi = ): ConsentiumApi =
@ -71,8 +71,8 @@ internal object NetworkModule {
@Qualifier @Qualifier
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
internal annotation class ConsentiumUrl annotation class ConsentiumUrl
@Qualifier @Qualifier
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
internal annotation class OkHttpClientDefault annotation class OkHttpClientDefault

View File

@ -1,9 +1,9 @@
package fr.openium.consentium.data.di package fr.openium.consentium.domain.di
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.repository.ConsentiumRepository import fr.openium.consentium.domain.repository.ConsentiumRepository
@EntryPoint @EntryPoint
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View File

@ -1,4 +1,4 @@
package fr.openium.consentium.data.repository package fr.openium.consentium.domain.repository
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

View File

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

View File

@ -0,0 +1,19 @@
package fr.openium.consentium.domain.useCase.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCase
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCaseImpl
@Module
@InstallIn(SingletonComponent::class)
interface ConsentiumUseCaseModule {
@Binds
fun bindGetUniqueInstallationIdUseCase(
getUniqueInstallationIdUseCaseImpl: GetConsentiumUniqueInstallationIdUseCaseImpl,
): GetConsentiumUniqueInstallationIdUseCase
}