21 Commits

Author SHA1 Message Date
7a4692f062 fix(CON--) : Fix slack message ko
All checks were successful
gitea-openium/consentium.droid/pipeline/head This commit looks good
2025-02-20 15:33:55 +01:00
b03c37352a Incrementation automatique du version code
All checks were successful
gitea-openium/consentium.droid/pipeline/head This commit looks good
2025-02-20 15:26:29 +01:00
0f4a3254e4 fix(CON--) : Fix app deployment wip
All checks were successful
gitea-openium/consentium.droid/pipeline/head This commit looks good
2025-02-20 15:18:09 +01:00
3ffd190406 feat(CON-285) : Respecter les critères d'accessibilité sur les boutons des écrans utilisateurs. 2025-02-20 12:00:36 +01:00
76d021d35d feat(CON-287) : Mettre en place le dark theme. 2025-02-20 10:59:15 +01:00
ed2d0ddb43 fix(CON-286) : Fix les transitions sur la navigation 2025-02-20 10:38:28 +01:00
4276adaf22 chore(-) : Update proguard for release 2025-02-17 17:20:27 +01:00
ad08a4325f fix(CON-271) : Fix rich text issue 2025-02-17 11:19:48 +01:00
c94f8a4f05 feat(CON-264) : Migrer les traductions vers l'anglais 2025-02-17 10:24:40 +01:00
9f9a66451f feat(CON-267) : Aligner les flux de post des consentements 2025-02-11 17:21:24 +01:00
af91841857 feat(CON-265) : Aligner les flux de l'api d'UI avec les nouveaux dev back 2025-02-11 17:05:29 +01:00
c57c9b7d05 feat(CON-223) : Ajouter l'auth
feat(CON-262) : Aligner les flux avec les nouveaux dev back
2025-02-11 15:00:04 +01:00
5303e594da Merge remote-tracking branch 'origin/develop' into develop 2025-02-03 08:57:23 +01:00
18f9fff4a1 chore(CON-238) : Mettre à jour le toml 2025-02-03 08:57:15 +01:00
cc578e3117 chore(DEVTOOLS-138): update jenkins pipeline to android-v3.0 (#14)
Reviewed-on: #14
2025-01-15 09:17:13 +01:00
75d179e046 chore(DEVTOOLS-139): update jenkins pipeline to android-v2.0 (#13)
Reviewed-on: #13
2025-01-14 17:07:14 +01:00
63cfc39d1e chore(#CON-243): fix Jenkinsfile (#12)
Reviewed-on: #12
Reviewed-by: Louis Legrand <l.legrand@openium.fr>
2025-01-14 11:02:34 +01:00
bf5f7b47b7 Merge pull request 'feat(CON-238) : Add webview for cookies policy' (#11) from feat-238 into develop
Reviewed-on: #11
Reviewed-by: Simon Tabaka <s.tabaka@openium.fr>
2025-01-07 16:19:01 +01:00
338d9f624a feat(CON-238) : Add webview for cookies policy 2025-01-07 15:30:45 +01:00
1f40d2fc98 Merge pull request 'feat(CON-171) : Summary consent screen' (#10) from feat(CON-171) into develop
Reviewed-on: #10
Reviewed-by: Arthur Valin <a.valin@openium.fr>
2025-01-07 14:52:40 +01:00
86a7020c2b feat(CON-171) : Summary consent screen
feat(CON-172) : Detail consent screen
feat(-) : Send consent to BO to save them
2025-01-07 14:47:59 +01:00
77 changed files with 805 additions and 420 deletions

15
Jenkinsfile vendored
View File

@ -1,3 +1,16 @@
library "openiumpipeline"
openiumDroidJob()
openiumDroidJob modules: [
"consentium": [
unitTestTasks: ["testDevDebugUnitTest"],
],
"consentium-ui": [
unitTestTasks: ["testDevDebugUnitTest"],
],
"app": [
unitTestTasks: ["testDevDebugUnitTest"],
testTasks: ["pixel5DevDebugAndroidTest"],
publishApkVariants : ["devDebug", "devRelease", "demoRelease", "prodRelease"],
],
],
publishChannel: '#int-consentium'

View File

@ -1,3 +1,4 @@
import com.android.build.api.dsl.ManagedVirtualDevice
import org.gradle.language.nativeplatform.internal.BuildType
import java.io.FileInputStream
import java.util.Properties
@ -9,7 +10,9 @@ plugins {
alias(libs.plugins.hilt)
alias(libs.plugins.serialization)
alias(libs.plugins.kotlin.compose)
id("fr.openium.publish")
}
apply(from = "publish.build.gradle")
// Keystore
val keystorePropertiesFile = rootProject.file("keys/keystore.properties")
@ -24,8 +27,6 @@ android {
applicationId = "fr.openium.consentium"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -48,6 +49,7 @@ android {
buildTypes {
debug {
isMinifyEnabled = false
isShrinkResources = false
versionNameSuffix = "-debug"
applicationIdSuffix = ".debug"
@ -72,6 +74,10 @@ android {
dimension = "version"
}
create("dev") {
dimension = "version"
}
create("demo") {
dimension = "version"
}
@ -87,6 +93,19 @@ android {
buildFeatures {
compose = true
}
testOptions {
animationsDisabled = true
managedDevices {
devices {
create<ManagedVirtualDevice>("pixel5") {
device = "Pixel 5"
apiLevel = 34
systemImageSource = "google"
}
}
}
}
}
dependencies {
@ -129,5 +148,4 @@ dependencies {
// Kotlin serialization
implementation(libs.kotlin.serialization)
}

6
app/publish.build.gradle Normal file
View File

@ -0,0 +1,6 @@
android {
defaultConfig {
versionName publish.versionName
versionCode publish.versionCode
}
}

View File

@ -17,8 +17,6 @@ import org.junit.Assert.*
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.openium.consentium", appContext.packageName)
assertTrue(true)
}
}

View File

@ -1,56 +1,82 @@
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
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.res.stringResource
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.R
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
private val SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION = slideIn(
animationSpec = tween(NAV_ANIMATION_TIME),
initialOffset = { fullSize -> IntOffset(x = fullSize.height, y = 0) }
)
private val SLIDE_IN_FROM_LEFT_ENTER_TRANSITION = slideIn(
animationSpec = tween(NAV_ANIMATION_TIME),
initialOffset = { fullSize -> IntOffset(x = -fullSize.height, y = 0) }
)
private val SLIDE_OUT_TO_LEFT_EXIT_TRANSITION = slideOut(
animationSpec = tween(NAV_ANIMATION_TIME),
targetOffset = { fullSize -> IntOffset(x = -fullSize.height, y = 0) }
)
private val SLIDE_OUT_TO_RIGHT_EXIT_TRANSITION = slideOut(
animationSpec = tween(NAV_ANIMATION_TIME),
targetOffset = { fullSize -> IntOffset(x = fullSize.height, y = 0) }
)
private infix fun String?.ifIn(destinations: List<Destination>): Boolean {
return this in destinations.map { it::class.qualifiedName }
}
private infix fun Boolean.thenTransitionWith(transition: EnterTransition): EnterTransition? {
return if (this) transition else null
}
private infix fun EnterTransition?.elseTransitionWith(transition: EnterTransition): EnterTransition {
return this ?: transition
}
private infix fun Boolean.thenTransitionWith(transition: ExitTransition): ExitTransition? {
return if (this) transition else null
}
private infix fun ExitTransition?.elseTransitionWith(transition: ExitTransition): ExitTransition {
return this ?: transition
}
@Composable
fun DemoNavGraph(navHostController: NavHostController) {
NavHost(
navController = navHostController,
startDestination = Destination.Splash,
enterTransition = {
slideIn(
animationSpec = tween(NAV_ANIMATION_TIME),
initialOffset = { fullSize -> IntOffset(x = fullSize.height, y = 0) }
)
},
exitTransition = {
slideOut(
animationSpec = tween(NAV_ANIMATION_TIME),
targetOffset = { fullSize -> IntOffset(x = -fullSize.height, y = 0) }
)
},
) {
composable<Destination.Splash> {
composable<Destination.Splash>(
enterTransition = {
SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION
},
exitTransition = {
SLIDE_OUT_TO_LEFT_EXIT_TRANSITION
}
) {
SplashScreen(
navigateToMain = {
navHostController.navigate(Destination.Main) {
@ -65,7 +91,18 @@ fun DemoNavGraph(navHostController: NavHostController) {
)
}
composable<Destination.Main> {
composable<Destination.Main>(
enterTransition = {
initialState.destination.route ifIn listOf(
Destination.Splash
) thenTransitionWith SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION elseTransitionWith SLIDE_IN_FROM_LEFT_ENTER_TRANSITION
},
exitTransition = {
targetState.destination.route ifIn listOf(
Destination.Splash
) thenTransitionWith SLIDE_OUT_TO_RIGHT_EXIT_TRANSITION elseTransitionWith SLIDE_OUT_TO_LEFT_EXIT_TRANSITION
}
) {
MainScreen(
onGoToConsentMaster = {
navHostController.navigate(Destination.Consent(ConsentiumPageUI.GENERAL_CONSENT))
@ -76,9 +113,25 @@ fun DemoNavGraph(navHostController: NavHostController) {
)
}
composable<Destination.Consent> { backStackEntry ->
composable<Destination.Consent>(
enterTransition = {
initialState.destination.route ifIn listOf(
Destination.Splash,
Destination.Main,
) thenTransitionWith SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION elseTransitionWith SLIDE_IN_FROM_LEFT_ENTER_TRANSITION
},
exitTransition = {
targetState.destination.route ifIn listOf(
Destination.Splash,
Destination.Main,
) thenTransitionWith SLIDE_OUT_TO_RIGHT_EXIT_TRANSITION elseTransitionWith SLIDE_OUT_TO_LEFT_EXIT_TRANSITION
}
) { backStackEntry ->
val consent = backStackEntry.toRoute<Destination.Consent>()
val appId = stringResource(R.string.app_id)
val apiKey = stringResource(R.string.api_key)
val context = LocalContext.current
ConsentiumComponent(
defaultLandingPage = consent.landingPage,
@ -91,21 +144,8 @@ fun DemoNavGraph(navHostController: NavHostController) {
},
consentium = Consentium(
context = context,
"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,
apiKey = apiKey,
appId = appId,
)
)
}

View File

@ -18,7 +18,6 @@ fun MainScreen(
onGoToConsentDetail: () -> Unit,
modifier: Modifier = Modifier,
) {
// View
Column(
modifier = modifier.fillMaxSize(),

View File

@ -14,8 +14,11 @@ 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.res.stringResource
import androidx.compose.ui.unit.dp
import fr.openium.consentium.R
import fr.openium.consentium.api.Consentium
import fr.openium.consentium.api.checkPurposeState
import fr.openium.consentium.api.state.FetchConsentiumState
@Composable
@ -25,14 +28,18 @@ fun SplashScreen(
) {
// Property
val context = LocalContext.current
val consentium = remember { Consentium(context = context, applicationId = "DemoApplicationId") }
val apiKey = stringResource(R.string.api_key)
val appId = stringResource(R.string.app_id)
val consentium = remember { Consentium(context = context, apiKey = apiKey, appId = appId) }
// Effect
LaunchedEffect(Unit) {
consentium.fetchConsentState.collect { consentState ->
when (consentState) {
FetchConsentiumState.Idle,
FetchConsentiumState.Loading -> {}
FetchConsentiumState.Loading,
-> {
}
FetchConsentiumState.Error -> {
// Handle error
@ -45,6 +52,22 @@ fun SplashScreen(
is FetchConsentiumState.Valid -> {
// The tracking services should be initialized here
val consents = consentState.purposes
consents.checkPurposeState("analytics") { consentState ->
// Initialize analytics
}
consents.checkPurposeState("ads") { consentState ->
// Initialize ads
}
consents.checkPurposeState("matomo") { consentState ->
// Initialize push
}
// Navigate on completion
navigateToMain()
}
}

View File

@ -8,14 +8,38 @@ import androidx.compose.runtime.Composable
private val DarkColorScheme = darkColorScheme(
primary = Primary,
onPrimary = OnPrimary,
secondary = Secondary,
tertiary = Tertiary
onSecondary = OnSecondary,
tertiary = Tertiary,
onSurfaceVariant = OnSurfaceVariant,
onSurface = OnSurface,
error = Error,
surfaceContainerHighest = SurfaceHighest,
surfaceContainerHigh = SurfaceHigh,
surfaceContainer = SurfaceMiddle,
)
private val LightColorScheme = lightColorScheme(
primary = Primary,
onPrimary = OnPrimary,
secondary = Secondary,
tertiary = Tertiary
onSecondary = OnSecondary,
tertiary = Tertiary,
onSurfaceVariant = OnSurfaceVariant,
onSurface = OnSurface,
error = Error,
surfaceContainerHighest = SurfaceHighest,
surfaceContainerHigh = SurfaceHigh,
surfaceContainer = SurfaceMiddle,
)
@Composable

View File

@ -1,3 +1,5 @@
<resources>
<string name="app_name">Consentium</string>
<string name="app_id" translatable="false">01938ce4-331a-7592-9e90-f09201ff4f36</string>
<string name="api_key" translatable="false">c452a27f-2e90-427d-be82-2f631c31dd09</string>
</resources>

3
app/version.properties Normal file
View File

@ -0,0 +1,3 @@
#Thu Feb 20 15:26:27 CET 2025
VERSION_CODE=2
VERSION_NAME=1.0.0

View File

@ -20,11 +20,13 @@ plugins {
// Kotlin serialization
alias(libs.plugins.serialization) apply false
}
// Firebase crashlytics
alias(libs.plugins.firebaseCrashlytics) apply false
// Google services
alias(libs.plugins.googleServices) apply false
buildscript {
repositories {
maven { url = uri("https://maven.openium.fr/") }
}
dependencies {
classpath(libs.openium.publish)
}
}

View File

@ -19,12 +19,26 @@ android {
}
buildTypes {
release {
debug {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
consumerProguardFiles("proguard-rules.pro")
}
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
consumerProguardFiles("proguard-rules.pro")
}
}
@ -34,6 +48,10 @@ android {
dimension = "version"
}
create("dev") {
dimension = "version"
}
create("demo") {
dimension = "version"
}
@ -67,8 +85,6 @@ dependencies {
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)
@ -100,10 +116,11 @@ dependencies {
implementation(libs.coil)
implementation(libs.coil.network)
// Rich text formatting
implementation(libs.rich.text)
// Tests
testImplementation(libs.test.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.test.espresso)
}

View File

@ -18,4 +18,56 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-dontwarn fr.openium.consentium.data.di.ConsentiumUrl
-dontwarn fr.openium.consentium.data.di.OkHttpClientDefault
-dontwarn java.lang.invoke.StringConcatFactory
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exception
-keep class com.google.firebase.crashlytics.** { *; }
-dontwarn com.google.firebase.crashlytics.**
# Keep enums
-keep public enum fr.openium.consentium.**{
*;
}
# Garder les annotations de Kotlin
-keepattributes *Annotation*
# Garder les classes et les membres annotés avec @Keep
-keep @androidx.annotation.Keep class * { *; }
-keepclassmembers class ** {
@androidx.annotation.Keep *;
}
# Garder les classes et les membres annotés avec @Serializable
-keep @kotlinx.serialization.Serializable class * { *; }
-keepclassmembers class ** {
@kotlinx.serialization.Serializable *;
}
# Garder les classes annotées avec @HiltAndroidApp
-keep @dagger.hilt.android.HiltAndroidApp class * { *; }
# Garder les classes et les membres pour Timber
-keep class timber.log.Timber { *; }
-keep interface timber.log.Timber$Tree { *; }
-keep public enum fr.openium.consentium_ui.ui.model.ConsentiumPageUI
-keepclassmembers enum fr.openium.consentium_ui.ui.model.ConsentiumPageUI {
<fields>;
}
-keep class fr.openium.consentium_ui.data.** { *; }
-keep class fr.openium.consentium_ui.domain.repository.** { *; }
-keep class fr.openium.consentium_ui.domain.usecase.** { *; }
-keep class fr.openium.consentium_ui.ui.** { *; }
-keepclassmembers class fr.openium.consentium_ui.data.** { *; }
-keepclassmembers class fr.openium.consentium_ui.domain.repository.** { *; }
-keepclassmembers class fr.openium.consentium_ui.domain.usecase.** { *; }
-keepclassmembers class fr.openium.consentium_ui.ui.** { *; }

View File

@ -1,13 +1,10 @@
package fr.openium.consentium_ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -17,8 +14,6 @@ import org.junit.Assert.*
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.openium.consentium_ui.test", appContext.packageName)
assertTrue(true)
}
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -3,13 +3,15 @@ package fr.openium.consentium_ui.data.remote
import fr.openium.consentium_ui.data.remote.model.GetConsentConfigDTO
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
internal interface ConsentiumUIApi {
@GET("consent-config")
@GET("applications/{application}")
suspend fun getConsentConfig(
applicationID: String,
installationID: String,
@Header("Authorization") token: String,
@Path("application") applicationId: String,
): Response<GetConsentConfigDTO>
}

View File

@ -6,8 +6,6 @@ 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
@ -17,24 +15,22 @@ internal object ConsentiumUIMockApi : ConsentiumUIApi {
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",
consentPageUrl = "https://www.openium.fr",
mainConsentText = "<p>[Nom de lapplication] utilise des cookies pour différents objectifs : faire fonctionner lapplication, améliorer nos services en mesurant lefficacité de nos contenus et afficher des publicités susceptibles de vous intéresser.<br></p>\n<p>En cliquant sur “Accepter et fermer”, vous acceptez cette utilisation sur lapplication 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 davis à tout moment depuis les paramètres de votre compte via longlet “Notifications et cookies”</p>\n",
durationText = "<p>Nous conservons votre choix pendant 12 mois. Vous pouvez changer davis à tout moment depuis les paramètres de votre compte via longlet “Notification et cookies” tetetettetettetetstts</p>\n"
)
),
consentPageUrl = "https://www.openium.fr",
purposes = listOf(
PurposeDTO(
id = "purpose-required",
order = 0,
isRequired = true,
isAccepted = PurposeStatusDTO.ACCEPTED,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
id = "UUID",
@ -43,40 +39,12 @@ internal object ConsentiumUIMockApi : ConsentiumUIApi {
name = "Nécessaire"
)
),
vendors = listOf(
VendorDTO(
id = "vendors-crashlytics",
order = 0,
isAccepted = true,
isRequired = true,
translations = listOf(
VendorTranslationDTO(
id = "UUID",
language = "fr",
text = "<p>Ces traceurs sont nécessaire au fonctionnement de lapplication. Ils permettent de vérifier la stabilité technique de lapplication 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).</p>"
)
)
),
VendorDTO(
id = "vendors-matomo",
order = 1,
isAccepted = true,
isRequired = false,
translations = listOf(
VendorTranslationDTO(
id = "UUID",
language = "fr",
text = "<p>Ces traceurs sont nécessaire au fonctionnement de lapplication. Ils permettent de vérifier la stabilité technique de lapplication 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).</p>"
)
)
)
)
),
PurposeDTO(
id = "purpose-advertising",
order = 1,
isRequired = false,
isAccepted = PurposeStatusDTO.REJECTED,
choice = PurposeStatusDTO.REFUSED,
translations = listOf(
PurposeTranslationDTO(
id = "UUID",
@ -85,27 +53,12 @@ internal object ConsentiumUIMockApi : ConsentiumUIApi {
name = "Publicité"
)
),
vendors = listOf(
VendorDTO(
id = "vendors-admob",
order = 0,
isAccepted = true,
isRequired = false,
translations = listOf(
VendorTranslationDTO(
id = "UUID",
language = "fr",
text = "<p>Ces traceurs sont nécessaires pour afficher des publicités susceptibles de vous intéresser. Ils permettent de mesurer lefficacité de nos campagnes publicitaires et de personnaliser les publicités affichées.</p>"
)
)
)
)
),
PurposeDTO(
id = "purpose-analytics",
order = 2,
isRequired = false,
isAccepted = PurposeStatusDTO.ACCEPTED,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
id = "UUID",
@ -114,27 +67,12 @@ internal object ConsentiumUIMockApi : ConsentiumUIApi {
name = "Analyse"
)
),
vendors = listOf(
VendorDTO(
id = "vendors-firebase",
order = 0,
isAccepted = true,
isRequired = false,
translations = listOf(
VendorTranslationDTO(
id = "UUID",
language = "fr",
text = "<p>Ces traceurs sont nécessaires pour mesurer lefficacité de nos contenus. Ils permettent de mesurer laudience de lapplication et de comprendre comment les utilisateurs interagissent avec lapplication.</p>"
)
)
)
)
),
PurposeDTO(
id = "purpose-personalization",
order = 3,
isRequired = false,
isAccepted = PurposeStatusDTO.ACCEPTED,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
id = "UUID",
@ -143,27 +81,12 @@ internal object ConsentiumUIMockApi : ConsentiumUIApi {
name = "Personnalisation"
)
),
vendors = listOf(
VendorDTO(
id = "vendors-firebase",
order = 0,
isAccepted = true,
isRequired = false,
translations = listOf(
VendorTranslationDTO(
id = "UUID",
language = "fr",
text = "<p>Ces traceurs sont nécessaires pour mesurer lefficacité de nos contenus. Ils permettent de mesurer laudience de lapplication et de comprendre comment les utilisateurs interagissent avec lapplication.</p>"
)
)
)
)
),
PurposeDTO(
id = "purpose-social",
order = 4,
isRequired = false,
isAccepted = PurposeStatusDTO.ACCEPTED,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
id = "UUID",
@ -172,28 +95,13 @@ internal object ConsentiumUIMockApi : ConsentiumUIApi {
name = "Social"
)
),
vendors = listOf(
VendorDTO(
id = "vendors-firebase",
order = 0,
isAccepted = true,
isRequired = false,
translations = listOf(
VendorTranslationDTO(
id = "UUID",
language = "fr",
text = "<p>Ces traceurs sont nécessaires pour mesurer lefficacité de nos contenus. Ils permettent de mesurer laudience de lapplication et de comprendre comment les utilisateurs interagissent avec lapplication.</p>"
)
)
)
)
)
)
)
override suspend fun getConsentConfig(
applicationID: String,
installationID: String,
token: String,
applicationId: String,
): Response<GetConsentConfigDTO> {
return Response.success(consents)
}

View File

@ -7,10 +7,8 @@ import kotlinx.serialization.Serializable
internal data class GetConsentConfigDTO(
@SerialName("id") val installationId: String,
@SerialName("name") val appName: String,
@SerialName("icon") val icon: String,
@SerialName("primaryColor") val primaryColor: String,
@SerialName("secondaryColor") val secondaryColor: String,
@SerialName("textColor") val textColor: String,
@SerialName("translation") val consentMainTextTranslation: List<MainConsentTextTranslationDTO>,
@SerialName("icon") val icon: String? = null,
@SerialName("consentPageUrl") val consentPageUrl: String,
@SerialName("translations") val consentMainTextTranslation: List<MainConsentTextTranslationDTO>,
@SerialName("purposes") val purposes: List<PurposeDTO>,
)

View File

@ -7,7 +7,8 @@ import kotlinx.serialization.Serializable
internal data class MainConsentTextTranslationDTO(
@SerialName("id") val id: String,
@SerialName("lang") val language: String,
@SerialName("consentPageUrl") val consentPageUrl: String,
@SerialName("mainConsentText") val mainConsentText: String,
@SerialName("durationText") val durationText: String,
@SerialName("consentPageUrl") val consentPageUrl: String,
)

View File

@ -5,10 +5,10 @@ import kotlinx.serialization.Serializable
@Serializable
internal data class PurposeDTO(
@SerialName("identifier") val id: String,
@SerialName("order") val order: Int,
@SerialName("sortOrder") val order: Int,
@SerialName("isRequired") val isRequired: Boolean,
@SerialName("isAccepted") val isAccepted: PurposeStatusDTO,
@SerialName("choice") val choice: PurposeStatusDTO,
@SerialName("translations") val translations: List<PurposeTranslationDTO>,
@SerialName("vendors") val vendors: List<VendorDTO>,
@SerialName("identifier") val id: String,
)

View File

@ -5,12 +5,12 @@ import kotlinx.serialization.Serializable
@Serializable
internal enum class PurposeStatusDTO {
@SerialName("ACCEPTED")
@SerialName("accepted")
ACCEPTED,
@SerialName("REJECTED")
REJECTED,
@SerialName("refused")
REFUSED,
@SerialName("NOT_DEFINED")
NOT_DEFINED,
@SerialName("partial")
PARTIAL,
}

View File

@ -7,9 +7,9 @@ internal fun PurposeDTO.toPurposeData() =
PurposeData(
identifier = id,
isRequired = isRequired,
isAccepted = isAccepted.toPurposeStatusData(),
choice = choice.toPurposeStatusData(),
order = order,
vendors = vendors.toVendorDataList(),
vendors = emptyList(),
translations = translations.toPurposeTranslationDataList(),
)

View File

@ -6,7 +6,7 @@ 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
PurposeStatusDTO.REFUSED -> PurposeStatusData.REJECTED
PurposeStatusDTO.PARTIAL -> PurposeStatusData.PARTIAL
}
}

View File

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

View File

@ -4,7 +4,7 @@ internal data class PurposeData(
val identifier: String,
val order: Int,
val isRequired: Boolean,
val isAccepted: PurposeStatusData,
val choice: PurposeStatusData,
val translations: List<PurposeTranslationData>,
val vendors: List<VendorData>,
)

View File

@ -3,5 +3,5 @@ package fr.openium.consentium_ui.domain.model
internal enum class PurposeStatusData {
ACCEPTED,
REJECTED,
NOT_DEFINED,
PARTIAL,
}

View File

@ -1,22 +1,23 @@
package fr.openium.consentium_ui.domain.repository
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCase
import fr.openium.consentium.domain.useCase.GetAuthTokenUseCase
import fr.openium.consentium_ui.data.remote.ConsentiumUIApi
import fr.openium.consentium_ui.domain.adapter.toConsentConfigData
import fr.openium.consentium_ui.domain.model.ContentConfigData
import timber.log.Timber
import javax.inject.Inject
internal class ConsentiumRepository @Inject constructor(
private val getConsentiumUniqueInstallationIdUseCase: GetConsentiumUniqueInstallationIdUseCase,
private val consentiumUIApi: ConsentiumUIApi,
private val getAuthTokenUseCase: GetAuthTokenUseCase,
) {
suspend fun getConsentiumConfig(
applicationId: String,
apiKey: String,
): ConsentiumUIRepositoryResponse {
val installationId = getConsentiumUniqueInstallationIdUseCase.invoke()
val consentsResponse = consentiumUIApi.getConsentConfig(applicationId, installationId)
return try {
val authToken = getAuthTokenUseCase(apiKey)
val consentsResponse = consentiumUIApi.getConsentConfig(authToken, applicationId)
val consentsBody = if (consentsResponse.isSuccessful) {
consentsResponse.body() ?: throw Exception()
} else {
@ -25,10 +26,10 @@ internal class ConsentiumRepository @Inject constructor(
ConsentiumUIRepositoryResponse.Success(consentsBody.toConsentConfigData())
} catch (e: Exception) {
Timber.d("$e")
ConsentiumUIRepositoryResponse.Error
}
}
}
internal interface ConsentiumUIRepositoryResponse {

View File

@ -10,6 +10,7 @@ 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.components.ConsentiumUIWebview
import fr.openium.consentium_ui.ui.model.ConsentiumPageUI
import fr.openium.consentium_ui.ui.model.DetailConsentUI
import fr.openium.consentium_ui.ui.model.LoadingElement
@ -26,6 +27,7 @@ internal fun ConsentiumScreen(
onSaveAndCloseDetails: (consents: DetailConsentUI) -> Unit,
onNavigateToDetails: () -> Unit,
onClickCookiesPolicies: () -> Unit,
onClickRetry: () -> Unit,
) {
when (state) {
is ConsentiumUIState.Loading -> {
@ -35,6 +37,7 @@ internal fun ConsentiumScreen(
is ConsentiumUIState.Error -> {
ConsentiumUIErrorComponent(
errorMessage = state.message,
onRetry = onClickRetry
)
}
@ -53,7 +56,7 @@ internal fun ConsentiumScreen(
)
}
ConsentiumPageUI.DETAILS_CONSENT -> {
else -> {
slideIn(
initialOffset = { fullSize -> IntOffset(fullSize.width, 0) }
).togetherWith(
@ -92,6 +95,12 @@ internal fun ConsentiumScreen(
loadingElement = loadingElement
)
}
ConsentiumPageUI.COOKIES_POLICY -> {
ConsentiumUIWebview(
url = state.detailConsentUI.cookiePolicyUrl
)
}
}
}

View File

@ -5,6 +5,7 @@ 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.R
import fr.openium.consentium_ui.domain.repository.ConsentiumRepository
import fr.openium.consentium_ui.domain.repository.ConsentiumUIRepositoryResponse
import fr.openium.consentium_ui.domain.usecase.GetApplicationLanguageUseCase
@ -30,12 +31,12 @@ internal class ConsentiumUIViewModel @Inject constructor(
private val _state = MutableStateFlow<ConsentiumUIState>(ConsentiumUIState.Loading)
val state: StateFlow<ConsentiumUIState> by lazy { _state.asStateFlow() }
fun init(appId: String) {
fun init(appId: String, apiKey: String) {
viewModelScope.launch {
_state.value = ConsentiumUIState.Loading
when (val consentiumUIFetchResponse = consentiumUIRepository.getConsentiumConfig(appId)) {
when (val consentiumUIFetchResponse = consentiumUIRepository.getConsentiumConfig(appId, apiKey)) {
is ConsentiumUIRepositoryResponse.Success -> {
val consentUIResponse =
@ -44,28 +45,32 @@ internal class ConsentiumUIViewModel @Inject constructor(
when (val consentUI = consentUIResponse) {
is GetConfigTextForLanguageUseCaseResponse.Success -> {
_state.emit(ConsentiumUIState.Loaded(
generalConsentUI = consentUI.configData.toGeneralConsentUI(),
detailConsentUI = consentUI.configData.toDetailConsentUI(),
))
_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(),
))
_state.emit(
ConsentiumUIState.Loaded(
generalConsentUI = consentUI.configData.toGeneralConsentUI(),
detailConsentUI = consentUI.configData.toDetailConsentUI(),
)
)
}
is GetConfigTextForLanguageUseCaseResponse.Error -> {
_state.emit(ConsentiumUIState.Error("Failed to load data"))
_state.emit(ConsentiumUIState.Error(context.getString(R.string.consents_error_loading_consents)))
}
}
}
is ConsentiumUIRepositoryResponse.Error -> {
_state.value = ConsentiumUIState.Error("Failed to load data")
_state.value = ConsentiumUIState.Error(context.getString(R.string.consents_error_loading_consents))
}
}

View File

@ -1,6 +1,7 @@
package fr.openium.consentium_ui.ui.adapter
import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.api.model.PurposeStatus
import fr.openium.consentium_ui.domain.model.ContentConfigData
import fr.openium.consentium_ui.ui.model.DetailConsentUI
import fr.openium.consentium_ui.ui.model.PurposeStatusUI
@ -9,6 +10,7 @@ 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() },
cookiePolicyUrl = mainTextTranslation.first().consentPageUrl,
)
internal fun DetailConsentUI.toPurposeChoices(): List<PurposeChoice> = purposes.map {
@ -16,16 +18,23 @@ internal fun DetailConsentUI.toPurposeChoices(): List<PurposeChoice> = purposes.
}
internal fun DetailConsentUI.toDeniedPurposeChoices(): List<PurposeChoice> = purposes.map {
it.toPurposeChoice().copy(isAccepted = false)
it.toPurposeChoice().copy(choice = PurposeStatus.REJECTED)
}
internal fun DetailConsentUI.toAcceptedPurposeChoices(): List<PurposeChoice> = purposes.map {
it.toPurposeChoice().copy(isAccepted = true)
it.toPurposeChoice().copy(choice = PurposeStatus.ACCEPTED)
}
internal fun PurposeUI.toPurposeChoice(): PurposeChoice =
PurposeChoice(
purposeIdentifier = id,
isAccepted = isAccepted == PurposeStatusUI.ACCEPTED,
choice = choice.toPurposeStatus(),
vendors = emptyList(), // Not in v1
)
)
internal fun PurposeStatusUI.toPurposeStatus(): PurposeStatus =
when (this) {
PurposeStatusUI.ACCEPTED -> PurposeStatus.ACCEPTED
PurposeStatusUI.REJECTED -> PurposeStatus.REJECTED
PurposeStatusUI.PARTIAL -> PurposeStatus.PARTIAL
}

View File

@ -6,5 +6,5 @@ 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
PurposeStatusData.PARTIAL -> PurposeStatusUI.PARTIAL
}

View File

@ -6,7 +6,7 @@ import fr.openium.consentium_ui.ui.model.PurposeUI
internal fun PurposeData.toPurposeUI(): PurposeUI = PurposeUI(
id = identifier,
isRequired = isRequired,
isAccepted = isAccepted.toPurposeStatusUI(),
choice = choice.toPurposeStatusUI(),
title = translations.first().name,
description = translations.first().text,
vendors = vendors.toVendorUIList(),

View File

@ -1,6 +1,7 @@
package fr.openium.consentium_ui.ui.components
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@ -50,7 +51,7 @@ fun ConsentiumComponent(
// Effect
LaunchedEffect(Unit) {
viewModel.init(consentium.applicationId)
viewModel.init(apiKey = consentium.apiKey, appId = consentium.appId)
}
LaunchedEffect(consentiumState) {
@ -70,6 +71,22 @@ fun ConsentiumComponent(
}
}
BackHandler {
when {
defaultLandingPage == ConsentiumPageUI.GENERAL_CONSENT && currentPage == ConsentiumPageUI.DETAILS_CONSENT -> {
currentPage = ConsentiumPageUI.GENERAL_CONSENT
}
currentPage == ConsentiumPageUI.COOKIES_POLICY -> {
currentPage = ConsentiumPageUI.GENERAL_CONSENT
}
else -> {
onQuitConsent()
}
}
}
// View
CompositionLocalProvider(
LocalColors provides colors,
@ -112,8 +129,11 @@ fun ConsentiumComponent(
}
},
onClickCookiesPolicies = {
// TODO Open cookies policies
currentPage = ConsentiumPageUI.COOKIES_POLICY
},
onClickRetry = {
viewModel.init(apiKey = consentium.apiKey, appId = consentium.appId)
}
)
}
}

View File

@ -29,7 +29,7 @@ import fr.openium.consentium_ui.ui.components.core.button.ConsentiumUIButtonStyl
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
import fr.openium.consentium_ui.ui.utils.toRichHtmlString
@Composable
@ -55,7 +55,7 @@ internal fun ConsentiumUIDetailConsentComponent(
IconButton(onClick = { onNavigateBack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
tint = ConsentiumUITheme.colors.onSurface,
modifier = Modifier.size(26.dp),
)
@ -73,7 +73,7 @@ internal fun ConsentiumUIDetailConsentComponent(
Spacer(modifier = Modifier.height(25.dp))
Text(
text = htmlToAnnotatedString(detailConsentUI.conservationDurationText),
text = detailConsentUI.conservationDurationText.toRichHtmlString(),
style = ConsentiumUITheme.typography.p3,
color = ConsentiumUITheme.colors.onSurface,
)

View File

@ -2,19 +2,31 @@ package fr.openium.consentium_ui.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import fr.openium.consentium_ui.R
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
@Composable
internal fun ConsentiumUIErrorComponent(
errorMessage: String,
onRetry: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -22,6 +34,15 @@ internal fun ConsentiumUIErrorComponent(
text = errorMessage,
color = ConsentiumUITheme.colors.error,
style = ConsentiumUITheme.typography.b2,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(24.dp))
ConsentButton(
text = stringResource(R.string.retry),
buttonStyle = ConsentiumUIButtonStyle.PRIMARY,
onclick = onRetry,
)
}
}

View File

@ -15,6 +15,7 @@ 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@ -25,7 +26,7 @@ import fr.openium.consentium_ui.ui.components.core.button.ConsentiumUIButtonStyl
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
import fr.openium.consentium_ui.ui.utils.toRichHtmlString
@Composable
internal fun ConsentiumUIGeneralConsentComponent(
@ -42,36 +43,40 @@ internal fun ConsentiumUIGeneralConsentComponent(
.fillMaxSize()
.padding(horizontal = 24.dp),
) {
if (generalConsentUI.iconUrl != null) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
AsyncImage(
modifier = Modifier.heightIn(min = 48.dp),
model = generalConsentUI.iconUrl,
contentDescription = stringResource(R.string.app_icon),
contentScale = ContentScale.FillBounds,
)
}
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))
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = htmlToAnnotatedString(generalConsentUI.mainConsentText),
text = generalConsentUI.mainConsentText.toRichHtmlString(),
style = ConsentiumUITheme.typography.p3,
textAlign = TextAlign.Start,
color = ConsentiumUITheme.colors.onSurface,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(16.dp))
TextLink(
text = stringResource(R.string.cookies),
onclick = onClickCookiesPolicies,
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier
.heightIn(16.dp)
.weight(1f))
TextLink(
text = stringResource(R.string.refuse),

View File

@ -0,0 +1,28 @@
package fr.openium.consentium_ui.ui.components
import android.view.ViewGroup
import android.webkit.WebView
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
@Composable
internal fun ConsentiumUIWebview(
url: String,
modifier: Modifier = Modifier,
) {
AndroidView(
modifier = modifier,
factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
},
update = { webView ->
webView.loadUrl(url)
}
)
}

View File

@ -14,6 +14,8 @@ 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.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.openium.consentium_ui.R
@ -21,14 +23,14 @@ 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
import fr.openium.consentium_ui.ui.utils.toRichHtmlString
@Composable
internal fun PurposeComponent(
purposeUI: PurposeUI,
) {
// Properties
var isChecked by remember { mutableStateOf(purposeUI.isAccepted != PurposeStatusUI.REJECTED) }
var isChecked by remember { mutableStateOf(purposeUI.choice != PurposeStatusUI.REJECTED) }
// View
Column(
@ -58,11 +60,15 @@ internal fun PurposeComponent(
checked = isChecked,
onCheckedChange = {
isChecked = !isChecked
purposeUI.isAccepted = if (isChecked) PurposeStatusUI.ACCEPTED else PurposeStatusUI.REJECTED
purposeUI.choice = if (isChecked) PurposeStatusUI.ACCEPTED else PurposeStatusUI.REJECTED
purposeUI.vendors.forEach { vendorUI ->
vendorUI.isAccepted = isChecked
}
},
modifier = Modifier
.semantics {
contentDescription = purposeUI.title
}
)
}
@ -81,7 +87,7 @@ internal fun PurposeComponent(
Spacer(modifier = Modifier.height(10.dp))
Text(
text = htmlToAnnotatedString(purposeUI.description),
text = purposeUI.description.toRichHtmlString(),
style = ConsentiumUITheme.typography.p3,
color = ConsentiumUITheme.colors.onSurface,
)
@ -95,7 +101,7 @@ private fun PurposeComponentPreview() {
purposeUI = PurposeUI(
id = "1",
isRequired = true,
isAccepted = PurposeStatusUI.ACCEPTED,
choice = PurposeStatusUI.ACCEPTED,
title = "Title",
description = "Description",
vendors = emptyList(),

View File

@ -15,6 +15,7 @@ import fr.openium.consentium_ui.ui.components.style.ConsentiumUITheme
internal fun ConsentiumUISwitch(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Switch(
checked = checked,
@ -22,6 +23,7 @@ internal fun ConsentiumUISwitch(
colors = SwitchDefaults.colors(
checkedTrackColor = ConsentiumUITheme.colors.secondary,
),
modifier = modifier,
)
}

View File

@ -12,7 +12,7 @@ import androidx.core.graphics.ColorUtils
object ConsentiumDefaults {
private const val SURFACE_PRIMARY_COLOR_BLEND_RATIO = 0.1f
private const val OUTLINE_COLOR_BLEND_RATIO = 0.1f
private object Typography {
private val DEFAULT_LINE_HEIGHT = 24.sp
@ -92,7 +92,7 @@ object ConsentiumDefaults {
fun colors(
primary: Color = MaterialTheme.colorScheme.primary,
onPrimary: Color = MaterialTheme.colorScheme.onPrimary,
secondary: Color = MaterialTheme.colorScheme.background,
secondary: Color = MaterialTheme.colorScheme.secondary,
onSecondary: Color = MaterialTheme.colorScheme.surfaceVariant,
tertiary: Color = MaterialTheme.colorScheme.onBackground,
onSurfaceVariant: Color = MaterialTheme.colorScheme.onBackground,
@ -101,7 +101,7 @@ object ConsentiumDefaults {
ColorUtils.blendARGB(
onPrimary.toArgb(),
primary.toArgb(),
SURFACE_PRIMARY_COLOR_BLEND_RATIO,
OUTLINE_COLOR_BLEND_RATIO,
)
),
error: Color = MaterialTheme.colorScheme.error,

View File

@ -2,5 +2,6 @@ package fr.openium.consentium_ui.ui.model
enum class ConsentiumPageUI {
GENERAL_CONSENT,
DETAILS_CONSENT
DETAILS_CONSENT,
COOKIES_POLICY,
}

View File

@ -1,6 +1,7 @@
package fr.openium.consentium_ui.ui.model
internal data class DetailConsentUI(
val cookiePolicyUrl: String,
val conservationDurationText: String,
val purposes: List<PurposeUI>,
)

View File

@ -2,7 +2,7 @@ package fr.openium.consentium_ui.ui.model
internal data class GeneralConsentUI(
val applicationName: String,
val iconUrl: String,
val iconUrl: String?,
val mainConsentText: String,
val consentPageUrl: String,
)

View File

@ -3,5 +3,5 @@ package fr.openium.consentium_ui.ui.model
internal enum class PurposeStatusUI {
ACCEPTED,
REJECTED,
NOT_DEFINED,
PARTIAL,
}

View File

@ -3,7 +3,7 @@ package fr.openium.consentium_ui.ui.model
internal data class PurposeUI(
val id: String,
val isRequired: Boolean,
var isAccepted: PurposeStatusUI,
var choice: PurposeStatusUI,
val title: String,
val description: String,
val vendors: List<VendorUI>,

View File

@ -1,53 +1,17 @@
package fr.openium.consentium_ui.ui.utils
import android.text.Spanned
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
import com.mohamedrejeb.richeditor.model.rememberRichTextState
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).forEach { span ->
val startSpan = spanned.getSpanStart(span)
val endSpan = spanned.getSpanEnd(span)
@Composable
fun String.toRichHtmlString(): AnnotatedString {
val state = rememberRichTextState()
// 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())
}
LaunchedEffect(this) {
state.setHtml(this@toRichHtmlString)
}
}
fun Any.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()
}
return state.annotatedString
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_icon">Icone de l\'application</string>
<string name="back">Retour</string>
<string name="cookies">Politique de cookies</string>
<string name="accept">Accepter et fermer</string>
<string name="refuse">Continuer sans accepter</string>
<string name="parameters">Paramétrer mes choix</string>
<string name="save">Enregistrer et fermer</string>
<string name="require">Requis</string>
<string name="retry">Réessayer</string>
<string name="save_consents_error_message">Une erreur est survenue l\'or de la sauvegarde de vos consentements.</string>
<string name="consents_error_loading_consents">Il y a eu une erreur lors du chargement de vos consentements</string>
</resources>

View File

@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cookies">Politique de cookies</string>
<string name="accept">Accepter et fermer</string>
<string name="refuse">Continuer sans accepter</string>
<string name="parameters">Paramétrer mes choix</string>
<string name="save">Enregistrer et fermer</string>
<string name="require">Requis</string>
<string name="save_consents_error_message">Une erreur est survenue l\'or de la ssauvegarde de vos consentements.</string>
<string name="app_icon">Application icon</string>
<string name="back">Back</string>
<string name="cookies">Cookie policy</string>
<string name="accept">Accept and close</string>
<string name="refuse">Continue without accepting</string>
<string name="parameters">Set my choices</string>
<string name="save">Save and close</string>
<string name="require">Required</string>
<string name="retry">Try again</string>
<string name="save_consents_error_message">An error occurred while saving your consents.</string>
<string name="consents_error_loading_consents">There was an error uploading your consents</string>
</resources>

View File

@ -20,11 +20,14 @@ android {
buildTypes {
debug {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
consumerProguardFiles("proguard-rules.pro")
}
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
consumerProguardFiles("proguard-rules.pro")
}
}
@ -34,6 +37,10 @@ android {
dimension = "version"
}
create("dev") {
dimension = "version"
}
create("demo") {
dimension = "version"
}

View File

@ -18,4 +18,42 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-dontwarn java.lang.invoke.StringConcatFactory
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exception
-keep class com.google.firebase.crashlytics.** { *; }
-dontwarn com.google.firebase.crashlytics.**
# Keep enums
-keep public enum fr.openium.consentium.**{
*;
}
# Garder les annotations de Kotlin
-keepattributes *Annotation*
# Garder les classes et les membres annotés avec @Keep
-keep @androidx.annotation.Keep class * { *; }
-keepclassmembers class ** {
@androidx.annotation.Keep *;
}
# Garder les classes et les membres annotés avec @Serializable
-keep @kotlinx.serialization.Serializable class * { *; }
-keepclassmembers class ** {
@kotlinx.serialization.Serializable *;
}
# Garder les classes annotées avec @HiltAndroidApp
-keep @dagger.hilt.android.HiltAndroidApp class * { *; }
# Garder les classes et les membres pour Timber
-keep class timber.log.Timber { *; }
-keep interface timber.log.Timber$Tree { *; }
-keep class fr.openium.consentium.** { *; }
-keepclassmembers class fr.openium.consentium.** { *; }

View File

@ -1,13 +1,10 @@
package fr.openium.consentium
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -17,8 +14,6 @@ import org.junit.Assert.*
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.openium.consentium.test", appContext.packageName)
assertTrue(true)
}
}

View File

@ -0,0 +1,37 @@
package fr.openium.consentium
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.di.ConsentiumUrl
import fr.openium.consentium.data.di.NetworkModule
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
@Module
@InstallIn(SingletonComponent::class)
class DevNetworkModule {
@Provides
fun provideOkHttpBuilder(
@ApplicationContext context: Context,
): OkHttpClient.Builder =
NetworkModule.standardOkHttpBuilder(context)
.addNetworkInterceptor(HttpLoggingInterceptor { message ->
Timber.tag("OkHttp")
Timber.v(message)
}.apply {
level = HttpLoggingInterceptor.Level.BODY
})
@ConsentiumUrl
@Provides
fun okHttpUrlConsentium(@ApplicationContext context: Context): HttpUrl =
"https://www.exemple.com".toHttpUrl()
}

View File

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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="backend_url" translatable="false">https://consentium-api-dev.openium.fr/api/v1/app/</string>
</resources>

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -2,7 +2,10 @@ package fr.openium.consentium.api
import android.content.Context
import dagger.hilt.android.EntryPointAccessors
import fr.openium.consentium.api.model.ConsentState
import fr.openium.consentium.api.model.Purpose
import fr.openium.consentium.api.model.PurposeChoice
import fr.openium.consentium.api.model.PurposeStatus
import fr.openium.consentium.api.state.FetchConsentiumState
import fr.openium.consentium.api.state.SetConsentiumState
import fr.openium.consentium.domain.di.RepositoryEntryPoint
@ -14,7 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
class Consentium(
context: Context,
val applicationId: String,
val apiKey: String,
val appId: String,
) {
private val _fetchConsentState = MutableStateFlow<FetchConsentiumState>(FetchConsentiumState.Idle)
val fetchConsentState by lazy { _fetchConsentState.asStateFlow() }
@ -31,11 +35,11 @@ class Consentium(
suspend fun fetchConsents() {
_fetchConsentState.value = FetchConsentiumState.Loading
try {
when (val consentResponse = consentiumRepository.getConsents(applicationId)) {
when (val consentResponse = consentiumRepository.getConsents(apiKey)) {
ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error
is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> {
val areConsentsValid = consentResponse.isValid
val areConsentsValid = consentResponse.state == ConsentState.VALID
if (areConsentsValid) {
_fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes)
} else {
@ -53,7 +57,7 @@ class Consentium(
) {
_saveConsentState.emit(SetConsentiumState.Loading)
try {
when (consentiumRepository.setConsents(applicationId, consent)) {
when (consentiumRepository.setConsents(apiKey, consent)) {
ConsentiumRepositorySetResponse.Error -> {
_saveConsentState.emit(SetConsentiumState.Error)
}
@ -66,4 +70,12 @@ class Consentium(
_saveConsentState.emit(SetConsentiumState.Error)
}
}
}
fun List<Purpose>.checkPurposeState(purposeId: String, onPurposeState: (PurposeStatus) -> Unit) {
onPurposeState(find { it.identifier == purposeId }?.choice ?: PurposeStatus.REJECTED)
}
fun List<Purpose>.checkPurposeState(purposeId: String): PurposeStatus {
return find { it.identifier == purposeId }?.choice ?: PurposeStatus.REJECTED
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
package fr.openium.consentium
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.di.ConsentiumUrl
import fr.openium.consentium.data.di.NetworkModule
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object ReleaseNetworkModule {
@Provides
fun provideOkHttpBuilder(@ApplicationContext context: Context): OkHttpClient.Builder {
return NetworkModule.standardOkHttpBuilder(context)
}
@ConsentiumUrl
@Provides
fun okHttpUrlConsentium(@ApplicationContext context: Context): HttpUrl =
context.getString(R.string.backend_url).toHttpUrl()
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="backend_url" translatable="false" tools:ignore="UnusedResources">https://consentium-api-dev.openium.fr/api/v1/app</string>
<string name="backend_url" translatable="false" tools:ignore="UnusedResources">https://consentium-api.openium.fr/api/v1/app/</string>
</resources>

View File

@ -23,9 +23,6 @@ composeBom = "2024.11.00"
# Timber
timber = "5.0.1"
# Material
material = "1.12.0"
# Serialization
serialization = "2.0.0"
jsonSerialization = "1.7.1"
@ -46,7 +43,7 @@ junitExtVersion = "1.2.1"
preferencesDataStore = "1.1.1"
# Plugins
agp = "8.7.3"
agp = "8.8.1"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.24"
junitVersion = "1.2.1"
@ -68,27 +65,31 @@ 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"
# Publish
publish = "1.2"
[libraries]
# AndroidX
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
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" }
# Lifecycle
androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
# Hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
@ -100,22 +101,18 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-ui = { module = "androidx.compose.ui:ui" }
compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-graphics = { group = "androidx.compose.ui", name = "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" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
# Kotlin serizalization
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jsonSerialization" }
# Compose navigation
androidx-navigation-common-ktx = { group = "androidx.navigation", name = "navigation-common-ktx", version.ref = "navigationCompose" }
androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationCompose" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
# Preferences DataStore
@ -142,18 +139,19 @@ matomo = { module = "com.github.matomo-org:matomo-sdk-android", version.ref = "m
# Clarity
clarity = { group = "com.microsoft.clarity", name = "clarity", version.ref = "clarityVersion" }
# GA4 (Firebase Analytics)
# GA4
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" }
# Rich text formating
rich-text = { module = "com.mohamedrejeb.richeditor:richeditor-compose", version = "1.0.0-rc05-k2" }
# Publish
openium-publish = { group = "fr.openium", name = "publish-plugin", version.ref = "publish" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
@ -162,8 +160,8 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
android-library = { id = "com.android.library", version.ref = "agp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" }
firebaseCrashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
googleServices = { id = "com.google.gms.google-services", version.ref = "googleServicesPlugin" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
google-services = { id = "com.google.gms.google-services", version.ref = "googleServicesPlugin" }
[bundles]
androidx = ["androidx-core-ktx", "androidx-activity-compose"]

View File

@ -1,6 +1,6 @@
#Mon Dec 09 14:35:54 CET 2024
#Mon Feb 17 11:35:23 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists