19 Commits

Author SHA1 Message Date
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
106 changed files with 2331 additions and 414 deletions

15
Jenkinsfile vendored
View File

@ -1,3 +1,16 @@
library "openiumpipeline" 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 org.gradle.language.nativeplatform.internal.BuildType
import java.io.FileInputStream import java.io.FileInputStream
import java.util.Properties import java.util.Properties
@ -6,11 +7,12 @@ plugins {
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt) alias(libs.plugins.hilt)
alias(libs.plugins.serialization) alias(libs.plugins.serialization)
alias(libs.plugins.kotlin.compose)
id("fr.openium.publish")
} }
apply(from = "publish.build.gradle")
// Keystore // Keystore
val keystorePropertiesFile = rootProject.file("keys/keystore.properties") val keystorePropertiesFile = rootProject.file("keys/keystore.properties")
@ -25,8 +27,6 @@ android {
applicationId = "fr.openium.consentium" applicationId = "fr.openium.consentium"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -49,6 +49,7 @@ android {
buildTypes { buildTypes {
debug { debug {
isMinifyEnabled = false isMinifyEnabled = false
isShrinkResources = false
versionNameSuffix = "-debug" versionNameSuffix = "-debug"
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
@ -73,6 +74,10 @@ android {
dimension = "version" dimension = "version"
} }
create("dev") {
dimension = "version"
}
create("demo") { create("demo") {
dimension = "version" dimension = "version"
} }
@ -88,6 +93,19 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
} }
testOptions {
animationsDisabled = true
managedDevices {
devices {
create<ManagedVirtualDevice>("pixel5") {
device = "Pixel 5"
apiLevel = 34
systemImageSource = "google"
}
}
}
}
} }
dependencies { dependencies {
@ -106,6 +124,8 @@ dependencies {
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose) implementation(libs.hilt.navigation.compose)
// Libs analytics
implementation(libs.matomo) implementation(libs.matomo)
implementation(libs.clarity) implementation(libs.clarity)
implementation(libs.ga4) implementation(libs.ga4)
@ -128,5 +148,4 @@ dependencies {
// Kotlin serialization // Kotlin serialization
implementation(libs.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 { class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
// Context of the app under test. assertTrue(true)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.openium.consentium", appContext.packageName)
} }
} }

View File

@ -1,6 +1,6 @@
package fr.openium.consentium package fr.openium.consentium
import fr.openium.consentium.ui.navigation.DemoNavGraph import DemoNavGraph
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent

View File

@ -1,41 +1,82 @@
package fr.openium.consentium.ui.navigation import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import Destination
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.slideIn import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.runtime.Composable 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.compose.ui.unit.IntOffset
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable 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.main.MainScreen
import fr.openium.consentium.ui.screens.splash.SplashScreen import fr.openium.consentium.ui.screens.splash.SplashScreen
import fr.openium.consentium_ui.ui.components.ConsentiumComponent
import fr.openium.consentium_ui.ui.model.ConsentiumPageUI
private const val NAV_ANIMATION_TIME = 500 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 @Composable
fun DemoNavGraph(navHostController: NavHostController) { fun DemoNavGraph(navHostController: NavHostController) {
NavHost( NavHost(
navController = navHostController, navController = navHostController,
startDestination = Destination.Splash, 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( SplashScreen(
navigateToMain = { navigateToMain = {
navHostController.navigate(Destination.Main) { navHostController.navigate(Destination.Main) {
@ -43,23 +84,70 @@ fun DemoNavGraph(navHostController: NavHostController) {
saveState = true saveState = true
} }
} }
}
)
}
composable<Destination.Main> {
MainScreen(
onGoToConsentDetail = {
navHostController.navigate(Destination.Consent)
}, },
onGoToConsentMaster = { navigateToConsent = {
navHostController.navigate(Destination.Consent) navHostController.navigate(Destination.Consent(ConsentiumPageUI.GENERAL_CONSENT))
} }
) )
} }
composable<Destination.Consent> { composable<Destination.Main>(
// TODO 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))
},
onGoToConsentDetail = {
navHostController.navigate(Destination.Consent(ConsentiumPageUI.DETAILS_CONSENT))
},
)
}
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,
onQuitConsent = {
navHostController.navigate(Destination.Main) {
popUpTo(navHostController.graph.findStartDestination().id) {
saveState = true
}
}
},
consentium = Consentium(
context = context,
apiKey = apiKey,
appId = appId,
)
)
} }
} }
} }

View File

@ -1,3 +1,4 @@
import fr.openium.consentium_ui.ui.model.ConsentiumPageUI
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -10,6 +11,8 @@ sealed interface Destination {
data object Main : Destination data object Main : Destination
@Serializable @Serializable
data object Consent : Destination data class Consent(
val landingPage: ConsentiumPageUI
): Destination
} }

View File

@ -1,10 +0,0 @@
package fr.openium.consentium.ui.screens.consent
import androidx.compose.runtime.Composable
@Composable
fun ConsentScreen() {
}

View File

@ -18,7 +18,6 @@ fun MainScreen(
onGoToConsentDetail: () -> Unit, onGoToConsentDetail: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
// View // View
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@ -43,7 +42,5 @@ fun MainScreen(
text = "Go to Consent Detail" text = "Go to Consent Detail"
) )
} }
} }
} }

View File

@ -10,36 +10,74 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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 @Composable
fun SplashScreen( fun SplashScreen(
navigateToMain: () -> Unit, navigateToMain: () -> Unit,
viewModel: SplashScreenViewModel = hiltViewModel(), navigateToConsent: () -> Unit,
) { ) {
// State // Property
val currentState by viewModel.state.collectAsState() val context = LocalContext.current
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 // Effect
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.initMain() consentium.fetchConsentState.collect { consentState ->
when (consentState) {
FetchConsentiumState.Idle,
FetchConsentiumState.Loading,
-> {
} }
LaunchedEffect(currentState) { FetchConsentiumState.Error -> {
when (val state = currentState) { // Handle error
is SplashScreenViewModel.State.Loaded -> { consentium.fetchConsents()
if (state.isSplashEnded) { }
is FetchConsentiumState.Invalid -> {
navigateToConsent()
}
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() navigateToMain()
} }
} }
} }
} }
LaunchedEffect(Unit) {
consentium.fetchConsents()
}
// View // View
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -47,7 +85,7 @@ fun SplashScreen(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text("Splash Screen") Text("Splash Screen") // TODO
Spacer(modifier = Modifier.height(14.dp)) Spacer(modifier = Modifier.height(14.dp))

View File

@ -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>(State.Loaded(false))
val state: StateFlow<State> = _state
fun initMain() {
viewModelScope.launch {
delay(1500L)
_state.value = State.Loaded(true)
}
}
sealed interface State {
data class Loaded(val isSplashEnded: Boolean) : State
}
}

View File

@ -2,10 +2,21 @@ package fr.openium.consentium.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) val Primary = Color(0xFF70ACDC)
val PurpleGrey80 = Color(0xFFCCC2DC) val OnPrimary = Color(0XFFFFFFFF)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val Secondary = Color(0xFFF29413)
val PurpleGrey40 = Color(0xFF625b71) val OnSecondary = Color(0xFFFFFFFF)
val Pink40 = Color(0xFF7D5260)
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)

View File

@ -1,39 +1,53 @@
package fr.openium.consentium.ui.theme package fr.openium.consentium.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = Primary,
secondary = PurpleGrey80, onPrimary = OnPrimary,
tertiary = Pink80
secondary = Secondary,
onSecondary = OnSecondary,
tertiary = Tertiary,
onSurfaceVariant = OnSurfaceVariant,
onSurface = OnSurface,
error = Error,
surfaceContainerHighest = SurfaceHighest,
surfaceContainerHigh = SurfaceHigh,
surfaceContainer = SurfaceMiddle,
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = Primary,
secondary = PurpleGrey40, onPrimary = OnPrimary,
tertiary = Pink40
secondary = Secondary,
onSecondary = OnSecondary,
tertiary = Tertiary,
onSurfaceVariant = OnSurfaceVariant,
onSurface = OnSurface,
error = Error,
surfaceContainerHighest = SurfaceHighest,
surfaceContainerHigh = SurfaceHigh,
surfaceContainer = SurfaceMiddle,
) )
@Composable @Composable
fun ConsentiumTheme( fun ConsentiumTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val colorScheme = when { 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 darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }

View File

@ -1,3 +1,5 @@
<resources> <resources>
<string name="app_name">Consentium</string> <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> </resources>

3
app/version.properties Normal file
View File

@ -0,0 +1,3 @@
#Wed Sep 27 10:07:57 CEST 2023
VERSION_NAME=1.0.0
VERSION_CODE=1

View File

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

View File

@ -3,6 +3,8 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.serialization) alias(libs.plugins.serialization)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt)
} }
android { android {
@ -17,9 +19,26 @@ android {
} }
buildTypes { buildTypes {
release { debug {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
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")
} }
} }
@ -29,6 +48,10 @@ android {
dimension = "version" dimension = "version"
} }
create("dev") {
dimension = "version"
}
create("demo") { create("demo") {
dimension = "version" dimension = "version"
} }
@ -45,6 +68,10 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
buildFeatures {
compose = true
}
} }
dependencies { dependencies {
@ -54,6 +81,20 @@ dependencies {
// AndroidX // AndroidX
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat) 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)
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 // Serialization
implementation(libs.kotlin.serialization) implementation(libs.kotlin.serialization)
@ -65,11 +106,19 @@ dependencies {
// Hilt // Hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
implementation(libs.hilt.navigation.compose)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
// Timber // Timber
implementation(libs.timber) implementation(libs.timber)
// Coil
implementation(libs.coil)
implementation(libs.coil.network)
// Rich text formatting
implementation(libs.rich.text)
// Tests // Tests
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -19,3 +19,55 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # 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 package fr.openium.consentium_ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
@ -17,8 +14,6 @@ import org.junit.Assert.*
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
// Context of the app under test. assertTrue(true)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.openium.consentium_ui.test", appContext.packageName)
} }
} }

View File

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

View File

@ -9,8 +9,8 @@ import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.di.ConsentiumUrl import fr.openium.consentium.data.di.ConsentiumUrl
import fr.openium.consentium.data.di.OkHttpClientDefault import fr.openium.consentium.data.di.OkHttpClientDefault
import fr.openium.consentium_ui.BuildConfig 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.ConsentiumUIApi
import fr.openium.consentium_ui.data.remote.mock.ConsentiumUIMockApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType

View File

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

View File

@ -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<GetConsentConfigDTO> {
return Response.success(consents)
}
}

View File

@ -0,0 +1,109 @@
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 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",
consentMainTextTranslation = listOf(
MainConsentTextTranslationDTO(
id = "UUID",
language = "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,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
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>",
name = "Nécessaire"
)
),
),
PurposeDTO(
id = "purpose-advertising",
order = 1,
isRequired = false,
choice = PurposeStatusDTO.REFUSED,
translations = listOf(
PurposeTranslationDTO(
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>",
name = "Publicité"
)
),
),
PurposeDTO(
id = "purpose-analytics",
order = 2,
isRequired = false,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
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>",
name = "Analyse"
)
),
),
PurposeDTO(
id = "purpose-personalization",
order = 3,
isRequired = false,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
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>",
name = "Personnalisation"
)
),
),
PurposeDTO(
id = "purpose-social",
order = 4,
isRequired = false,
choice = PurposeStatusDTO.ACCEPTED,
translations = listOf(
PurposeTranslationDTO(
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>",
name = "Social"
)
),
)
)
)
override suspend fun getConsentConfig(
token: String,
applicationId: String,
): Response<GetConsentConfigDTO> {
return Response.success(consents)
}
}

View File

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

View File

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

View File

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

View File

@ -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("refused")
REFUSED,
@SerialName("partial")
PARTIAL,
}

View File

@ -8,4 +8,5 @@ internal data class PurposeTranslationDTO(
@SerialName("id") val id: String, @SerialName("id") val id: String,
@SerialName("lang") val language: String, @SerialName("lang") val language: String,
@SerialName("text") val text: String, @SerialName("text") val text: String,
@SerialName("name") val name: String,
) )

View File

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

View File

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

View File

@ -8,6 +8,7 @@ internal fun PurposeTranslationDTO.toPurposeTranslationData() =
id = id, id = id,
language = language, language = language,
text = text, text = text,
name = name,
) )
internal fun List<PurposeTranslationDTO>.toPurposeTranslationDataList() = map { it.toPurposeTranslationData() } internal fun List<PurposeTranslationDTO>.toPurposeTranslationDataList() = map { it.toPurposeTranslationData() }

View File

@ -4,6 +4,8 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import fr.openium.consentium_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.GetConfigTextForLanguageUseCase
import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCaseImpl import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCaseImpl
@ -16,4 +18,9 @@ internal interface ConsentiumUseCaseModule {
getConfigTextForLanguageUseCaseImpl: GetConfigTextForLanguageUseCaseImpl, getConfigTextForLanguageUseCaseImpl: GetConfigTextForLanguageUseCaseImpl,
): GetConfigTextForLanguageUseCase ): GetConfigTextForLanguageUseCase
@Binds
fun bindGetApplicationLangageUseCase(
getApplicationLangageUseCaseImpl: GetApplicationLanguageUseCaseImpl,
): GetApplicationLanguageUseCase
} }

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package fr.openium.consentium_ui.domain.model
internal enum class PurposeStatusData {
ACCEPTED,
REJECTED,
PARTIAL,
}

View File

@ -4,4 +4,5 @@ internal data class PurposeTranslationData(
val id: String, val id: String,
val language: String, val language: String,
val text: String, val text: String,
val name: String,
) )

View File

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

View File

@ -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
}
}

View File

@ -6,22 +6,24 @@ import javax.inject.Inject
private const val FALLBACK_LANGUAGE = "en" private const val FALLBACK_LANGUAGE = "en"
internal interface GetConfigTextForLanguageUseCase { internal interface GetConfigTextForLanguageUseCase {
suspend fun invoke( suspend operator fun invoke(
language: String = FALLBACK_LANGUAGE, language: String = FALLBACK_LANGUAGE,
configData: ContentConfigData, configData: ContentConfigData,
): GetConfigTextForLanguageUseCaseResponce ): GetConfigTextForLanguageUseCaseResponse
} }
internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetConfigTextForLanguageUseCase { internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() :
GetConfigTextForLanguageUseCase {
override suspend fun invoke( override suspend fun invoke(
language: String, language: String,
configData: ContentConfigData, configData: ContentConfigData,
): GetConfigTextForLanguageUseCaseResponce { ): GetConfigTextForLanguageUseCaseResponse {
return try { return try {
val canIUseTheTranslation = configData.mainTextTranslation.any { it.language == language } && val canIUseTheTranslation =
configData.mainTextTranslation.any { it.language == language } &&
configData.purposes.all { purposeData -> configData.purposes.all { purposeData ->
purposeData.translations.any { it.language == language } && purposeData.translations.any { it.language == language } &&
purposeData.vendors.all { vendorData -> purposeData.vendors.all { vendorData ->
@ -32,7 +34,8 @@ internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetCo
val languageToUse = if (canIUseTheTranslation) { val languageToUse = if (canIUseTheTranslation) {
language language
} else { } else {
val isThereAGoodFallbackLanguage = configData.mainTextTranslation.any { it.language == FALLBACK_LANGUAGE } && val isThereAGoodFallbackLanguage =
configData.mainTextTranslation.any { it.language == FALLBACK_LANGUAGE } &&
configData.purposes.all { purposeData -> configData.purposes.all { purposeData ->
purposeData.translations.any { it.language == FALLBACK_LANGUAGE } && purposeData.translations.any { it.language == FALLBACK_LANGUAGE } &&
purposeData.vendors.all { vendorData -> purposeData.vendors.all { vendorData ->
@ -61,23 +64,24 @@ internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetCo
) )
if (languageToUse == FALLBACK_LANGUAGE) { if (languageToUse == FALLBACK_LANGUAGE) {
GetConfigTextForLanguageUseCaseResponce.DefaultLanguage(filteredConfigData) GetConfigTextForLanguageUseCaseResponse.DefaultLanguage(filteredConfigData)
} else { } else {
GetConfigTextForLanguageUseCaseResponce.Success(filteredConfigData) GetConfigTextForLanguageUseCaseResponse.Success(filteredConfigData)
} }
} catch (e: Exception) { } 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
} }

View File

@ -0,0 +1,110 @@
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.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
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,
onClickRetry: () -> Unit,
) {
when (state) {
is ConsentiumUIState.Loading -> {
ConsentiumUILoadingComponent()
}
is ConsentiumUIState.Error -> {
ConsentiumUIErrorComponent(
errorMessage = state.message,
onRetry = onClickRetry
)
}
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) }
)
)
}
else -> {
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
)
}
ConsentiumPageUI.COOKIES_POLICY -> {
ConsentiumUIWebview(
url = state.detailConsentUI.cookiePolicyUrl
)
}
}
}
}
}
}

View File

@ -0,0 +1,79 @@
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.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
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>(ConsentiumUIState.Loading)
val state: StateFlow<ConsentiumUIState> by lazy { _state.asStateFlow() }
fun init(appId: String, apiKey: String) {
viewModelScope.launch {
_state.value = ConsentiumUIState.Loading
when (val consentiumUIFetchResponse = consentiumUIRepository.getConsentiumConfig(appId, apiKey)) {
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(context.getString(R.string.consents_error_loading_consents)))
}
}
}
is ConsentiumUIRepositoryResponse.Error -> {
_state.value = ConsentiumUIState.Error(context.getString(R.string.consents_error_loading_consents))
}
}
}
}
}

View File

@ -0,0 +1,40 @@
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
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 {
it.toPurposeChoice()
}
internal fun DetailConsentUI.toDeniedPurposeChoices(): List<PurposeChoice> = purposes.map {
it.toPurposeChoice().copy(choice = PurposeStatus.REJECTED)
}
internal fun DetailConsentUI.toAcceptedPurposeChoices(): List<PurposeChoice> = purposes.map {
it.toPurposeChoice().copy(choice = PurposeStatus.ACCEPTED)
}
internal fun PurposeUI.toPurposeChoice(): PurposeChoice =
PurposeChoice(
purposeIdentifier = id,
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

@ -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,
)

View File

@ -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.PARTIAL -> PurposeStatusUI.PARTIAL
}

View File

@ -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,
choice = choice.toPurposeStatusUI(),
title = translations.first().name,
description = translations.first().text,
vendors = vendors.toVendorUIList(),
)

View File

@ -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<VendorData>.toVendorUIList() = map { it.toVendorUI() }

View File

@ -0,0 +1,139 @@
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
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(apiKey = consentium.apiKey, appId = consentium.appId)
}
LaunchedEffect(consentiumState) {
when (consentiumState) {
SetConsentiumState.Idle,
SetConsentiumState.Loading,
-> {
}
SetConsentiumState.Error -> {
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
}
SetConsentiumState.Success -> {
onQuitConsent()
}
}
}
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,
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 = {
currentPage = ConsentiumPageUI.COOKIES_POLICY
},
onClickRetry = {
viewModel.init(apiKey = consentium.apiKey, appId = consentium.appId)
}
)
}
}

View File

@ -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.toRichHtmlString
@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 = stringResource(R.string.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 = detailConsentUI.conservationDurationText.toRichHtmlString(),
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))
}
}

View File

@ -0,0 +1,48 @@
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()
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
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

@ -0,0 +1,124 @@
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.text.style.TextOverflow
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.toRichHtmlString
@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),
) {
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,
)
}
Spacer(modifier = Modifier.height(24.dp))
}
Text(
text = generalConsentUI.mainConsentText.toRichHtmlString(),
style = ConsentiumUITheme.typography.p3,
textAlign = TextAlign.Start,
color = ConsentiumUITheme.colors.onSurface,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.height(16.dp))
TextLink(
text = stringResource(R.string.cookies),
onclick = onClickCookiesPolicies,
)
Spacer(modifier = Modifier
.heightIn(16.dp)
.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 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. \n\n 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”.",
applicationName = "Application name",
consentPageUrl = "https://www.google.com"
),
onClickCookiesPolicies = {},
onDenyAndClose = {},
onAcceptAndClose = {},
onNavigateToDetails = {},
loadingElement = LoadingElement.LINK,
)
}

View File

@ -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
)
}
}

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

@ -0,0 +1,110 @@
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.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
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.toRichHtmlString
@Composable
internal fun PurposeComponent(
purposeUI: PurposeUI,
) {
// Properties
var isChecked by remember { mutableStateOf(purposeUI.choice != 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.choice = if (isChecked) PurposeStatusUI.ACCEPTED else PurposeStatusUI.REJECTED
purposeUI.vendors.forEach { vendorUI ->
vendorUI.isAccepted = isChecked
}
},
modifier = Modifier
.semantics {
contentDescription = purposeUI.title
}
)
}
/** 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 = purposeUI.description.toRichHtmlString(),
style = ConsentiumUITheme.typography.p3,
color = ConsentiumUITheme.colors.onSurface,
)
}
}
@Preview
@Composable
private fun PurposeComponentPreview() {
PurposeComponent(
purposeUI = PurposeUI(
id = "1",
isRequired = true,
choice = PurposeStatusUI.ACCEPTED,
title = "Title",
description = "Description",
vendors = emptyList(),
)
)
}

View File

@ -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,
)
}

View File

@ -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 = {}
)
}

View File

@ -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
}
}

View File

@ -0,0 +1,46 @@
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,
modifier: Modifier = Modifier,
) {
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedTrackColor = ConsentiumUITheme.colors.secondary,
),
modifier = modifier,
)
}
@Preview
@Composable
private fun ConsentiumUISwitchPreview() {
Column {
ConsentiumUISwitch(
checked = true,
onCheckedChange = {},
)
Spacer(modifier = Modifier.height(16.dp))
ConsentiumUISwitch(
checked = false,
onCheckedChange = {},
)
}
}

View File

@ -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.secondary,
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,
)

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package fr.openium.consentium_ui.ui.model
enum class ConsentiumPageUI {
GENERAL_CONSENT,
DETAILS_CONSENT,
COOKIES_POLICY,
}

View File

@ -0,0 +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

@ -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,
)

View File

@ -0,0 +1,7 @@
package fr.openium.consentium_ui.ui.model
internal enum class LoadingElement {
BUTTON,
LINK,
NONE,
}

View File

@ -0,0 +1,7 @@
package fr.openium.consentium_ui.ui.model
internal enum class PurposeStatusUI {
ACCEPTED,
REJECTED,
PARTIAL,
}

View File

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

View File

@ -0,0 +1,6 @@
package fr.openium.consentium_ui.ui.model
internal data class VendorUI(
val id: String,
var isAccepted: Boolean,
)

View File

@ -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
}

View File

@ -0,0 +1,17 @@
package fr.openium.consentium_ui.ui.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.text.AnnotatedString
import com.mohamedrejeb.richeditor.model.rememberRichTextState
@Composable
fun String.toRichHtmlString(): AnnotatedString {
val state = rememberRichTextState()
LaunchedEffect(this) {
state.setHtml(this@toRichHtmlString)
}
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

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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 { buildTypes {
debug { debug {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
consumerProguardFiles("proguard-rules.pro")
} }
release { release {
isMinifyEnabled = true isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
consumerProguardFiles("proguard-rules.pro")
} }
} }
@ -34,6 +37,10 @@ android {
dimension = "version" dimension = "version"
} }
create("dev") {
dimension = "version"
}
create("demo") { create("demo") {
dimension = "version" dimension = "version"
} }

View File

@ -19,3 +19,41 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # 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 package fr.openium.consentium
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
@ -17,8 +14,6 @@ import org.junit.Assert.*
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
// Context of the app under test. assertTrue(true)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.openium.consentium.test", appContext.packageName)
} }
} }

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 @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class ConsentiumDebugNetworkModule { class DevNetworkModule {
@Provides @Provides
fun provideOkHttpBuilder( 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"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -2,11 +2,10 @@ package fr.openium.consentium.api
import android.content.Context import android.content.Context
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import fr.openium.consentium.api.model.ConsentState
import fr.openium.consentium.api.model.Purpose
import fr.openium.consentium.api.model.PurposeChoice 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.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.FetchConsentiumState
import fr.openium.consentium.api.state.SetConsentiumState import fr.openium.consentium.api.state.SetConsentiumState
import fr.openium.consentium.domain.di.RepositoryEntryPoint import fr.openium.consentium.domain.di.RepositoryEntryPoint
@ -18,13 +17,14 @@ import kotlinx.coroutines.flow.asStateFlow
class Consentium( class Consentium(
context: Context, context: Context,
val applicationId: String, val apiKey: String,
val appId: String,
) { ) {
private val _fetchConsentState = MutableStateFlow<FetchConsentiumState>(FetchConsentiumState.Idle) private val _fetchConsentState = MutableStateFlow<FetchConsentiumState>(FetchConsentiumState.Idle)
val fetchConsentState by lazy { _fetchConsentState.asStateFlow() } val fetchConsentState by lazy { _fetchConsentState.asStateFlow() }
private val _setConsentState = MutableStateFlow<SetConsentiumState>(SetConsentiumState.Idle) private val _saveConsentState = MutableStateFlow<SetConsentiumState>(SetConsentiumState.Idle)
val setConsentState by lazy { _setConsentState.asStateFlow() } val saveConsentState by lazy { _saveConsentState.asStateFlow() }
private val consentiumRepository: ConsentiumRepository by lazy { private val consentiumRepository: ConsentiumRepository by lazy {
val appContext = context.applicationContext val appContext = context.applicationContext
@ -32,42 +32,14 @@ class Consentium(
entryPoint.provideConsentiumRepository() 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() { suspend fun fetchConsents() {
_fetchConsentState.value = FetchConsentiumState.Loading _fetchConsentState.value = FetchConsentiumState.Loading
try { try {
when (val consentResponse = consentiumRepository.getConsents(applicationId)) { when (val consentResponse = consentiumRepository.getConsents(apiKey)) {
ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error
is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> { is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> {
val areConsentsValid = consentResponse.isValid val areConsentsValid = consentResponse.state == ConsentState.VALID
if (areConsentsValid) { if (areConsentsValid) {
_fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes) _fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes)
} else { } else {
@ -75,24 +47,35 @@ class Consentium(
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
_fetchConsentState.value = FetchConsentiumState.Error _fetchConsentState.value = FetchConsentiumState.Error
} }
} }
suspend fun setConsents( suspend fun saveConsents(
consent: List<PurposeChoice>, consent: List<PurposeChoice>,
) { ) {
_setConsentState.value = SetConsentiumState.Loading _saveConsentState.emit(SetConsentiumState.Loading)
try { try {
when (consentiumRepository.setConsents(applicationId, consent)) { when (consentiumRepository.setConsents(apiKey, consent)) {
ConsentiumRepositorySetResponse.Error -> _setConsentState.value = SetConsentiumState.Error ConsentiumRepositorySetResponse.Error -> {
ConsentiumRepositorySetResponse.SetConsentsSuccess -> _setConsentState.value = SetConsentiumState.Success _saveConsentState.emit(SetConsentiumState.Error)
} }
ConsentiumRepositorySetResponse.SetConsentsSuccess -> {
_saveConsentState.emit(SetConsentiumState.Success)
}
}
} catch (e: Exception) { } catch (e: Exception) {
_setConsentState.value = SetConsentiumState.Error _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

@ -1,12 +1,10 @@
package fr.openium.consentium.api.adapter package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.Purpose import fr.openium.consentium.api.model.Purpose
import fr.openium.consentium.api.model.PurposeIdentifier
import fr.openium.consentium.data.remote.model.GetConsent import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.PurposeDTO.toPurpose() = Purpose( internal fun GetConsent.PurposeDTO.toPurpose() = Purpose(
identifier = PurposeIdentifier.fromString(identifier), identifier = identifier,
isRequired = isRequired, choice = choice.toPurposeStatus(),
isAccepted = isAccepted.toPurposeStatus(),
vendors = vendors?.map { it.toVendor() } ?: emptyList(), vendors = vendors?.map { it.toVendor() } ?: emptyList(),
) )

View File

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

View File

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

View File

@ -1,11 +1,10 @@
package fr.openium.consentium.api.adapter package fr.openium.consentium.api.adapter
import fr.openium.consentium.api.model.Vendor import fr.openium.consentium.api.model.Vendor
import fr.openium.consentium.api.model.VendorIdentifier
import fr.openium.consentium.data.remote.model.GetConsent import fr.openium.consentium.data.remote.model.GetConsent
internal fun GetConsent.VendorDTO.toVendor() = Vendor( internal fun GetConsent.VendorDTO.toVendor() = Vendor(
identifier = VendorIdentifier.fromString(identifier), identifier = identifier,
isAccepted = isAccepted, isAccepted = isAccepted,
isRequired = isRequired, isRequired = isRequired,
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}
}

View File

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

View File

@ -1,7 +1,7 @@
package fr.openium.consentium.api.model package fr.openium.consentium.api.model
data class Vendor( data class Vendor(
val identifier: VendorIdentifier, val identifier: String,
val isAccepted: Boolean, val isAccepted: Boolean,
val isRequired: Boolean, val isRequired: Boolean,
) )

View File

@ -1,6 +1,6 @@
package fr.openium.consentium.api.model package fr.openium.consentium.api.model
data class VendorChoice( data class VendorChoice(
val vendorIdentifier: VendorIdentifier, val vendorIdentifier: String,
val isAccepted: Boolean, val isAccepted: Boolean,
) )

View File

@ -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"
}
}

View File

@ -8,7 +8,7 @@ import dagger.Reusable
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.BuildConfig 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 fr.openium.consentium.data.remote.ConsentiumApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Cache import okhttp3.Cache

View File

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

View File

@ -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.ConsentiumApi
import fr.openium.consentium.data.remote.model.GetConsent import fr.openium.consentium.data.remote.model.GetConsent
@ -10,42 +10,30 @@ import java.util.UUID
internal object ConsentiumMockApi : ConsentiumApi { internal object ConsentiumMockApi : ConsentiumApi {
private val consents = GetConsent.GetConsentPayloadDTO( private val consents = GetConsent.GetConsentPayloadDTO(
id = UUID.randomUUID().toString(),
installationId = UUID.randomUUID().toString(), installationId = UUID.randomUUID().toString(),
purposes = listOf( purposes = listOf(
GetConsent.PurposeDTO( GetConsent.PurposeDTO(
identifier = "purpose-audience", identifier = "purpose-audience",
isRequired = true, vendors = null,
isAccepted = GetConsent.PurposeStatusDTO.ACCEPTED, choice = GetConsent.PurposeStatusDTO.ACCEPTED,
vendors = listOf(
GetConsent.VendorDTO(
identifier = "vendor-clarity",
isAccepted = true,
isRequired = true,
),
GetConsent.VendorDTO(
identifier = "vendor-matomo",
isAccepted = true,
isRequired = false,
)
)
), ),
GetConsent.PurposeDTO( GetConsent.PurposeDTO(
identifier = "purpose-required", identifier = "purpose-required",
isRequired = true, vendors = null,
isAccepted = GetConsent.PurposeStatusDTO.REJECTED, choice = GetConsent.PurposeStatusDTO.REJECTED,
vendors = null
), ),
), ),
isValid = true 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(500) delay(2000)
return Response.success(consents) return Response.success(consents)
} }
override suspend fun setConsents(applicationId: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response<Any> { override suspend fun setConsents(token: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response<Unit> {
delay(500) delay(2000)
return Response.success(Unit) return Response.success(Unit)
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
package fr.openium.consentium.domain.useCase
import javax.inject.Inject
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
interface GetAuthTokenUseCase {
suspend operator fun invoke(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"
}
}

Some files were not shown because too many files have changed in this diff Show More