diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 463b333..16d3bbc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,10 +6,9 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) alias(libs.plugins.hilt) alias(libs.plugins.serialization) - + alias(libs.plugins.kotlin.compose) } // Keystore @@ -106,6 +105,8 @@ dependencies { implementation(libs.hilt.android) ksp(libs.hilt.compiler) implementation(libs.hilt.navigation.compose) + + // Libs analytics implementation(libs.matomo) implementation(libs.clarity) implementation(libs.ga4) diff --git a/app/src/main/java/fr/openium/consentium/MainActivity.kt b/app/src/main/java/fr/openium/consentium/MainActivity.kt index 6abe1c3..566097c 100644 --- a/app/src/main/java/fr/openium/consentium/MainActivity.kt +++ b/app/src/main/java/fr/openium/consentium/MainActivity.kt @@ -1,6 +1,6 @@ package fr.openium.consentium -import fr.openium.consentium.ui.navigation.DemoNavGraph +import DemoNavGraph import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -24,7 +24,7 @@ class MainActivity : ComponentActivity() { val navHostController = rememberNavController() ConsentiumTheme { - Scaffold( modifier = Modifier.fillMaxSize() ) { paddingValues -> + Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues -> Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/fr/openium/consentium/ui/navigation/DemoNavGraph.kt b/app/src/main/java/fr/openium/consentium/ui/navigation/DemoNavGraph.kt index b86b8eb..90dbdf0 100644 --- a/app/src/main/java/fr/openium/consentium/ui/navigation/DemoNavGraph.kt +++ b/app/src/main/java/fr/openium/consentium/ui/navigation/DemoNavGraph.kt @@ -1,17 +1,32 @@ -package fr.openium.consentium.ui.navigation - -import Destination import androidx.compose.animation.core.tween import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntOffset import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import fr.openium.consentium.api.Consentium import fr.openium.consentium.ui.screens.main.MainScreen import fr.openium.consentium.ui.screens.splash.SplashScreen +import fr.openium.consentium.ui.theme.Error +import fr.openium.consentium.ui.theme.OnPrimary +import fr.openium.consentium.ui.theme.OnSecondary +import fr.openium.consentium.ui.theme.OnSurface +import fr.openium.consentium.ui.theme.OnSurfaceVariant +import fr.openium.consentium.ui.theme.Primary +import fr.openium.consentium.ui.theme.Secondary +import fr.openium.consentium.ui.theme.Success +import fr.openium.consentium.ui.theme.SurfaceHigh +import fr.openium.consentium.ui.theme.SurfaceHighest +import fr.openium.consentium.ui.theme.SurfaceMiddle +import fr.openium.consentium.ui.theme.Tertiary +import fr.openium.consentium_ui.ui.components.ConsentiumComponent +import fr.openium.consentium_ui.ui.components.style.ConsentiumDefaults +import fr.openium.consentium_ui.ui.model.ConsentiumPageUI private const val NAV_ANIMATION_TIME = 500 @@ -43,23 +58,56 @@ fun DemoNavGraph(navHostController: NavHostController) { saveState = true } } + }, + navigateToConsent = { + navHostController.navigate(Destination.Consent(ConsentiumPageUI.GENERAL_CONSENT)) } ) } composable { MainScreen( - onGoToConsentDetail = { - navHostController.navigate(Destination.Consent) - }, onGoToConsentMaster = { - navHostController.navigate(Destination.Consent) - } + navHostController.navigate(Destination.Consent(ConsentiumPageUI.GENERAL_CONSENT)) + }, + onGoToConsentDetail = { + navHostController.navigate(Destination.Consent(ConsentiumPageUI.DETAILS_CONSENT)) + }, ) } - composable { - // TODO + composable { backStackEntry -> + val consent = backStackEntry.toRoute() + + val context = LocalContext.current + ConsentiumComponent( + defaultLandingPage = consent.landingPage, + onQuitConsent = { + navHostController.navigate(Destination.Main) { + popUpTo(navHostController.graph.findStartDestination().id) { + saveState = true + } + } + }, + consentium = Consentium( + context = context, + applicationId = "ApplicationId", + ), + colors = ConsentiumDefaults.colors( + primary = Primary, + onPrimary = OnPrimary, + secondary = Secondary, + onSecondary = OnSecondary, + tertiary = Tertiary, + onSurfaceVariant = OnSurfaceVariant, + onSurface = OnSurface, + error = Error, + surfaceHighest = SurfaceHighest, + surfaceHigh = SurfaceHigh, + surfaceMiddle = SurfaceMiddle, + success = Success, + ) + ) } } } diff --git a/app/src/main/java/fr/openium/consentium/ui/navigation/Destination.kt b/app/src/main/java/fr/openium/consentium/ui/navigation/Destination.kt index 6045891..ad28daa 100644 --- a/app/src/main/java/fr/openium/consentium/ui/navigation/Destination.kt +++ b/app/src/main/java/fr/openium/consentium/ui/navigation/Destination.kt @@ -1,3 +1,4 @@ +import fr.openium.consentium_ui.ui.model.ConsentiumPageUI import kotlinx.serialization.Serializable @@ -10,6 +11,8 @@ sealed interface Destination { data object Main : Destination @Serializable - data object Consent : Destination + data class Consent( + val landingPage: ConsentiumPageUI + ): Destination } diff --git a/app/src/main/java/fr/openium/consentium/ui/screens/consent/ConsentScreen.kt b/app/src/main/java/fr/openium/consentium/ui/screens/consent/ConsentScreen.kt deleted file mode 100644 index d0a7060..0000000 --- a/app/src/main/java/fr/openium/consentium/ui/screens/consent/ConsentScreen.kt +++ /dev/null @@ -1,10 +0,0 @@ -package fr.openium.consentium.ui.screens.consent - -import androidx.compose.runtime.Composable - -@Composable -fun ConsentScreen() { - - - -} \ No newline at end of file diff --git a/app/src/main/java/fr/openium/consentium/ui/screens/main/MainScreen.kt b/app/src/main/java/fr/openium/consentium/ui/screens/main/MainScreen.kt index 3b362d6..1879e5e 100644 --- a/app/src/main/java/fr/openium/consentium/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/fr/openium/consentium/ui/screens/main/MainScreen.kt @@ -43,7 +43,5 @@ fun MainScreen( text = "Go to Consent Detail" ) } - } - } \ No newline at end of file diff --git a/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreen.kt b/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreen.kt index 43599c7..59054b4 100644 --- a/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreen.kt +++ b/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreen.kt @@ -10,36 +10,51 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import fr.openium.consentium.api.Consentium +import fr.openium.consentium.api.state.FetchConsentiumState @Composable fun SplashScreen( navigateToMain: () -> Unit, - viewModel: SplashScreenViewModel = hiltViewModel(), + navigateToConsent: () -> Unit, ) { - // State - val currentState by viewModel.state.collectAsState() + // Property + val context = LocalContext.current + val consentium = remember { Consentium(context = context, applicationId = "DemoApplicationId") } // Effect LaunchedEffect(Unit) { - viewModel.initMain() - } + consentium.fetchConsentState.collect { consentState -> + when (consentState) { + FetchConsentiumState.Idle, + FetchConsentiumState.Loading -> {} - LaunchedEffect(currentState) { - when (val state = currentState) { - is SplashScreenViewModel.State.Loaded -> { - if (state.isSplashEnded) { + FetchConsentiumState.Error -> { + // Handle error + consentium.fetchConsents() + } + + is FetchConsentiumState.Invalid -> { + navigateToConsent() + } + + is FetchConsentiumState.Valid -> { + // The tracking services should be initialized here navigateToMain() } } } } + LaunchedEffect(Unit) { + consentium.fetchConsents() + } + // View Column( modifier = Modifier.fillMaxSize(), @@ -47,7 +62,7 @@ fun SplashScreen( verticalArrangement = Arrangement.Center ) { - Text("Splash Screen") + Text("Splash Screen") // TODO Spacer(modifier = Modifier.height(14.dp)) diff --git a/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreenViewModel.kt b/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreenViewModel.kt deleted file mode 100644 index 4767e08..0000000 --- a/app/src/main/java/fr/openium/consentium/ui/screens/splash/SplashScreenViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -package fr.openium.consentium.ui.screens.splash - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SplashScreenViewModel @Inject constructor( -) : ViewModel() { - - private val _state = MutableStateFlow(State.Loaded(false)) - val state: StateFlow = _state - - fun initMain() { - viewModelScope.launch { - delay(1500L) - _state.value = State.Loaded(true) - } - } - - sealed interface State { - data class Loaded(val isSplashEnded: Boolean) : State - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/openium/consentium/ui/theme/Color.kt b/app/src/main/java/fr/openium/consentium/ui/theme/Color.kt index ea1f50a..ce83fff 100644 --- a/app/src/main/java/fr/openium/consentium/ui/theme/Color.kt +++ b/app/src/main/java/fr/openium/consentium/ui/theme/Color.kt @@ -2,10 +2,21 @@ package fr.openium.consentium.ui.theme import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) +val Primary = Color(0xFF70ACDC) +val OnPrimary = Color(0XFFFFFFFF) -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Secondary = Color(0xFFF29413) +val OnSecondary = Color(0xFFFFFFFF) + +val Tertiary = Color(0xFF3470A0) + +val OnSurfaceVariant = Color(0xFF3470A0) +val OnSurface = Color(0xFF163752) + +val Error = Color(0xFF3470A0) + +val SurfaceHighest = Color(0xFFFFFFFF) +val SurfaceHigh = Color(0xFFF2F8FC) +val SurfaceMiddle = Color(0xFFD6E6F5) + +val Success = Color(0xFF479B3F) \ No newline at end of file diff --git a/app/src/main/java/fr/openium/consentium/ui/theme/Theme.kt b/app/src/main/java/fr/openium/consentium/ui/theme/Theme.kt index ca7b84f..001768d 100644 --- a/app/src/main/java/fr/openium/consentium/ui/theme/Theme.kt +++ b/app/src/main/java/fr/openium/consentium/ui/theme/Theme.kt @@ -1,39 +1,29 @@ package fr.openium.consentium.ui.theme -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 + primary = Primary, + secondary = Secondary, + tertiary = Tertiary ) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 + primary = Primary, + secondary = Secondary, + tertiary = Tertiary ) @Composable fun ConsentiumTheme( darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = true, content: @Composable () -> Unit, ) { val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme else -> LightColorScheme } diff --git a/consentium-ui/build.gradle.kts b/consentium-ui/build.gradle.kts index d8afeb1..b6a711b 100644 --- a/consentium-ui/build.gradle.kts +++ b/consentium-ui/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.serialization) alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.hilt) } android { @@ -19,7 +21,10 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } @@ -45,6 +50,10 @@ android { kotlinOptions { jvmTarget = "11" } + buildFeatures { + compose = true + } + } dependencies { @@ -54,6 +63,22 @@ dependencies { // AndroidX implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) + implementation(libs.androidx.runtime.android) + implementation(libs.androidx.foundation.android) + implementation(libs.androidx.ui.android) + implementation(libs.androidx.foundation.layout.android) + implementation(libs.androidx.ui.tooling.preview.android) + implementation(libs.androidx.material3.android) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.activity.compose) + + + //Compose + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.material3) // Serialization implementation(libs.kotlin.serialization) @@ -65,13 +90,20 @@ dependencies { // Hilt implementation(libs.hilt.android) + implementation(libs.hilt.navigation.compose) ksp(libs.hilt.compiler) // Timber implementation(libs.timber) + // Coil + implementation(libs.coil) + implementation(libs.coil.network) + // Tests testImplementation(libs.test.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.test.espresso) + + } \ No newline at end of file diff --git a/consentium-ui/src/main/AndroidManifest.xml b/consentium-ui/src/main/AndroidManifest.xml index a5918e6..a880029 100644 --- a/consentium-ui/src/main/AndroidManifest.xml +++ b/consentium-ui/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/di/NetworkModule.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/di/NetworkModule.kt index 5afcbaf..c397779 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/di/NetworkModule.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/di/NetworkModule.kt @@ -9,8 +9,8 @@ import dagger.hilt.components.SingletonComponent import fr.openium.consentium.data.di.ConsentiumUrl import fr.openium.consentium.data.di.OkHttpClientDefault import fr.openium.consentium_ui.BuildConfig -import fr.openium.consentium_ui.api.mock.ConsentiumUIMockApi import fr.openium.consentium_ui.data.remote.ConsentiumUIApi +import fr.openium.consentium_ui.data.remote.mock.ConsentiumUIMockApi import kotlinx.serialization.json.Json import okhttp3.HttpUrl import okhttp3.MediaType.Companion.toMediaType diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/mock/ConsentiumMock.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/mock/ConsentiumMock.kt deleted file mode 100644 index 65d610b..0000000 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/mock/ConsentiumMock.kt +++ /dev/null @@ -1,25 +0,0 @@ -package fr.openium.consentium_ui.api.mock - -import fr.openium.consentium_ui.data.remote.ConsentiumUIApi -import fr.openium.consentium_ui.data.remote.model.GetConsentConfigDTO -import retrofit2.Response -import java.util.UUID - -internal object ConsentiumUIMockApi : ConsentiumUIApi { - - private val consents = GetConsentConfigDTO( - installationId = UUID.randomUUID().toString(), - appName = "Consentium", - icon = "https://www.example.com/icon.png", - primaryColor = "#FF0000", - secondaryColor = "#00FF00", - textColor = "#0000FF", - consentMainTextTranslation = emptyList(), - purposes = emptyList(), - ) - - override suspend fun getConsentConfig(applicationID: String, installationID: String): Response { - return Response.success(consents) - } - -} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/mock/ConsentiumUIMock.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/mock/ConsentiumUIMock.kt new file mode 100644 index 0000000..e52727f --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/mock/ConsentiumUIMock.kt @@ -0,0 +1,201 @@ +package fr.openium.consentium_ui.data.remote.mock + +import fr.openium.consentium_ui.data.remote.ConsentiumUIApi +import fr.openium.consentium_ui.data.remote.model.GetConsentConfigDTO +import fr.openium.consentium_ui.data.remote.model.MainConsentTextTranslationDTO +import fr.openium.consentium_ui.data.remote.model.PurposeDTO +import fr.openium.consentium_ui.data.remote.model.PurposeStatusDTO +import fr.openium.consentium_ui.data.remote.model.PurposeTranslationDTO +import fr.openium.consentium_ui.data.remote.model.VendorDTO +import fr.openium.consentium_ui.data.remote.model.VendorTranslationDTO +import retrofit2.Response +import java.util.UUID + +internal object ConsentiumUIMockApi : ConsentiumUIApi { + + private val consents = GetConsentConfigDTO( + installationId = UUID.randomUUID().toString(), + appName = "Consentium", + icon = "https://amp.openium.fr/openium.png", + primaryColor = "#FF0000", + secondaryColor = "#00FF00", + textColor = "#0000FF", + consentMainTextTranslation = listOf( + MainConsentTextTranslationDTO( + id = "UUID", + language = "fr", + consentPageUrl = "https:consentium.fr", + mainConsentText = "

[Nom de l’application] utilise des cookies pour différents objectifs : faire fonctionner l’application, améliorer nos services en mesurant l’efficacité de nos contenus et afficher des publicités susceptibles de vous intéresser.

\n

En cliquant sur “Accepter et fermer”, vous acceptez cette utilisation sur l’application mobile. Vous pouvez également paramétrer vos choix en cliquant sur “Paramétrer mes choix” ou refuser ces cookies en cliquant sur “Continuer sans accepter”. Vous pouvez changer d’avis à tout moment depuis les paramètres de votre compte via l’onglet “Notifications et cookies”

\n", + durationText = "

Nous conservons votre choix pendant 12 mois. Vous pouvez changer d’avis à tout moment depuis les paramètres de votre compte via l’onglet “Notification et cookies” tetetettetettetetstts

\n" + ) + ), + purposes = listOf( + PurposeDTO( + id = "purpose-required", + order = 0, + isRequired = true, + isAccepted = PurposeStatusDTO.ACCEPTED, + translations = listOf( + PurposeTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaire au fonctionnement de l’application. Ils permettent de vérifier la stabilité technique de l’application et de mesurer notre audience. Ces données ne sont utilisées que pour notre compte exclusif (en ne produisant que des données statistiques anonymes).

", + name = "Nécessaire" + ) + ), + vendors = listOf( + VendorDTO( + id = "vendors-crashlytics", + order = 0, + isAccepted = true, + isRequired = true, + translations = listOf( + VendorTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaire au fonctionnement de l’application. Ils permettent de vérifier la stabilité technique de l’application et de mesurer notre audience. Ces données ne sont utilisées que pour notre compte exclusif (en ne produisant que des données statistiques anonymes).

" + ) + ) + ), + VendorDTO( + id = "vendors-matomo", + order = 1, + isAccepted = true, + isRequired = false, + translations = listOf( + VendorTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaire au fonctionnement de l’application. Ils permettent de vérifier la stabilité technique de l’application et de mesurer notre audience. Ces données ne sont utilisées que pour notre compte exclusif (en ne produisant que des données statistiques anonymes).

" + ) + ) + ) + ) + ), + PurposeDTO( + id = "purpose-advertising", + order = 1, + isRequired = false, + isAccepted = PurposeStatusDTO.REJECTED, + translations = listOf( + PurposeTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour afficher des publicités susceptibles de vous intéresser. Ils permettent de mesurer l’efficacité de nos campagnes publicitaires et de personnaliser les publicités affichées.

", + name = "Publicité" + ) + ), + vendors = listOf( + VendorDTO( + id = "vendors-admob", + order = 0, + isAccepted = true, + isRequired = false, + translations = listOf( + VendorTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour afficher des publicités susceptibles de vous intéresser. Ils permettent de mesurer l’efficacité de nos campagnes publicitaires et de personnaliser les publicités affichées.

" + ) + ) + ) + ) + ), + PurposeDTO( + id = "purpose-analytics", + order = 2, + isRequired = false, + isAccepted = PurposeStatusDTO.ACCEPTED, + translations = listOf( + PurposeTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour mesurer l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.

", + name = "Analyse" + ) + ), + vendors = listOf( + VendorDTO( + id = "vendors-firebase", + order = 0, + isAccepted = true, + isRequired = false, + translations = listOf( + VendorTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour mesurer l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.

" + ) + ) + ) + ) + ), + PurposeDTO( + id = "purpose-personalization", + order = 3, + isRequired = false, + isAccepted = PurposeStatusDTO.ACCEPTED, + translations = listOf( + PurposeTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour mesurer l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.

", + name = "Personnalisation" + ) + ), + vendors = listOf( + VendorDTO( + id = "vendors-firebase", + order = 0, + isAccepted = true, + isRequired = false, + translations = listOf( + VendorTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour mesurer l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.

" + ) + ) + ) + ) + ), + PurposeDTO( + id = "purpose-social", + order = 4, + isRequired = false, + isAccepted = PurposeStatusDTO.ACCEPTED, + translations = listOf( + PurposeTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour mesurer l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.

", + name = "Social" + ) + ), + vendors = listOf( + VendorDTO( + id = "vendors-firebase", + order = 0, + isAccepted = true, + isRequired = false, + translations = listOf( + VendorTranslationDTO( + id = "UUID", + language = "fr", + text = "

Ces traceurs sont nécessaires pour mesurer l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.

" + ) + ) + ) + ) + ) + ) + ) + + override suspend fun getConsentConfig( + applicationID: String, + installationID: String, + ): Response { + return Response.success(consents) + } + +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeDTO.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeDTO.kt index c3bcff5..4281a71 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeDTO.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeDTO.kt @@ -8,7 +8,7 @@ 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("isAccepted") val isAccepted: PurposeStatusDTO, @SerialName("translations") val translations: List, @SerialName("vendors") val vendors: List, -) \ No newline at end of file +) diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeStatusDTO.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeStatusDTO.kt new file mode 100644 index 0000000..9476710 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeStatusDTO.kt @@ -0,0 +1,16 @@ +package fr.openium.consentium_ui.data.remote.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal enum class PurposeStatusDTO { + @SerialName("ACCEPTED") + ACCEPTED, + + @SerialName("REJECTED") + REJECTED, + + @SerialName("NOT_DEFINED") + NOT_DEFINED, +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeTranslationDTO.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeTranslationDTO.kt index 4c733ef..b6951aa 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeTranslationDTO.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/data/remote/model/PurposeTranslationDTO.kt @@ -8,4 +8,5 @@ internal data class PurposeTranslationDTO( @SerialName("id") val id: String, @SerialName("lang") val language: String, @SerialName("text") val text: String, + @SerialName("name") val name: String, ) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeDataAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeDataAdapter.kt index 9288100..e48e322 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeDataAdapter.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeDataAdapter.kt @@ -7,7 +7,7 @@ internal fun PurposeDTO.toPurposeData() = PurposeData( identifier = id, isRequired = isRequired, - isAccepted = isAccepted, + isAccepted = isAccepted.toPurposeStatusData(), order = order, vendors = vendors.toVendorDataList(), translations = translations.toPurposeTranslationDataList(), diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeStatusDataAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeStatusDataAdapter.kt new file mode 100644 index 0000000..d688a82 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeStatusDataAdapter.kt @@ -0,0 +1,12 @@ +package fr.openium.consentium_ui.domain.adapter + +import fr.openium.consentium_ui.data.remote.model.PurposeStatusDTO +import fr.openium.consentium_ui.domain.model.PurposeStatusData + +internal fun PurposeStatusDTO.toPurposeStatusData(): PurposeStatusData { + return when (this) { + PurposeStatusDTO.ACCEPTED -> PurposeStatusData.ACCEPTED + PurposeStatusDTO.REJECTED -> PurposeStatusData.REJECTED + PurposeStatusDTO.NOT_DEFINED -> PurposeStatusData.NOT_DEFINED + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeTranslationDataAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeTranslationDataAdapter.kt index f0274ec..004d115 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeTranslationDataAdapter.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/adapter/PurposeTranslationDataAdapter.kt @@ -8,6 +8,7 @@ internal fun PurposeTranslationDTO.toPurposeTranslationData() = id = id, language = language, text = text, + name = name, ) internal fun List.toPurposeTranslationDataList() = map { it.toPurposeTranslationData() } \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/di/ConsentiumUseCaseModule.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/di/ConsentiumUseCaseModule.kt index 283a320..e032491 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/di/ConsentiumUseCaseModule.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/di/ConsentiumUseCaseModule.kt @@ -4,6 +4,8 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import fr.openium.consentium_ui.domain.usecase.GetApplicationLanguageUseCase +import fr.openium.consentium_ui.domain.usecase.GetApplicationLanguageUseCaseImpl import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCase import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCaseImpl @@ -16,4 +18,9 @@ internal interface ConsentiumUseCaseModule { getConfigTextForLanguageUseCaseImpl: GetConfigTextForLanguageUseCaseImpl, ): GetConfigTextForLanguageUseCase + @Binds + fun bindGetApplicationLangageUseCase( + getApplicationLangageUseCaseImpl: GetApplicationLanguageUseCaseImpl, + ): GetApplicationLanguageUseCase + } \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeData.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeData.kt index 9b40568..4f751aa 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeData.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeData.kt @@ -4,7 +4,7 @@ internal data class PurposeData( val identifier: String, val order: Int, val isRequired: Boolean, - val isAccepted: Boolean, + val isAccepted: PurposeStatusData, val translations: List, val vendors: List, ) diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeStatusData.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeStatusData.kt new file mode 100644 index 0000000..059a684 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeStatusData.kt @@ -0,0 +1,7 @@ +package fr.openium.consentium_ui.domain.model + +internal enum class PurposeStatusData { + ACCEPTED, + REJECTED, + NOT_DEFINED, +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeTranslationData.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeTranslationData.kt index a305e91..e32f2dd 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeTranslationData.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/model/PurposeTranslationData.kt @@ -4,4 +4,5 @@ internal data class PurposeTranslationData( val id: String, val language: String, val text: String, + val name: String, ) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/repository/ConsentiumUIRepository.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/repository/ConsentiumUIRepository.kt index 4161123..49fa1a5 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/repository/ConsentiumUIRepository.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/repository/ConsentiumUIRepository.kt @@ -10,7 +10,6 @@ internal class ConsentiumRepository @Inject constructor( private val getConsentiumUniqueInstallationIdUseCase: GetConsentiumUniqueInstallationIdUseCase, private val consentiumUIApi: ConsentiumUIApi, ) { - suspend fun getConsentiumConfig( applicationId: String, ): ConsentiumUIRepositoryResponse { diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetApplicationLanguageUseCase.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetApplicationLanguageUseCase.kt new file mode 100644 index 0000000..002864b --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetApplicationLanguageUseCase.kt @@ -0,0 +1,14 @@ +package fr.openium.consentium_ui.domain.usecase + +import android.content.Context +import javax.inject.Inject + +internal interface GetApplicationLanguageUseCase { + suspend operator fun invoke(context: Context): String +} + +internal class GetApplicationLanguageUseCaseImpl @Inject constructor() : GetApplicationLanguageUseCase { + override suspend fun invoke(context: Context): String { + return context.resources.configuration.locales[0].language + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetConfigTextForLanguage.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetConfigTextForLanguageUseCase.kt similarity index 62% rename from consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetConfigTextForLanguage.kt rename to consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetConfigTextForLanguageUseCase.kt index 3b8ca2c..40db602 100644 --- a/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetConfigTextForLanguage.kt +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/domain/usecase/GetConfigTextForLanguageUseCase.kt @@ -6,39 +6,42 @@ import javax.inject.Inject private const val FALLBACK_LANGUAGE = "en" internal interface GetConfigTextForLanguageUseCase { - suspend fun invoke( + suspend operator fun invoke( language: String = FALLBACK_LANGUAGE, configData: ContentConfigData, - ): GetConfigTextForLanguageUseCaseResponce + ): GetConfigTextForLanguageUseCaseResponse } -internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetConfigTextForLanguageUseCase { +internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : + GetConfigTextForLanguageUseCase { override suspend fun invoke( language: String, configData: ContentConfigData, - ): GetConfigTextForLanguageUseCaseResponce { + ): GetConfigTextForLanguageUseCaseResponse { 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 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 } - } - } + 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 { @@ -61,23 +64,24 @@ internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetCo ) if (languageToUse == FALLBACK_LANGUAGE) { - GetConfigTextForLanguageUseCaseResponce.DefaultLanguage(filteredConfigData) + GetConfigTextForLanguageUseCaseResponse.DefaultLanguage(filteredConfigData) } else { - GetConfigTextForLanguageUseCaseResponce.Success(filteredConfigData) + GetConfigTextForLanguageUseCaseResponse.Success(filteredConfigData) } } catch (e: Exception) { - GetConfigTextForLanguageUseCaseResponce.Error + GetConfigTextForLanguageUseCaseResponse.Error } } } -internal interface GetConfigTextForLanguageUseCaseResponce { +internal interface GetConfigTextForLanguageUseCaseResponse { - data object Error : GetConfigTextForLanguageUseCaseResponce + data object Error : GetConfigTextForLanguageUseCaseResponse - data class Success(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponce + data class Success(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponse - data class DefaultLanguage(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponce + data class DefaultLanguage(val configData: ContentConfigData) : + GetConfigTextForLanguageUseCaseResponse } diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/ConsentiumUIScreen.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/ConsentiumUIScreen.kt new file mode 100644 index 0000000..09e0cf9 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/ConsentiumUIScreen.kt @@ -0,0 +1,101 @@ +package fr.openium.consentium_ui.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut +import androidx.compose.animation.togetherWith +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.IntOffset +import fr.openium.consentium_ui.ui.components.ConsentiumUIDetailConsentComponent +import fr.openium.consentium_ui.ui.components.ConsentiumUIErrorComponent +import fr.openium.consentium_ui.ui.components.ConsentiumUIGeneralConsentComponent +import fr.openium.consentium_ui.ui.components.ConsentiumUILoadingComponent +import fr.openium.consentium_ui.ui.model.ConsentiumPageUI +import fr.openium.consentium_ui.ui.model.DetailConsentUI +import fr.openium.consentium_ui.ui.model.LoadingElement +import fr.openium.consentium_ui.ui.state.ConsentiumUIState + +@Composable +internal fun ConsentiumScreen( + page: ConsentiumPageUI, + state: ConsentiumUIState, + loadingElement: LoadingElement, + onNavigateBack: () -> Unit, + onAcceptAndClose: (consents: DetailConsentUI) -> Unit, + onDenyAndClose: (consents: DetailConsentUI) -> Unit, + onSaveAndCloseDetails: (consents: DetailConsentUI) -> Unit, + onNavigateToDetails: () -> Unit, + onClickCookiesPolicies: () -> Unit, +) { + when (state) { + is ConsentiumUIState.Loading -> { + ConsentiumUILoadingComponent() + } + + is ConsentiumUIState.Error -> { + ConsentiumUIErrorComponent( + errorMessage = state.message, + ) + } + + is ConsentiumUIState.Loaded -> { + AnimatedContent( + targetState = page, + transitionSpec = { + when (page) { + ConsentiumPageUI.GENERAL_CONSENT -> { + slideIn( + initialOffset = { fullSize -> IntOffset(-fullSize.width, 0) } + ).togetherWith( + slideOut( + targetOffset = { fullSize -> IntOffset(fullSize.width, 0) } + ) + ) + } + + ConsentiumPageUI.DETAILS_CONSENT -> { + slideIn( + initialOffset = { fullSize -> IntOffset(fullSize.width, 0) } + ).togetherWith( + slideOut( + targetOffset = { fullSize -> IntOffset(-fullSize.width, 0) } + ) + ) + } + } + } + ) { currentPage -> + + when (currentPage) { + ConsentiumPageUI.GENERAL_CONSENT -> { + ConsentiumUIGeneralConsentComponent( + generalConsentUI = state.generalConsentUI, + onNavigateToDetails = onNavigateToDetails, + onAcceptAndClose = { + onAcceptAndClose(state.detailConsentUI) + }, + onDenyAndClose = { + onDenyAndClose(state.detailConsentUI) + }, + onClickCookiesPolicies = onClickCookiesPolicies, + loadingElement = loadingElement + ) + } + + ConsentiumPageUI.DETAILS_CONSENT -> { + ConsentiumUIDetailConsentComponent( + detailConsentUI = state.detailConsentUI, + onNavigateBack = onNavigateBack, + onSave = { + onSaveAndCloseDetails(state.detailConsentUI) + }, + loadingElement = loadingElement + ) + } + } + + } + } + } +} + diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/ConsentiumUIViewModel.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/ConsentiumUIViewModel.kt new file mode 100644 index 0000000..cca69a1 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/ConsentiumUIViewModel.kt @@ -0,0 +1,74 @@ +package fr.openium.consentium_ui.ui + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import fr.openium.consentium_ui.domain.repository.ConsentiumRepository +import fr.openium.consentium_ui.domain.repository.ConsentiumUIRepositoryResponse +import fr.openium.consentium_ui.domain.usecase.GetApplicationLanguageUseCase +import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCase +import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCaseResponse +import fr.openium.consentium_ui.ui.adapter.toDetailConsentUI +import fr.openium.consentium_ui.ui.adapter.toGeneralConsentUI +import fr.openium.consentium_ui.ui.state.ConsentiumUIState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class ConsentiumUIViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val consentiumUIRepository: ConsentiumRepository, + private val configTextLanguageUseCase: GetConfigTextForLanguageUseCase, + private val getApplicationLanguageUseCase: GetApplicationLanguageUseCase, +) : ViewModel() { + + private val _state = MutableStateFlow(ConsentiumUIState.Loading) + val state: StateFlow by lazy { _state.asStateFlow() } + + fun init(appId: String) { + viewModelScope.launch { + + _state.value = ConsentiumUIState.Loading + + when (val consentiumUIFetchResponse = consentiumUIRepository.getConsentiumConfig(appId)) { + + is ConsentiumUIRepositoryResponse.Success -> { + val consentUIResponse = + configTextLanguageUseCase(getApplicationLanguageUseCase(context), consentiumUIFetchResponse.contentConfigData) + + when (val consentUI = consentUIResponse) { + + is GetConfigTextForLanguageUseCaseResponse.Success -> { + _state.emit(ConsentiumUIState.Loaded( + generalConsentUI = consentUI.configData.toGeneralConsentUI(), + detailConsentUI = consentUI.configData.toDetailConsentUI(), + )) + } + + is GetConfigTextForLanguageUseCaseResponse.DefaultLanguage -> { + _state.emit(ConsentiumUIState.Loaded( + generalConsentUI = consentUI.configData.toGeneralConsentUI(), + detailConsentUI = consentUI.configData.toDetailConsentUI(), + )) + } + + is GetConfigTextForLanguageUseCaseResponse.Error -> { + _state.emit(ConsentiumUIState.Error("Failed to load data")) + } + + } + } + + is ConsentiumUIRepositoryResponse.Error -> { + _state.value = ConsentiumUIState.Error("Failed to load data") + } + + } + } + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/DetailConsentUIAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/DetailConsentUIAdapter.kt new file mode 100644 index 0000000..ca35d90 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/DetailConsentUIAdapter.kt @@ -0,0 +1,31 @@ +package fr.openium.consentium_ui.ui.adapter + +import fr.openium.consentium.api.model.PurposeChoice +import fr.openium.consentium_ui.domain.model.ContentConfigData +import fr.openium.consentium_ui.ui.model.DetailConsentUI +import fr.openium.consentium_ui.ui.model.PurposeStatusUI +import fr.openium.consentium_ui.ui.model.PurposeUI + +internal fun ContentConfigData.toDetailConsentUI(): DetailConsentUI = DetailConsentUI( + conservationDurationText = mainTextTranslation.first().durationText, + purposes = purposes.sortedBy { it.order }.map { it.toPurposeUI() }, +) + +internal fun DetailConsentUI.toPurposeChoices(): List = purposes.map { + it.toPurposeChoice() +} + +internal fun DetailConsentUI.toDeniedPurposeChoices(): List = purposes.map { + it.toPurposeChoice().copy(isAccepted = false) +} + +internal fun DetailConsentUI.toAcceptedPurposeChoices(): List = purposes.map { + it.toPurposeChoice().copy(isAccepted = true) +} + +internal fun PurposeUI.toPurposeChoice(): PurposeChoice = + PurposeChoice( + purposeIdentifier = id, + isAccepted = isAccepted == PurposeStatusUI.ACCEPTED, + vendors = emptyList(), // Not in v1 + ) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/GeneralConsentUIAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/GeneralConsentUIAdapter.kt new file mode 100644 index 0000000..1d009f2 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/GeneralConsentUIAdapter.kt @@ -0,0 +1,12 @@ +package fr.openium.consentium_ui.ui.adapter + +import fr.openium.consentium_ui.domain.model.ContentConfigData +import fr.openium.consentium_ui.ui.model.GeneralConsentUI + +internal fun ContentConfigData.toGeneralConsentUI(): GeneralConsentUI = + GeneralConsentUI( + applicationName = applicationName, + iconUrl = iconUrl, + mainConsentText = mainTextTranslation.first().mainConsentText, + consentPageUrl = mainTextTranslation.first().consentPageUrl, + ) diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/PurposeStatusUIAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/PurposeStatusUIAdapter.kt new file mode 100644 index 0000000..b34c94e --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/PurposeStatusUIAdapter.kt @@ -0,0 +1,10 @@ +package fr.openium.consentium_ui.ui.adapter + +import fr.openium.consentium_ui.domain.model.PurposeStatusData +import fr.openium.consentium_ui.ui.model.PurposeStatusUI + +internal fun PurposeStatusData.toPurposeStatusUI(): PurposeStatusUI = when(this) { + PurposeStatusData.ACCEPTED -> PurposeStatusUI.ACCEPTED + PurposeStatusData.REJECTED -> PurposeStatusUI.REJECTED + PurposeStatusData.NOT_DEFINED -> PurposeStatusUI.NOT_DEFINED +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/PurposeUIAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/PurposeUIAdapter.kt new file mode 100644 index 0000000..75b1aef --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/PurposeUIAdapter.kt @@ -0,0 +1,13 @@ +package fr.openium.consentium_ui.ui.adapter + +import fr.openium.consentium_ui.domain.model.PurposeData +import fr.openium.consentium_ui.ui.model.PurposeUI + +internal fun PurposeData.toPurposeUI(): PurposeUI = PurposeUI( + id = identifier, + isRequired = isRequired, + isAccepted = isAccepted.toPurposeStatusUI(), + title = translations.first().name, + description = translations.first().text, + vendors = vendors.toVendorUIList(), +) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/VendorUIAdapter.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/VendorUIAdapter.kt new file mode 100644 index 0000000..7659a05 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/adapter/VendorUIAdapter.kt @@ -0,0 +1,11 @@ +package fr.openium.consentium_ui.ui.adapter + +import fr.openium.consentium_ui.domain.model.VendorData +import fr.openium.consentium_ui.ui.model.VendorUI + +internal fun VendorData.toVendorUI() = VendorUI( + id = identifier, + isAccepted = isAccepted, +) + +internal fun List.toVendorUIList() = map { it.toVendorUI() } \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIComponent.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIComponent.kt new file mode 100644 index 0000000..19dd652 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIComponent.kt @@ -0,0 +1,119 @@ +package fr.openium.consentium_ui.ui.components + +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import fr.openium.consentium.api.Consentium +import fr.openium.consentium.api.state.SetConsentiumState +import fr.openium.consentium_ui.R +import fr.openium.consentium_ui.ui.ConsentiumScreen +import fr.openium.consentium_ui.ui.ConsentiumUIViewModel +import fr.openium.consentium_ui.ui.adapter.toAcceptedPurposeChoices +import fr.openium.consentium_ui.ui.adapter.toDeniedPurposeChoices +import fr.openium.consentium_ui.ui.adapter.toPurposeChoices +import fr.openium.consentium_ui.ui.components.style.ConsentiumColors +import fr.openium.consentium_ui.ui.components.style.ConsentiumDefaults +import fr.openium.consentium_ui.ui.components.style.ConsentiumTypography +import fr.openium.consentium_ui.ui.components.style.LocalColors +import fr.openium.consentium_ui.ui.components.style.LocalTypography +import fr.openium.consentium_ui.ui.model.ConsentiumPageUI +import fr.openium.consentium_ui.ui.model.LoadingElement +import kotlinx.coroutines.launch + +@Composable +fun ConsentiumComponent( + consentium: Consentium, + onQuitConsent: () -> (Unit), + colors: ConsentiumColors = ConsentiumDefaults.colors(), + typography: ConsentiumTypography = ConsentiumDefaults.typography(), + defaultLandingPage: ConsentiumPageUI = ConsentiumPageUI.GENERAL_CONSENT, +) { + // Property + val viewModel: ConsentiumUIViewModel = hiltViewModel() + val loadingState by viewModel.state.collectAsStateWithLifecycle() + var currentPage by remember(defaultLandingPage) { mutableStateOf(defaultLandingPage) } + val scope = rememberCoroutineScope() + val context = LocalContext.current + val errorMessage = stringResource(id = R.string.save_consents_error_message) + val consentiumState by consentium.saveConsentState.collectAsStateWithLifecycle() + var loadingElement by remember { mutableStateOf(LoadingElement.NONE) } + + // Effect + LaunchedEffect(Unit) { + viewModel.init(consentium.applicationId) + } + + LaunchedEffect(consentiumState) { + when (consentiumState) { + SetConsentiumState.Idle, + SetConsentiumState.Loading, + -> { + } + + SetConsentiumState.Error -> { + Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() + } + + SetConsentiumState.Success -> { + onQuitConsent() + } + } + } + + // View + CompositionLocalProvider( + LocalColors provides colors, + LocalTypography provides typography, + ) { + ConsentiumScreen( + state = loadingState, + page = currentPage, + loadingElement = if (consentiumState is SetConsentiumState.Loading) loadingElement else LoadingElement.NONE, + onNavigateBack = { + when { + defaultLandingPage == ConsentiumPageUI.GENERAL_CONSENT && currentPage == ConsentiumPageUI.DETAILS_CONSENT -> { + currentPage = ConsentiumPageUI.GENERAL_CONSENT + } + + else -> { + onQuitConsent() + } + } + }, + onNavigateToDetails = { + currentPage = ConsentiumPageUI.DETAILS_CONSENT + }, + onAcceptAndClose = { consent -> + scope.launch { + loadingElement = LoadingElement.BUTTON + consentium.saveConsents(consent.toAcceptedPurposeChoices()) + } + }, + onDenyAndClose = { consent -> + scope.launch { + loadingElement = LoadingElement.LINK + consentium.saveConsents(consent.toDeniedPurposeChoices()) + } + }, + onSaveAndCloseDetails = { consent -> + scope.launch { + loadingElement = LoadingElement.BUTTON + consentium.saveConsents(consent.toPurposeChoices()) + } + }, + onClickCookiesPolicies = { + // TODO Open cookies policies + }, + ) + } +} diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIDetailConsentComponent.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIDetailConsentComponent.kt new file mode 100644 index 0000000..a83d86c --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIDetailConsentComponent.kt @@ -0,0 +1,108 @@ +package fr.openium.consentium_ui.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import fr.openium.consentium_ui.R +import fr.openium.consentium_ui.ui.components.core.PurposeComponent +import fr.openium.consentium_ui.ui.components.core.button.ConsentButton +import fr.openium.consentium_ui.ui.components.core.button.ConsentiumUIButtonStyle +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme +import fr.openium.consentium_ui.ui.model.DetailConsentUI +import fr.openium.consentium_ui.ui.model.LoadingElement +import fr.openium.consentium_ui.ui.utils.htmlToAnnotatedString + + +@Composable +internal fun ConsentiumUIDetailConsentComponent( + detailConsentUI: DetailConsentUI, + loadingElement: LoadingElement, + onNavigateBack: () -> Unit, + onSave: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + ) { + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth() + ) { + IconButton(onClick = { onNavigateBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = ConsentiumUITheme.colors.onSurface, + modifier = Modifier.size(26.dp), + ) + } + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = stringResource(R.string.parameters), + style = ConsentiumUITheme.typography.h2, + color = ConsentiumUITheme.colors.onSurface, + ) + } + + Spacer(modifier = Modifier.height(25.dp)) + + Text( + text = htmlToAnnotatedString(detailConsentUI.conservationDurationText), + style = ConsentiumUITheme.typography.p3, + color = ConsentiumUITheme.colors.onSurface, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + items(detailConsentUI.purposes) { purposeUI -> + PurposeComponent( + purposeUI = purposeUI, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + ConsentButton( + text = stringResource(R.string.save), + buttonStyle = ConsentiumUIButtonStyle.PRIMARY, + onclick = onSave, + isLoading = loadingElement == LoadingElement.BUTTON, + ) + + Spacer(modifier = Modifier.height(10.dp)) + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIErrorComponent.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIErrorComponent.kt new file mode 100644 index 0000000..4b18b74 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIErrorComponent.kt @@ -0,0 +1,27 @@ +package fr.openium.consentium_ui.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme + +@Composable +internal fun ConsentiumUIErrorComponent( + errorMessage: String, +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = errorMessage, + color = ConsentiumUITheme.colors.error, + style = ConsentiumUITheme.typography.b2, + ) + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIGeneralConsentComponent.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIGeneralConsentComponent.kt new file mode 100644 index 0000000..76ef21d --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUIGeneralConsentComponent.kt @@ -0,0 +1,119 @@ +package fr.openium.consentium_ui.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import fr.openium.consentium_ui.R +import fr.openium.consentium_ui.ui.components.core.TextLink +import fr.openium.consentium_ui.ui.components.core.button.ConsentButton +import fr.openium.consentium_ui.ui.components.core.button.ConsentiumUIButtonStyle +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme +import fr.openium.consentium_ui.ui.model.GeneralConsentUI +import fr.openium.consentium_ui.ui.model.LoadingElement +import fr.openium.consentium_ui.ui.utils.htmlToAnnotatedString + +@Composable +internal fun ConsentiumUIGeneralConsentComponent( + generalConsentUI: GeneralConsentUI, + loadingElement: LoadingElement, + onClickCookiesPolicies: () -> Unit, + onDenyAndClose: () -> Unit, + onAcceptAndClose: () -> Unit, + onNavigateToDetails: () -> Unit, +) { + // View + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + ) { + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + AsyncImage( + modifier = Modifier.heightIn(min = 48.dp), + model = generalConsentUI.iconUrl, + contentDescription = "Image", + contentScale = ContentScale.FillBounds, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = htmlToAnnotatedString(generalConsentUI.mainConsentText), + style = ConsentiumUITheme.typography.p3, + textAlign = TextAlign.Start, + color = ConsentiumUITheme.colors.onSurface, + ) + + Spacer(modifier = Modifier.height(20.dp)) + + TextLink( + text = stringResource(R.string.cookies), + onclick = onClickCookiesPolicies, + ) + + Spacer(modifier = Modifier.weight(1f)) + + TextLink( + text = stringResource(R.string.refuse), + onclick = onDenyAndClose, + isLoading = loadingElement == LoadingElement.LINK, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ConsentButton( + text = stringResource(R.string.accept), + buttonStyle = ConsentiumUIButtonStyle.PRIMARY, + onclick = onAcceptAndClose, + isLoading = loadingElement == LoadingElement.BUTTON, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ConsentButton( + text = stringResource(R.string.parameters), + buttonStyle = ConsentiumUIButtonStyle.SECONDARY, + onclick = onNavigateToDetails, + ) + + Spacer(modifier = Modifier.height(10.dp)) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun ConsentiumUIGeneralConsentComponentPreview() { + ConsentiumUIGeneralConsentComponent( + generalConsentUI = GeneralConsentUI( + iconUrl = "https://amp.openium.fr/openium.png", + mainConsentText = "[Nom de l’application] utilise des cookies pour différents objectifs : faire fonctionner l’application, améliorer nos services en mesurant l’efficacité de nos contenus et afficher des publicités susceptibles de vous intéresser. \n\n En cliquant sur “Accepter et fermer”, vous acceptez cette utilisation sur l’application mobile. Vous pouvez également paramétrer vos choix en cliquant sur “Paramétrer mes choix” ou refuser ces cookies en cliquant sur “Continuer sans accepter”. Vous pouvez changer d’avis à tout moment depuis les paramètres de votre compte via l’onglet “Notifications et cookies”.", + applicationName = "Application name", + consentPageUrl = "https://www.google.com" + ), + onClickCookiesPolicies = {}, + onDenyAndClose = {}, + onAcceptAndClose = {}, + onNavigateToDetails = {}, + loadingElement = LoadingElement.LINK, + ) +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUILoadingComponent.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUILoadingComponent.kt new file mode 100644 index 0000000..9fa06b1 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/ConsentiumUILoadingComponent.kt @@ -0,0 +1,23 @@ +package fr.openium.consentium_ui.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme + +@Composable +internal fun ConsentiumUILoadingComponent() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + color = ConsentiumUITheme.colors.primary + ) + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/PurposeComponent.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/PurposeComponent.kt new file mode 100644 index 0000000..3e7aab4 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/PurposeComponent.kt @@ -0,0 +1,104 @@ +package fr.openium.consentium_ui.ui.components.core + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import fr.openium.consentium_ui.R +import fr.openium.consentium_ui.ui.components.core.toggle.ConsentiumUISwitch +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme +import fr.openium.consentium_ui.ui.model.PurposeStatusUI +import fr.openium.consentium_ui.ui.model.PurposeUI +import fr.openium.consentium_ui.ui.utils.htmlToAnnotatedString + +@Composable +internal fun PurposeComponent( + purposeUI: PurposeUI, +) { + // Properties + var isChecked by remember { mutableStateOf(purposeUI.isAccepted != PurposeStatusUI.REJECTED) } + + // View + Column( + modifier = Modifier.fillMaxWidth(), + ) { + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = purposeUI.title, + style = ConsentiumUITheme.typography.b3, + color = ConsentiumUITheme.colors.onSurface, + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (purposeUI.isRequired) { + Text( + text = stringResource(id = R.string.require), + style = ConsentiumUITheme.typography.b3, + color = ConsentiumUITheme.colors.onSurface, + ) + } else { + ConsentiumUISwitch( + checked = isChecked, + onCheckedChange = { + isChecked = !isChecked + purposeUI.isAccepted = if (isChecked) PurposeStatusUI.ACCEPTED else PurposeStatusUI.REJECTED + purposeUI.vendors.forEach { vendorUI -> + vendorUI.isAccepted = isChecked + } + }, + ) + } + + /** To enable in v2 + Spacer(modifier = Modifier.width(10.dp)) + + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = ConsentiumUITheme.colors.onSurface, + ) + **/ + } + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = htmlToAnnotatedString(purposeUI.description), + style = ConsentiumUITheme.typography.p3, + color = ConsentiumUITheme.colors.onSurface, + ) + } +} + +@Preview +@Composable +private fun PurposeComponentPreview() { + PurposeComponent( + purposeUI = PurposeUI( + id = "1", + isRequired = true, + isAccepted = PurposeStatusUI.ACCEPTED, + title = "Title", + description = "Description", + vendors = emptyList(), + ) + ) +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/TextLink.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/TextLink.kt new file mode 100644 index 0000000..66de915 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/TextLink.kt @@ -0,0 +1,65 @@ +package fr.openium.consentium_ui.ui.components.core + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme + +@Composable +internal fun TextLink( + text: String, + onclick: () -> Unit, + color: Color = ConsentiumUITheme.colors.primary, + isLoading: Boolean = false, +) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onclick, + ) + ) { + Text( + text = text, + color = color, + style = ConsentiumUITheme.typography.b3, + textDecoration = TextDecoration.Underline + ) + + if (isLoading) { + Spacer(modifier = Modifier.width(10.dp)) + + CircularProgressIndicator( + color = color, + modifier = Modifier.size(14.dp), + strokeWidth = 3.dp + ) + } + } + +} + +@Preview +@Composable +private fun TextLinkPreview() { + TextLink( + text = "Continuer sans accepter", + onclick = {}, + isLoading = true, + ) +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/button/ConsentiumUIButton.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/button/ConsentiumUIButton.kt new file mode 100644 index 0000000..608e458 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/button/ConsentiumUIButton.kt @@ -0,0 +1,55 @@ +package fr.openium.consentium_ui.ui.components.core.button + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme + +@Composable +internal fun ConsentButton( + text: String, + buttonStyle: ConsentiumUIButtonStyle, + onclick: () -> Unit, + isLoading: Boolean = false, +) { + Button( + onClick = onclick, + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + shape = RoundedCornerShape(6.dp), + colors = ButtonDefaults.buttonColors(buttonStyle.backgroundColor()) + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = ConsentiumUITheme.colors.onPrimary, + ) + } else { + Text( + text = text, + color = ConsentiumUITheme.colors.onPrimary, + style = ConsentiumUITheme.typography.button, + ) + } + } +} + +@Preview +@Composable +private fun ConsentButtonPreview() { + ConsentButton( + text = "Accept and close", + buttonStyle = ConsentiumUIButtonStyle.PRIMARY, + onclick = {} + ) +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/button/ConsentiumUIButtonStyle.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/button/ConsentiumUIButtonStyle.kt new file mode 100644 index 0000000..4a50f39 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/button/ConsentiumUIButtonStyle.kt @@ -0,0 +1,18 @@ +package fr.openium.consentium_ui.ui.components.core.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme + +internal enum class ConsentiumUIButtonStyle { + PRIMARY, + SECONDARY, +} + +@Composable +internal fun ConsentiumUIButtonStyle.backgroundColor(): Color { + return when (this) { + ConsentiumUIButtonStyle.PRIMARY -> ConsentiumUITheme.colors.primary + ConsentiumUIButtonStyle.SECONDARY -> ConsentiumUITheme.colors.secondary + } +} diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/toggle/ConsentiumUISwitch.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/toggle/ConsentiumUISwitch.kt new file mode 100644 index 0000000..eabc223 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/core/toggle/ConsentiumUISwitch.kt @@ -0,0 +1,44 @@ +package fr.openium.consentium_ui.ui.components.core.toggle + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme + +@Composable +internal fun ConsentiumUISwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedTrackColor = ConsentiumUITheme.colors.secondary, + ), + ) +} + +@Preview +@Composable +private fun ConsentiumUISwitchPreview() { + Column { + ConsentiumUISwitch( + checked = true, + onCheckedChange = {}, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ConsentiumUISwitch( + checked = false, + onCheckedChange = {}, + ) + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/style/ConsentiumDefaults.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/style/ConsentiumDefaults.kt new file mode 100644 index 0000000..854d364 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/style/ConsentiumDefaults.kt @@ -0,0 +1,181 @@ +package fr.openium.consentium_ui.ui.components.style + +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import androidx.core.graphics.ColorUtils + + +object ConsentiumDefaults { + private const val OUTLINE_COLOR_BLEND_RATIO = 0.1f + + private object Typography { + private val DEFAULT_LINE_HEIGHT = 24.sp + private val LINE_HEIGHT_18 = 18.sp + private val LINE_HEIGHT_22 = 22.sp + private val DEFAULT_LETTER_SPACING = 0.5.sp + + val h1 = TextStyle( + fontSize = 32.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val h2 = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val b1 = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = DEFAULT_LINE_HEIGHT, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val p1 = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Normal, + lineHeight = DEFAULT_LINE_HEIGHT, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val b2 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = LINE_HEIGHT_22, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val p2 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + lineHeight = LINE_HEIGHT_22, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val b3 = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = LINE_HEIGHT_18, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val p3 = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = LINE_HEIGHT_18, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val button = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = DEFAULT_LETTER_SPACING + ) + + val sm2 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = LINE_HEIGHT_22, + letterSpacing = DEFAULT_LETTER_SPACING + ) + } + + @Composable + fun colors( + primary: Color = MaterialTheme.colorScheme.primary, + onPrimary: Color = MaterialTheme.colorScheme.onPrimary, + secondary: Color = MaterialTheme.colorScheme.background, + onSecondary: Color = MaterialTheme.colorScheme.surfaceVariant, + tertiary: Color = MaterialTheme.colorScheme.onBackground, + onSurfaceVariant: Color = MaterialTheme.colorScheme.onBackground, + onSurface: Color = MaterialTheme.colorScheme.onBackground, + outline: Color = Color( + ColorUtils.blendARGB( + onPrimary.toArgb(), + primary.toArgb(), + OUTLINE_COLOR_BLEND_RATIO, + ) + ), + error: Color = MaterialTheme.colorScheme.error, + surfaceHighest: Color = MaterialTheme.colorScheme.surfaceContainerHighest, + surfaceHigh: Color = MaterialTheme.colorScheme.surfaceContainerHigh, + surfaceMiddle: Color = MaterialTheme.colorScheme.surfaceContainer, + success: Color = Color(0xFF479B3F), + ): ConsentiumColors = ConsentiumColors( + primary = primary, + onPrimary = onPrimary, + secondary = secondary, + onSecondary = onSecondary, + tertiary = tertiary, + onSurfaceVariant = onSurfaceVariant, + onSurface = onSurface, + outline = outline, + error = error, + surfaceHighest = surfaceHighest, + surfaceHigh = surfaceHigh, + surfaceMiddle = surfaceMiddle, + success = success, + ) + + @Composable + fun typography( + h1: TextStyle = LocalTextStyle.current.merge(Typography.h1), + h2: TextStyle = LocalTextStyle.current.merge(Typography.h2), + b1: TextStyle = LocalTextStyle.current.merge(Typography.b1), + p1: TextStyle = LocalTextStyle.current.merge(Typography.p1), + b2: TextStyle = LocalTextStyle.current.merge(Typography.b2), + p2: TextStyle = LocalTextStyle.current.merge(Typography.p2), + b3: TextStyle = LocalTextStyle.current.merge(Typography.b3), + p3: TextStyle = LocalTextStyle.current.merge(Typography.p3), + button: TextStyle = LocalTextStyle.current.merge(Typography.button), + sm2: TextStyle = LocalTextStyle.current.merge(Typography.sm2), + ): ConsentiumTypography = ConsentiumTypography( + h1 = h1, + h2 = h2, + b1 = b1, + p1 = p1, + b2 = b2, + p2 = p2, + b3 = b3, + p3 = p3, + button = button, + sm2 = sm2, + ) +} + +data class ConsentiumColors internal constructor( + val primary: Color, + val onPrimary: Color, + val secondary: Color, + val onSecondary: Color, + val tertiary: Color, + val onSurfaceVariant: Color, + val onSurface: Color, + val outline: Color, + val error: Color, + val surfaceHighest: Color, + val surfaceHigh: Color, + val surfaceMiddle: Color, + val success: Color, +) + +data class ConsentiumTypography internal constructor( + val h1: TextStyle, + val h2: TextStyle, + val b1: TextStyle, + val p1: TextStyle, + val b2: TextStyle, + val p2: TextStyle, + val b3: TextStyle, + val p3: TextStyle, + val button: TextStyle, + val sm2: TextStyle, +) diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/style/ConsentiumUITheme.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/style/ConsentiumUITheme.kt new file mode 100644 index 0000000..2ad1a54 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/components/style/ConsentiumUITheme.kt @@ -0,0 +1,52 @@ +package fr.openium.consentium_ui.ui.components.style + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle + +internal val LocalColors = staticCompositionLocalOf { + ConsentiumColors( + primary = Color.Unspecified, + onPrimary = Color.Unspecified, + secondary = Color.Unspecified, + onSecondary = Color.Unspecified, + tertiary = Color.Unspecified, + onSurfaceVariant = Color.Unspecified, + onSurface = Color.Unspecified, + outline = Color.Unspecified, + error = Color.Unspecified, + surfaceHighest = Color.Unspecified, + surfaceHigh = Color.Unspecified, + surfaceMiddle = Color.Unspecified, + success = Color.Unspecified, + ) +} + +internal val LocalTypography = staticCompositionLocalOf { + ConsentiumTypography( + h1 = TextStyle.Default, + h2 = TextStyle.Default, + b1 = TextStyle.Default, + p1 = TextStyle.Default, + b2 = TextStyle.Default, + p2 = TextStyle.Default, + b3 = TextStyle.Default, + p3 = TextStyle.Default, + button = TextStyle.Default, + sm2 = TextStyle.Default, + ) +} + +internal object ConsentiumUITheme { + val colors: ConsentiumColors + @Composable + @ReadOnlyComposable + get() = LocalColors.current + + val typography: ConsentiumTypography + @Composable + @ReadOnlyComposable + get() = LocalTypography.current +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/ConsentiumPageUI.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/ConsentiumPageUI.kt new file mode 100644 index 0000000..2771aaa --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/ConsentiumPageUI.kt @@ -0,0 +1,6 @@ +package fr.openium.consentium_ui.ui.model + +enum class ConsentiumPageUI { + GENERAL_CONSENT, + DETAILS_CONSENT +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/DetailConsentUI.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/DetailConsentUI.kt new file mode 100644 index 0000000..f2e4cf1 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/DetailConsentUI.kt @@ -0,0 +1,6 @@ +package fr.openium.consentium_ui.ui.model + +internal data class DetailConsentUI( + val conservationDurationText: String, + val purposes: List, +) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/GeneralConsentUI.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/GeneralConsentUI.kt new file mode 100644 index 0000000..118b6cc --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/GeneralConsentUI.kt @@ -0,0 +1,8 @@ +package fr.openium.consentium_ui.ui.model + +internal data class GeneralConsentUI( + val applicationName: String, + val iconUrl: String, + val mainConsentText: String, + val consentPageUrl: String, +) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/LoadingElement.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/LoadingElement.kt new file mode 100644 index 0000000..1c4a2bd --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/LoadingElement.kt @@ -0,0 +1,7 @@ +package fr.openium.consentium_ui.ui.model + +internal enum class LoadingElement { + BUTTON, + LINK, + NONE, +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/PurposeStatusUI.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/PurposeStatusUI.kt new file mode 100644 index 0000000..c0bf849 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/PurposeStatusUI.kt @@ -0,0 +1,7 @@ +package fr.openium.consentium_ui.ui.model + +internal enum class PurposeStatusUI { + ACCEPTED, + REJECTED, + NOT_DEFINED, +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/PurposeUI.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/PurposeUI.kt new file mode 100644 index 0000000..adbe3ac --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/PurposeUI.kt @@ -0,0 +1,10 @@ +package fr.openium.consentium_ui.ui.model + +internal data class PurposeUI( + val id: String, + val isRequired: Boolean, + var isAccepted: PurposeStatusUI, + val title: String, + val description: String, + val vendors: List, +) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/VendorUI.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/VendorUI.kt new file mode 100644 index 0000000..85c7d8f --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/model/VendorUI.kt @@ -0,0 +1,6 @@ +package fr.openium.consentium_ui.ui.model + +internal data class VendorUI( + val id: String, + var isAccepted: Boolean, +) \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/state/ConsentiumUIState.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/state/ConsentiumUIState.kt new file mode 100644 index 0000000..a4c68b2 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/state/ConsentiumUIState.kt @@ -0,0 +1,17 @@ +package fr.openium.consentium_ui.ui.state + +import fr.openium.consentium_ui.ui.model.DetailConsentUI +import fr.openium.consentium_ui.ui.model.GeneralConsentUI + +internal sealed interface ConsentiumUIState { + + data object Loading : ConsentiumUIState + + data class Loaded( + val generalConsentUI: GeneralConsentUI, + val detailConsentUI: DetailConsentUI, + ) : ConsentiumUIState + + data class Error(val message: String) : ConsentiumUIState + +} \ No newline at end of file diff --git a/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/utils/HtmlToAnnotatedString.kt b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/utils/HtmlToAnnotatedString.kt new file mode 100644 index 0000000..98d02c3 --- /dev/null +++ b/consentium-ui/src/main/java/fr/openium/consentium_ui/ui/utils/HtmlToAnnotatedString.kt @@ -0,0 +1,54 @@ +package fr.openium.consentium_ui.ui.utils + +import android.text.ParcelableSpan +import android.text.Spanned +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.core.text.HtmlCompat + +fun htmlToAnnotatedString(html: String): AnnotatedString { + val spanned: Spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) + return buildAnnotatedString { + val text = spanned.toString() + var start = 0 + spanned.getSpans(0, spanned.length, Any::class.java).map { it as ParcelableSpan }.forEach { span -> + val startSpan = spanned.getSpanStart(span) + val endSpan = spanned.getSpanEnd(span) + + // Ajoutez le texte précédent sans style + if (start < startSpan) { + append(text.substring(start, startSpan)) + } + + // Ajoutez le texte stylé + withStyle(style = span.toSpanStyle()) { + append(text.substring(startSpan, endSpan)) + } + start = endSpan + } + + // Ajoutez le texte restant sans style + if (start < text.length) { + append(text.substring(start, text.length).trimEnd()) + } + } +} + +fun ParcelableSpan.toSpanStyle(): SpanStyle { + return when (this) { + is android.text.style.StyleSpan -> { + when (style) { + android.graphics.Typeface.BOLD -> SpanStyle(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) + android.graphics.Typeface.ITALIC -> SpanStyle(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) + else -> SpanStyle() + } + } + + is android.text.style.UnderlineSpan -> SpanStyle(textDecoration = TextDecoration.Underline) + is android.text.style.StrikethroughSpan -> SpanStyle(textDecoration = TextDecoration.LineThrough) + else -> SpanStyle() + } +} \ No newline at end of file diff --git a/consentium-ui/src/main/res/values/strings.xml b/consentium-ui/src/main/res/values/strings.xml new file mode 100644 index 0000000..b495dd0 --- /dev/null +++ b/consentium-ui/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Politique de cookies + Accepter et fermer + Continuer sans accepter + Paramétrer mes choix + Enregistrer et fermer + Requis + Une erreur est survenue l\'or de la ssauvegarde de vos consentements. + \ 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 index 9d85af3..05fadb7 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/Consentium.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/Consentium.kt @@ -3,10 +3,6 @@ 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.domain.di.RepositoryEntryPoint @@ -23,8 +19,8 @@ class Consentium( 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 _saveConsentState = MutableStateFlow(SetConsentiumState.Idle) + val saveConsentState by lazy { _saveConsentState.asStateFlow() } private val consentiumRepository: ConsentiumRepository by lazy { val appContext = context.applicationContext @@ -32,34 +28,6 @@ class Consentium( 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 { @@ -75,24 +43,27 @@ class Consentium( } } } - } catch (e: Exception) { _fetchConsentState.value = FetchConsentiumState.Error } } - suspend fun setConsents( + suspend fun saveConsents( consent: List, ) { - _setConsentState.value = SetConsentiumState.Loading + _saveConsentState.emit(SetConsentiumState.Loading) try { when (consentiumRepository.setConsents(applicationId, consent)) { - ConsentiumRepositorySetResponse.Error -> _setConsentState.value = SetConsentiumState.Error - ConsentiumRepositorySetResponse.SetConsentsSuccess -> _setConsentState.value = SetConsentiumState.Success - } + ConsentiumRepositorySetResponse.Error -> { + _saveConsentState.emit(SetConsentiumState.Error) + } + ConsentiumRepositorySetResponse.SetConsentsSuccess -> { + _saveConsentState.emit(SetConsentiumState.Success) + } + } } catch (e: Exception) { - _setConsentState.value = SetConsentiumState.Error + _saveConsentState.emit(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 index f60776b..12e9937 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeAdapter.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/PurposeAdapter.kt @@ -1,11 +1,10 @@ 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), + identifier = identifier, isRequired = isRequired, isAccepted = isAccepted.toPurposeStatus(), vendors = vendors?.map { it.toVendor() } ?: emptyList(), 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 index b9c3bba..f86ad1d 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorAdapter.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/adapter/VendorAdapter.kt @@ -1,11 +1,10 @@ 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), + identifier = identifier, isAccepted = isAccepted, isRequired = isRequired, ) \ 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 index 6d9caab..185444f 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/model/Purpose.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/model/Purpose.kt @@ -1,7 +1,7 @@ package fr.openium.consentium.api.model data class Purpose( - val identifier: PurposeIdentifier, + val identifier: String, val isRequired: Boolean, val isAccepted: PurposeStatus, val vendors: List, 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 index bbd4c4a..f926ad7 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/model/PurposeChoice.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/model/PurposeChoice.kt @@ -1,7 +1,7 @@ package fr.openium.consentium.api.model data class PurposeChoice( - val purposeIdentifier: PurposeIdentifier, + val purposeIdentifier: String, 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 deleted file mode 100644 index 3e3ce8f..0000000 --- a/consentium/src/main/java/fr/openium/consentium/api/model/PurposeIdentifier.kt +++ /dev/null @@ -1,24 +0,0 @@ -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/Vendor.kt b/consentium/src/main/java/fr/openium/consentium/api/model/Vendor.kt index 3499ae3..64043e6 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/model/Vendor.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/model/Vendor.kt @@ -1,7 +1,7 @@ package fr.openium.consentium.api.model data class Vendor( - val identifier: VendorIdentifier, + val identifier: String, 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 index 98681ab..985485e 100644 --- a/consentium/src/main/java/fr/openium/consentium/api/model/VendorChoice.kt +++ b/consentium/src/main/java/fr/openium/consentium/api/model/VendorChoice.kt @@ -1,6 +1,6 @@ package fr.openium.consentium.api.model data class VendorChoice( - val vendorIdentifier: VendorIdentifier, + val vendorIdentifier: String, 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 deleted file mode 100644 index 65b1b70..0000000 --- a/consentium/src/main/java/fr/openium/consentium/api/model/VendorIdentifier.kt +++ /dev/null @@ -1,27 +0,0 @@ -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/data/di/NetworkModule.kt b/consentium/src/main/java/fr/openium/consentium/data/di/NetworkModule.kt index 0d84244..3017abb 100644 --- a/consentium/src/main/java/fr/openium/consentium/data/di/NetworkModule.kt +++ b/consentium/src/main/java/fr/openium/consentium/data/di/NetworkModule.kt @@ -8,7 +8,7 @@ import dagger.Reusable import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import fr.openium.consentium.BuildConfig -import fr.openium.consentium.api.mock.ConsentiumMockApi +import fr.openium.consentium.data.remote.mock.ConsentiumMockApi import fr.openium.consentium.data.remote.ConsentiumApi import kotlinx.serialization.json.Json import okhttp3.Cache diff --git a/consentium/src/main/java/fr/openium/consentium/data/remote/mock/ConsentiumMock.kt b/consentium/src/main/java/fr/openium/consentium/data/remote/mock/ConsentiumMock.kt index b935c61..cbb46ed 100644 --- a/consentium/src/main/java/fr/openium/consentium/data/remote/mock/ConsentiumMock.kt +++ b/consentium/src/main/java/fr/openium/consentium/data/remote/mock/ConsentiumMock.kt @@ -1,4 +1,4 @@ -package fr.openium.consentium.api.mock +package fr.openium.consentium.data.remote.mock import fr.openium.consentium.data.remote.ConsentiumApi import fr.openium.consentium.data.remote.model.GetConsent @@ -36,16 +36,16 @@ internal object ConsentiumMockApi : ConsentiumApi { vendors = null ), ), - isValid = true + isValid = false, ) override suspend fun getConsents(applicationId: String, installationId: String): Response { - delay(500) + delay(2000) return Response.success(consents) } override suspend fun setConsents(applicationId: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response { - delay(500) + delay(2000) return Response.success(Unit) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97650ee..87fb9f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,8 +65,18 @@ firebaseCrashlyticsKtx = "19.2.0" # Clarity clarityVersion = "1.3.2" +# Coil +coil = "3.0.4" + + # GA4 ga4 = "22.1.2" +runtimeAndroid = "1.7.6" +foundationAndroid = "1.7.6" +foundationLayoutAndroid = "1.7.5" +uiAndroid = "1.7.5" +uiToolingPreviewAndroid = "1.7.6" +material3Android = "1.3.1" [libraries] @@ -94,9 +104,11 @@ compose-ui = { module = "androidx.compose.ui:ui" } compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } # Material material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" } # Kotlin serizalization kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jsonSerialization" } @@ -132,6 +144,15 @@ clarity = { group = "com.microsoft.clarity", name = "clarity", version.ref = "cl # GA4 (Firebase Analytics) ga4 = { module = "com.google.firebase:firebase-analytics", version.ref = "ga4" } +androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } +androidx-foundation-android = { group = "androidx.compose.foundation", name = "foundation-android", version.ref = "foundationAndroid" } +androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundationLayoutAndroid" } +androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "uiAndroid" } +androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" } + +# Coil +coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }