Compare commits
19 Commits
5db3391b7d
...
develop/v1
Author | SHA1 | Date | |
---|---|---|---|
0f4a3254e4 | |||
3ffd190406 | |||
76d021d35d | |||
ed2d0ddb43 | |||
4276adaf22 | |||
ad08a4325f | |||
c94f8a4f05 | |||
9f9a66451f | |||
af91841857 | |||
c57c9b7d05 | |||
5303e594da | |||
18f9fff4a1 | |||
cc578e3117 | |||
75d179e046 | |||
63cfc39d1e | |||
bf5f7b47b7 | |||
338d9f624a | |||
1f40d2fc98 | |||
86a7020c2b |
15
Jenkinsfile
vendored
15
Jenkinsfile
vendored
@ -1,3 +1,16 @@
|
||||
library "openiumpipeline"
|
||||
|
||||
openiumDroidJob()
|
||||
openiumDroidJob modules: [
|
||||
"consentium": [
|
||||
unitTestTasks: ["testDevDebugUnitTest"],
|
||||
],
|
||||
"consentium-ui": [
|
||||
unitTestTasks: ["testDevDebugUnitTest"],
|
||||
],
|
||||
"app": [
|
||||
unitTestTasks: ["testDevDebugUnitTest"],
|
||||
testTasks: ["pixel5DevDebugAndroidTest"],
|
||||
publishApkVariants : ["devDebug", "devRelease", "demoRelease", "prodRelease"],
|
||||
],
|
||||
]
|
||||
publishChannel: '#int-consentium'
|
@ -1,3 +1,4 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
import org.gradle.language.nativeplatform.internal.BuildType
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
@ -6,11 +7,12 @@ plugins {
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.serialization)
|
||||
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("fr.openium.publish")
|
||||
}
|
||||
apply(from = "publish.build.gradle")
|
||||
|
||||
// Keystore
|
||||
val keystorePropertiesFile = rootProject.file("keys/keystore.properties")
|
||||
@ -25,8 +27,6 @@ android {
|
||||
applicationId = "fr.openium.consentium"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@ -49,6 +49,7 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
|
||||
versionNameSuffix = "-debug"
|
||||
applicationIdSuffix = ".debug"
|
||||
@ -73,6 +74,10 @@ android {
|
||||
dimension = "version"
|
||||
}
|
||||
|
||||
create("dev") {
|
||||
dimension = "version"
|
||||
}
|
||||
|
||||
create("demo") {
|
||||
dimension = "version"
|
||||
}
|
||||
@ -88,6 +93,19 @@ android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
managedDevices {
|
||||
devices {
|
||||
create<ManagedVirtualDevice>("pixel5") {
|
||||
device = "Pixel 5"
|
||||
apiLevel = 34
|
||||
systemImageSource = "google"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -106,6 +124,8 @@ dependencies {
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
|
||||
// Libs analytics
|
||||
implementation(libs.matomo)
|
||||
implementation(libs.clarity)
|
||||
implementation(libs.ga4)
|
||||
@ -128,5 +148,4 @@ dependencies {
|
||||
|
||||
// Kotlin serialization
|
||||
implementation(libs.kotlin.serialization)
|
||||
|
||||
}
|
6
app/publish.build.gradle
Normal file
6
app/publish.build.gradle
Normal file
@ -0,0 +1,6 @@
|
||||
android {
|
||||
defaultConfig {
|
||||
versionName publish.versionName
|
||||
versionCode publish.versionCode
|
||||
}
|
||||
}
|
@ -17,8 +17,6 @@ import org.junit.Assert.*
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("fr.openium.consentium", appContext.packageName)
|
||||
assertTrue(true)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package fr.openium.consentium
|
||||
|
||||
import fr.openium.consentium.ui.navigation.DemoNavGraph
|
||||
import DemoNavGraph
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@ -24,7 +24,7 @@ class MainActivity : ComponentActivity() {
|
||||
val navHostController = rememberNavController()
|
||||
|
||||
ConsentiumTheme {
|
||||
Scaffold( modifier = Modifier.fillMaxSize() ) { paddingValues ->
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -1,41 +1,82 @@
|
||||
package fr.openium.consentium.ui.navigation
|
||||
|
||||
import Destination
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import fr.openium.consentium.R
|
||||
import fr.openium.consentium.api.Consentium
|
||||
import fr.openium.consentium.ui.screens.main.MainScreen
|
||||
import fr.openium.consentium.ui.screens.splash.SplashScreen
|
||||
import fr.openium.consentium_ui.ui.components.ConsentiumComponent
|
||||
import fr.openium.consentium_ui.ui.model.ConsentiumPageUI
|
||||
|
||||
private const val NAV_ANIMATION_TIME = 500
|
||||
|
||||
private val SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION = slideIn(
|
||||
animationSpec = tween(NAV_ANIMATION_TIME),
|
||||
initialOffset = { fullSize -> IntOffset(x = fullSize.height, y = 0) }
|
||||
)
|
||||
|
||||
private val SLIDE_IN_FROM_LEFT_ENTER_TRANSITION = slideIn(
|
||||
animationSpec = tween(NAV_ANIMATION_TIME),
|
||||
initialOffset = { fullSize -> IntOffset(x = -fullSize.height, y = 0) }
|
||||
)
|
||||
|
||||
private val SLIDE_OUT_TO_LEFT_EXIT_TRANSITION = slideOut(
|
||||
animationSpec = tween(NAV_ANIMATION_TIME),
|
||||
targetOffset = { fullSize -> IntOffset(x = -fullSize.height, y = 0) }
|
||||
)
|
||||
|
||||
private val SLIDE_OUT_TO_RIGHT_EXIT_TRANSITION = slideOut(
|
||||
animationSpec = tween(NAV_ANIMATION_TIME),
|
||||
targetOffset = { fullSize -> IntOffset(x = fullSize.height, y = 0) }
|
||||
)
|
||||
|
||||
private infix fun String?.ifIn(destinations: List<Destination>): Boolean {
|
||||
return this in destinations.map { it::class.qualifiedName }
|
||||
}
|
||||
|
||||
private infix fun Boolean.thenTransitionWith(transition: EnterTransition): EnterTransition? {
|
||||
return if (this) transition else null
|
||||
}
|
||||
|
||||
private infix fun EnterTransition?.elseTransitionWith(transition: EnterTransition): EnterTransition {
|
||||
return this ?: transition
|
||||
}
|
||||
|
||||
private infix fun Boolean.thenTransitionWith(transition: ExitTransition): ExitTransition? {
|
||||
return if (this) transition else null
|
||||
}
|
||||
|
||||
private infix fun ExitTransition?.elseTransitionWith(transition: ExitTransition): ExitTransition {
|
||||
return this ?: transition
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DemoNavGraph(navHostController: NavHostController) {
|
||||
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = Destination.Splash,
|
||||
enterTransition = {
|
||||
slideIn(
|
||||
animationSpec = tween(NAV_ANIMATION_TIME),
|
||||
initialOffset = { fullSize -> IntOffset(x = fullSize.height, y = 0) }
|
||||
)
|
||||
},
|
||||
exitTransition = {
|
||||
slideOut(
|
||||
animationSpec = tween(NAV_ANIMATION_TIME),
|
||||
targetOffset = { fullSize -> IntOffset(x = -fullSize.height, y = 0) }
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
composable<Destination.Splash> {
|
||||
composable<Destination.Splash>(
|
||||
enterTransition = {
|
||||
SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION
|
||||
},
|
||||
exitTransition = {
|
||||
SLIDE_OUT_TO_LEFT_EXIT_TRANSITION
|
||||
}
|
||||
) {
|
||||
SplashScreen(
|
||||
navigateToMain = {
|
||||
navHostController.navigate(Destination.Main) {
|
||||
@ -43,23 +84,70 @@ fun DemoNavGraph(navHostController: NavHostController) {
|
||||
saveState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<Destination.Main> {
|
||||
MainScreen(
|
||||
onGoToConsentDetail = {
|
||||
navHostController.navigate(Destination.Consent)
|
||||
},
|
||||
onGoToConsentMaster = {
|
||||
navHostController.navigate(Destination.Consent)
|
||||
navigateToConsent = {
|
||||
navHostController.navigate(Destination.Consent(ConsentiumPageUI.GENERAL_CONSENT))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<Destination.Consent> {
|
||||
// TODO
|
||||
composable<Destination.Main>(
|
||||
enterTransition = {
|
||||
initialState.destination.route ifIn listOf(
|
||||
Destination.Splash
|
||||
) thenTransitionWith SLIDE_IN_FROM_RIGHT_ENTER_TRANSITION elseTransitionWith SLIDE_IN_FROM_LEFT_ENTER_TRANSITION
|
||||
},
|
||||
exitTransition = {
|
||||
targetState.destination.route ifIn listOf(
|
||||
Destination.Splash
|
||||
) thenTransitionWith SLIDE_OUT_TO_RIGHT_EXIT_TRANSITION elseTransitionWith SLIDE_OUT_TO_LEFT_EXIT_TRANSITION
|
||||
}
|
||||
) {
|
||||
MainScreen(
|
||||
onGoToConsentMaster = {
|
||||
navHostController.navigate(Destination.Consent(ConsentiumPageUI.GENERAL_CONSENT))
|
||||
},
|
||||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import fr.openium.consentium_ui.ui.model.ConsentiumPageUI
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@ -10,6 +11,8 @@ sealed interface Destination {
|
||||
data object Main : Destination
|
||||
|
||||
@Serializable
|
||||
data object Consent : Destination
|
||||
data class Consent(
|
||||
val landingPage: ConsentiumPageUI
|
||||
): Destination
|
||||
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
package fr.openium.consentium.ui.screens.consent
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun ConsentScreen() {
|
||||
|
||||
|
||||
|
||||
}
|
@ -18,7 +18,6 @@ fun MainScreen(
|
||||
onGoToConsentDetail: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
// View
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
@ -43,7 +42,5 @@ fun MainScreen(
|
||||
text = "Go to Consent Detail"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -10,36 +10,74 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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
|
||||
fun SplashScreen(
|
||||
navigateToMain: () -> Unit,
|
||||
viewModel: SplashScreenViewModel = hiltViewModel(),
|
||||
navigateToConsent: () -> Unit,
|
||||
) {
|
||||
// State
|
||||
val currentState by viewModel.state.collectAsState()
|
||||
// Property
|
||||
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
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.initMain()
|
||||
}
|
||||
consentium.fetchConsentState.collect { consentState ->
|
||||
when (consentState) {
|
||||
FetchConsentiumState.Idle,
|
||||
FetchConsentiumState.Loading,
|
||||
-> {
|
||||
}
|
||||
|
||||
LaunchedEffect(currentState) {
|
||||
when (val state = currentState) {
|
||||
is SplashScreenViewModel.State.Loaded -> {
|
||||
if (state.isSplashEnded) {
|
||||
FetchConsentiumState.Error -> {
|
||||
// Handle error
|
||||
consentium.fetchConsents()
|
||||
}
|
||||
|
||||
is FetchConsentiumState.Invalid -> {
|
||||
navigateToConsent()
|
||||
}
|
||||
|
||||
is FetchConsentiumState.Valid -> {
|
||||
// The tracking services should be initialized here
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
consentium.fetchConsents()
|
||||
}
|
||||
|
||||
// View
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@ -47,7 +85,7 @@ fun SplashScreen(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
Text("Splash Screen")
|
||||
Text("Splash Screen") // TODO
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -2,10 +2,21 @@ package fr.openium.consentium.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
val Primary = Color(0xFF70ACDC)
|
||||
val OnPrimary = Color(0XFFFFFFFF)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val Secondary = Color(0xFFF29413)
|
||||
val OnSecondary = Color(0xFFFFFFFF)
|
||||
|
||||
val Tertiary = Color(0xFF3470A0)
|
||||
|
||||
val OnSurfaceVariant = Color(0xFF3470A0)
|
||||
val OnSurface = Color(0xFF163752)
|
||||
|
||||
val Error = Color(0xFF3470A0)
|
||||
|
||||
val SurfaceHighest = Color(0xFFFFFFFF)
|
||||
val SurfaceHigh = Color(0xFFF2F8FC)
|
||||
val SurfaceMiddle = Color(0xFFD6E6F5)
|
||||
|
||||
val Success = Color(0xFF479B3F)
|
@ -1,39 +1,53 @@
|
||||
package fr.openium.consentium.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
primary = Primary,
|
||||
onPrimary = OnPrimary,
|
||||
|
||||
secondary = Secondary,
|
||||
onSecondary = OnSecondary,
|
||||
|
||||
tertiary = Tertiary,
|
||||
|
||||
onSurfaceVariant = OnSurfaceVariant,
|
||||
onSurface = OnSurface,
|
||||
|
||||
error = Error,
|
||||
surfaceContainerHighest = SurfaceHighest,
|
||||
surfaceContainerHigh = SurfaceHigh,
|
||||
surfaceContainer = SurfaceMiddle,
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
primary = Primary,
|
||||
onPrimary = OnPrimary,
|
||||
|
||||
secondary = Secondary,
|
||||
onSecondary = OnSecondary,
|
||||
|
||||
tertiary = Tertiary,
|
||||
|
||||
onSurfaceVariant = OnSurfaceVariant,
|
||||
onSurface = OnSurface,
|
||||
|
||||
error = Error,
|
||||
surfaceContainerHighest = SurfaceHighest,
|
||||
surfaceContainerHigh = SurfaceHigh,
|
||||
surfaceContainer = SurfaceMiddle,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ConsentiumTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">Consentium</string>
|
||||
<string name="app_id" translatable="false">01938ce4-331a-7592-9e90-f09201ff4f36</string>
|
||||
<string name="api_key" translatable="false">c452a27f-2e90-427d-be82-2f631c31dd09</string>
|
||||
</resources>
|
3
app/version.properties
Normal file
3
app/version.properties
Normal file
@ -0,0 +1,3 @@
|
||||
#Wed Sep 27 10:07:57 CEST 2023
|
||||
VERSION_NAME=1.0.0
|
||||
VERSION_CODE=1
|
@ -20,11 +20,13 @@ plugins {
|
||||
|
||||
// Kotlin serialization
|
||||
alias(libs.plugins.serialization) apply false
|
||||
}
|
||||
|
||||
// Firebase crashlytics
|
||||
alias(libs.plugins.firebaseCrashlytics) apply false
|
||||
|
||||
// Google services
|
||||
alias(libs.plugins.googleServices) apply false
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url = uri("https://maven.openium.fr/") }
|
||||
}
|
||||
dependencies {
|
||||
classpath(libs.openium.publish)
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ plugins {
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
@ -17,9 +19,26 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
debug {
|
||||
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"
|
||||
}
|
||||
|
||||
create("dev") {
|
||||
dimension = "version"
|
||||
}
|
||||
|
||||
create("demo") {
|
||||
dimension = "version"
|
||||
}
|
||||
@ -45,6 +68,10 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -54,6 +81,20 @@ dependencies {
|
||||
// AndroidX
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.runtime.android)
|
||||
implementation(libs.androidx.foundation.android)
|
||||
implementation(libs.androidx.ui.android)
|
||||
implementation(libs.androidx.foundation.layout.android)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
|
||||
//Compose
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui.graphics)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.material3)
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlin.serialization)
|
||||
@ -65,11 +106,19 @@ dependencies {
|
||||
|
||||
// Hilt
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
// Timber
|
||||
implementation(libs.timber)
|
||||
|
||||
// Coil
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.network)
|
||||
|
||||
// Rich text formatting
|
||||
implementation(libs.rich.text)
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.test.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
54
consentium-ui/proguard-rules.pro
vendored
54
consentium-ui/proguard-rules.pro
vendored
@ -18,4 +18,56 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontwarn fr.openium.consentium.data.di.ConsentiumUrl
|
||||
-dontwarn fr.openium.consentium.data.di.OkHttpClientDefault
|
||||
-dontwarn java.lang.invoke.StringConcatFactory
|
||||
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep public class * extends java.lang.Exception
|
||||
|
||||
-keep class com.google.firebase.crashlytics.** { *; }
|
||||
-dontwarn com.google.firebase.crashlytics.**
|
||||
|
||||
# Keep enums
|
||||
-keep public enum fr.openium.consentium.**{
|
||||
*;
|
||||
}
|
||||
|
||||
# Garder les annotations de Kotlin
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Garder les classes et les membres annotés avec @Keep
|
||||
-keep @androidx.annotation.Keep class * { *; }
|
||||
-keepclassmembers class ** {
|
||||
@androidx.annotation.Keep *;
|
||||
}
|
||||
|
||||
# Garder les classes et les membres annotés avec @Serializable
|
||||
-keep @kotlinx.serialization.Serializable class * { *; }
|
||||
-keepclassmembers class ** {
|
||||
@kotlinx.serialization.Serializable *;
|
||||
}
|
||||
|
||||
# Garder les classes annotées avec @HiltAndroidApp
|
||||
-keep @dagger.hilt.android.HiltAndroidApp class * { *; }
|
||||
|
||||
# Garder les classes et les membres pour Timber
|
||||
-keep class timber.log.Timber { *; }
|
||||
-keep interface timber.log.Timber$Tree { *; }
|
||||
|
||||
-keep public enum fr.openium.consentium_ui.ui.model.ConsentiumPageUI
|
||||
-keepclassmembers enum fr.openium.consentium_ui.ui.model.ConsentiumPageUI {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
-keep class fr.openium.consentium_ui.data.** { *; }
|
||||
-keep class fr.openium.consentium_ui.domain.repository.** { *; }
|
||||
-keep class fr.openium.consentium_ui.domain.usecase.** { *; }
|
||||
-keep class fr.openium.consentium_ui.ui.** { *; }
|
||||
-keepclassmembers class fr.openium.consentium_ui.data.** { *; }
|
||||
-keepclassmembers class fr.openium.consentium_ui.domain.repository.** { *; }
|
||||
-keepclassmembers class fr.openium.consentium_ui.domain.usecase.** { *; }
|
||||
-keepclassmembers class fr.openium.consentium_ui.ui.** { *; }
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
package fr.openium.consentium_ui
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@ -17,8 +14,6 @@ import org.junit.Assert.*
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("fr.openium.consentium_ui.test", appContext.packageName)
|
||||
assertTrue(true)
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
@ -9,8 +9,8 @@ import dagger.hilt.components.SingletonComponent
|
||||
import fr.openium.consentium.data.di.ConsentiumUrl
|
||||
import fr.openium.consentium.data.di.OkHttpClientDefault
|
||||
import fr.openium.consentium_ui.BuildConfig
|
||||
import fr.openium.consentium_ui.api.mock.ConsentiumUIMockApi
|
||||
import fr.openium.consentium_ui.data.remote.ConsentiumUIApi
|
||||
import fr.openium.consentium_ui.data.remote.mock.ConsentiumUIMockApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
|
@ -3,13 +3,15 @@ package fr.openium.consentium_ui.data.remote
|
||||
import fr.openium.consentium_ui.data.remote.model.GetConsentConfigDTO
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Path
|
||||
|
||||
internal interface ConsentiumUIApi {
|
||||
|
||||
@GET("consent-config")
|
||||
@GET("applications/{application}")
|
||||
suspend fun getConsentConfig(
|
||||
applicationID: String,
|
||||
installationID: String,
|
||||
@Header("Authorization") token: String,
|
||||
@Path("application") applicationId: String,
|
||||
): Response<GetConsentConfigDTO>
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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 l’application] utilise des cookies pour différents objectifs : faire fonctionner l’application, améliorer nos services en mesurant l’efficacité de nos contenus et afficher des publicités susceptibles de vous intéresser.<br></p>\n<p>En cliquant sur “Accepter et fermer”, vous acceptez cette utilisation sur l’application mobile. Vous pouvez également paramétrer vos choix en cliquant sur “Paramétrer mes choix” ou refuser ces cookies en cliquant sur “Continuer sans accepter”. Vous pouvez changer d’avis à tout moment depuis les paramètres de votre compte via l’onglet “Notifications et cookies”</p>\n",
|
||||
durationText = "<p>Nous conservons votre choix pendant 12 mois. Vous pouvez changer d’avis à tout moment depuis les paramètres de votre compte via l’onglet “Notification et cookies” tetetettetettetetstts</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 l’application. Ils permettent de vérifier la stabilité technique de l’application et de mesurer notre audience. Ces données ne sont utilisées que pour notre compte exclusif (en ne produisant que des données statistiques anonymes).</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 l’efficacité 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 l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.</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 l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.</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 l’efficacité de nos contenus. Ils permettent de mesurer l’audience de l’application et de comprendre comment les utilisateurs interagissent avec l’application.</p>",
|
||||
name = "Social"
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
override suspend fun getConsentConfig(
|
||||
token: String,
|
||||
applicationId: String,
|
||||
): Response<GetConsentConfigDTO> {
|
||||
return Response.success(consents)
|
||||
}
|
||||
|
||||
}
|
@ -7,10 +7,8 @@ import kotlinx.serialization.Serializable
|
||||
internal data class GetConsentConfigDTO(
|
||||
@SerialName("id") val installationId: String,
|
||||
@SerialName("name") val appName: String,
|
||||
@SerialName("icon") val icon: String,
|
||||
@SerialName("primaryColor") val primaryColor: String,
|
||||
@SerialName("secondaryColor") val secondaryColor: String,
|
||||
@SerialName("textColor") val textColor: String,
|
||||
@SerialName("translation") val consentMainTextTranslation: List<MainConsentTextTranslationDTO>,
|
||||
@SerialName("icon") val icon: String? = null,
|
||||
@SerialName("consentPageUrl") val consentPageUrl: String,
|
||||
@SerialName("translations") val consentMainTextTranslation: List<MainConsentTextTranslationDTO>,
|
||||
@SerialName("purposes") val purposes: List<PurposeDTO>,
|
||||
)
|
@ -7,7 +7,8 @@ import kotlinx.serialization.Serializable
|
||||
internal data class MainConsentTextTranslationDTO(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("lang") val language: String,
|
||||
@SerialName("consentPageUrl") val consentPageUrl: String,
|
||||
@SerialName("mainConsentText") val mainConsentText: String,
|
||||
@SerialName("durationText") val durationText: String,
|
||||
@SerialName("consentPageUrl") val consentPageUrl: String,
|
||||
|
||||
)
|
@ -5,10 +5,10 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class PurposeDTO(
|
||||
@SerialName("identifier") val id: String,
|
||||
@SerialName("order") val order: Int,
|
||||
@SerialName("sortOrder") val order: Int,
|
||||
@SerialName("isRequired") val isRequired: Boolean,
|
||||
@SerialName("isAccepted") val isAccepted: Boolean,
|
||||
@SerialName("choice") val choice: PurposeStatusDTO,
|
||||
@SerialName("translations") val translations: List<PurposeTranslationDTO>,
|
||||
@SerialName("vendors") val vendors: List<VendorDTO>,
|
||||
)
|
||||
@SerialName("identifier") val id: String,
|
||||
|
||||
)
|
||||
|
@ -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,
|
||||
}
|
@ -8,4 +8,5 @@ internal data class PurposeTranslationDTO(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("lang") val language: String,
|
||||
@SerialName("text") val text: String,
|
||||
@SerialName("name") val name: String,
|
||||
)
|
@ -7,9 +7,9 @@ internal fun PurposeDTO.toPurposeData() =
|
||||
PurposeData(
|
||||
identifier = id,
|
||||
isRequired = isRequired,
|
||||
isAccepted = isAccepted,
|
||||
choice = choice.toPurposeStatusData(),
|
||||
order = order,
|
||||
vendors = vendors.toVendorDataList(),
|
||||
vendors = emptyList(),
|
||||
translations = translations.toPurposeTranslationDataList(),
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ internal fun PurposeTranslationDTO.toPurposeTranslationData() =
|
||||
id = id,
|
||||
language = language,
|
||||
text = text,
|
||||
name = name,
|
||||
)
|
||||
|
||||
internal fun List<PurposeTranslationDTO>.toPurposeTranslationDataList() = map { it.toPurposeTranslationData() }
|
@ -4,6 +4,8 @@ import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import fr.openium.consentium_ui.domain.usecase.GetApplicationLanguageUseCase
|
||||
import fr.openium.consentium_ui.domain.usecase.GetApplicationLanguageUseCaseImpl
|
||||
import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCase
|
||||
import fr.openium.consentium_ui.domain.usecase.GetConfigTextForLanguageUseCaseImpl
|
||||
|
||||
@ -16,4 +18,9 @@ internal interface ConsentiumUseCaseModule {
|
||||
getConfigTextForLanguageUseCaseImpl: GetConfigTextForLanguageUseCaseImpl,
|
||||
): GetConfigTextForLanguageUseCase
|
||||
|
||||
@Binds
|
||||
fun bindGetApplicationLangageUseCase(
|
||||
getApplicationLangageUseCaseImpl: GetApplicationLanguageUseCaseImpl,
|
||||
): GetApplicationLanguageUseCase
|
||||
|
||||
}
|
@ -2,7 +2,7 @@ package fr.openium.consentium_ui.domain.model
|
||||
|
||||
internal data class ContentConfigData(
|
||||
val applicationName: String,
|
||||
val iconUrl : String,
|
||||
val iconUrl : String?,
|
||||
val mainTextTranslation: List<MainConsentTextTranslationData>,
|
||||
val purposes: List<PurposeData>
|
||||
)
|
||||
|
@ -4,7 +4,7 @@ internal data class PurposeData(
|
||||
val identifier: String,
|
||||
val order: Int,
|
||||
val isRequired: Boolean,
|
||||
val isAccepted: Boolean,
|
||||
val choice: PurposeStatusData,
|
||||
val translations: List<PurposeTranslationData>,
|
||||
val vendors: List<VendorData>,
|
||||
)
|
||||
|
@ -0,0 +1,7 @@
|
||||
package fr.openium.consentium_ui.domain.model
|
||||
|
||||
internal enum class PurposeStatusData {
|
||||
ACCEPTED,
|
||||
REJECTED,
|
||||
PARTIAL,
|
||||
}
|
@ -4,4 +4,5 @@ internal data class PurposeTranslationData(
|
||||
val id: String,
|
||||
val language: String,
|
||||
val text: String,
|
||||
val name: String,
|
||||
)
|
@ -1,23 +1,23 @@
|
||||
package fr.openium.consentium_ui.domain.repository
|
||||
|
||||
import fr.openium.consentium.domain.useCase.GetConsentiumUniqueInstallationIdUseCase
|
||||
import fr.openium.consentium.domain.useCase.GetAuthTokenUseCase
|
||||
import fr.openium.consentium_ui.data.remote.ConsentiumUIApi
|
||||
import fr.openium.consentium_ui.domain.adapter.toConsentConfigData
|
||||
import fr.openium.consentium_ui.domain.model.ContentConfigData
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ConsentiumRepository @Inject constructor(
|
||||
private val getConsentiumUniqueInstallationIdUseCase: GetConsentiumUniqueInstallationIdUseCase,
|
||||
private val consentiumUIApi: ConsentiumUIApi,
|
||||
private val getAuthTokenUseCase: GetAuthTokenUseCase,
|
||||
) {
|
||||
|
||||
suspend fun getConsentiumConfig(
|
||||
applicationId: String,
|
||||
apiKey: String,
|
||||
): ConsentiumUIRepositoryResponse {
|
||||
val installationId = getConsentiumUniqueInstallationIdUseCase.invoke()
|
||||
val consentsResponse = consentiumUIApi.getConsentConfig(applicationId, installationId)
|
||||
|
||||
return try {
|
||||
val authToken = getAuthTokenUseCase(apiKey)
|
||||
val consentsResponse = consentiumUIApi.getConsentConfig(authToken, applicationId)
|
||||
val consentsBody = if (consentsResponse.isSuccessful) {
|
||||
consentsResponse.body() ?: throw Exception()
|
||||
} else {
|
||||
@ -26,10 +26,10 @@ internal class ConsentiumRepository @Inject constructor(
|
||||
|
||||
ConsentiumUIRepositoryResponse.Success(consentsBody.toConsentConfigData())
|
||||
} catch (e: Exception) {
|
||||
Timber.d("$e")
|
||||
ConsentiumUIRepositoryResponse.Error
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal interface ConsentiumUIRepositoryResponse {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -6,39 +6,42 @@ import javax.inject.Inject
|
||||
private const val FALLBACK_LANGUAGE = "en"
|
||||
|
||||
internal interface GetConfigTextForLanguageUseCase {
|
||||
suspend fun invoke(
|
||||
suspend operator fun invoke(
|
||||
language: String = FALLBACK_LANGUAGE,
|
||||
configData: ContentConfigData,
|
||||
): GetConfigTextForLanguageUseCaseResponce
|
||||
): GetConfigTextForLanguageUseCaseResponse
|
||||
}
|
||||
|
||||
internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetConfigTextForLanguageUseCase {
|
||||
internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() :
|
||||
GetConfigTextForLanguageUseCase {
|
||||
|
||||
override suspend fun invoke(
|
||||
language: String,
|
||||
configData: ContentConfigData,
|
||||
): GetConfigTextForLanguageUseCaseResponce {
|
||||
): GetConfigTextForLanguageUseCaseResponse {
|
||||
|
||||
return try {
|
||||
|
||||
val canIUseTheTranslation = configData.mainTextTranslation.any { it.language == language } &&
|
||||
configData.purposes.all { purposeData ->
|
||||
purposeData.translations.any { it.language == language } &&
|
||||
purposeData.vendors.all { vendorData ->
|
||||
vendorData.translations.any { it.language == language }
|
||||
}
|
||||
}
|
||||
val canIUseTheTranslation =
|
||||
configData.mainTextTranslation.any { it.language == language } &&
|
||||
configData.purposes.all { purposeData ->
|
||||
purposeData.translations.any { it.language == language } &&
|
||||
purposeData.vendors.all { vendorData ->
|
||||
vendorData.translations.any { it.language == language }
|
||||
}
|
||||
}
|
||||
|
||||
val languageToUse = if (canIUseTheTranslation) {
|
||||
language
|
||||
} else {
|
||||
val isThereAGoodFallbackLanguage = configData.mainTextTranslation.any { it.language == FALLBACK_LANGUAGE } &&
|
||||
configData.purposes.all { purposeData ->
|
||||
purposeData.translations.any { it.language == FALLBACK_LANGUAGE } &&
|
||||
purposeData.vendors.all { vendorData ->
|
||||
vendorData.translations.any { it.language == FALLBACK_LANGUAGE }
|
||||
}
|
||||
}
|
||||
val isThereAGoodFallbackLanguage =
|
||||
configData.mainTextTranslation.any { it.language == FALLBACK_LANGUAGE } &&
|
||||
configData.purposes.all { purposeData ->
|
||||
purposeData.translations.any { it.language == FALLBACK_LANGUAGE } &&
|
||||
purposeData.vendors.all { vendorData ->
|
||||
vendorData.translations.any { it.language == FALLBACK_LANGUAGE }
|
||||
}
|
||||
}
|
||||
if (isThereAGoodFallbackLanguage) {
|
||||
FALLBACK_LANGUAGE
|
||||
} else {
|
||||
@ -61,23 +64,24 @@ internal class GetConfigTextForLanguageUseCaseImpl @Inject constructor() : GetCo
|
||||
)
|
||||
|
||||
if (languageToUse == FALLBACK_LANGUAGE) {
|
||||
GetConfigTextForLanguageUseCaseResponce.DefaultLanguage(filteredConfigData)
|
||||
GetConfigTextForLanguageUseCaseResponse.DefaultLanguage(filteredConfigData)
|
||||
} else {
|
||||
GetConfigTextForLanguageUseCaseResponce.Success(filteredConfigData)
|
||||
GetConfigTextForLanguageUseCaseResponse.Success(filteredConfigData)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
GetConfigTextForLanguageUseCaseResponce.Error
|
||||
GetConfigTextForLanguageUseCaseResponse.Error
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal interface GetConfigTextForLanguageUseCaseResponce {
|
||||
internal interface GetConfigTextForLanguageUseCaseResponse {
|
||||
|
||||
data object Error : GetConfigTextForLanguageUseCaseResponce
|
||||
data object Error : GetConfigTextForLanguageUseCaseResponse
|
||||
|
||||
data class Success(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponce
|
||||
data class Success(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponse
|
||||
|
||||
data class DefaultLanguage(val configData: ContentConfigData) : GetConfigTextForLanguageUseCaseResponce
|
||||
data class DefaultLanguage(val configData: ContentConfigData) :
|
||||
GetConfigTextForLanguageUseCaseResponse
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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,
|
||||
)
|
@ -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
|
||||
}
|
@ -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(),
|
||||
)
|
@ -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() }
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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 l’application] utilise des cookies pour différents objectifs : faire fonctionner l’application, améliorer nos services en mesurant l’efficacité de nos contenus et afficher des publicités susceptibles de vous intéresser. \n\n En cliquant sur “Accepter et fermer”, vous acceptez cette utilisation sur l’application mobile. Vous pouvez également paramétrer vos choix en cliquant sur “Paramétrer mes choix” ou refuser ces cookies en cliquant sur “Continuer sans accepter”. Vous pouvez changer d’avis à tout moment depuis les paramètres de votre compte via l’onglet “Notifications et cookies”.",
|
||||
applicationName = "Application name",
|
||||
consentPageUrl = "https://www.google.com"
|
||||
),
|
||||
onClickCookiesPolicies = {},
|
||||
onDenyAndClose = {},
|
||||
onAcceptAndClose = {},
|
||||
onNavigateToDetails = {},
|
||||
loadingElement = LoadingElement.LINK,
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
@ -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(),
|
||||
)
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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 = {}
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package fr.openium.consentium_ui.ui.model
|
||||
|
||||
enum class ConsentiumPageUI {
|
||||
GENERAL_CONSENT,
|
||||
DETAILS_CONSENT,
|
||||
COOKIES_POLICY,
|
||||
}
|
@ -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>,
|
||||
)
|
@ -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,
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package fr.openium.consentium_ui.ui.model
|
||||
|
||||
internal enum class LoadingElement {
|
||||
BUTTON,
|
||||
LINK,
|
||||
NONE,
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package fr.openium.consentium_ui.ui.model
|
||||
|
||||
internal enum class PurposeStatusUI {
|
||||
ACCEPTED,
|
||||
REJECTED,
|
||||
PARTIAL,
|
||||
}
|
@ -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>,
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package fr.openium.consentium_ui.ui.model
|
||||
|
||||
internal data class VendorUI(
|
||||
val id: String,
|
||||
var isAccepted: Boolean,
|
||||
)
|
@ -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
|
||||
|
||||
}
|
@ -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
|
||||
}
|
14
consentium-ui/src/main/res/values-fr/strings.xml
Normal file
14
consentium-ui/src/main/res/values-fr/strings.xml
Normal 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>
|
14
consentium-ui/src/main/res/values/strings.xml
Normal file
14
consentium-ui/src/main/res/values/strings.xml
Normal 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>
|
@ -20,11 +20,14 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +37,10 @@ android {
|
||||
dimension = "version"
|
||||
}
|
||||
|
||||
create("dev") {
|
||||
dimension = "version"
|
||||
}
|
||||
|
||||
create("demo") {
|
||||
dimension = "version"
|
||||
}
|
||||
|
40
consentium/proguard-rules.pro
vendored
40
consentium/proguard-rules.pro
vendored
@ -18,4 +18,42 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontwarn java.lang.invoke.StringConcatFactory
|
||||
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep public class * extends java.lang.Exception
|
||||
|
||||
-keep class com.google.firebase.crashlytics.** { *; }
|
||||
-dontwarn com.google.firebase.crashlytics.**
|
||||
|
||||
# Keep enums
|
||||
-keep public enum fr.openium.consentium.**{
|
||||
*;
|
||||
}
|
||||
|
||||
# Garder les annotations de Kotlin
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Garder les classes et les membres annotés avec @Keep
|
||||
-keep @androidx.annotation.Keep class * { *; }
|
||||
-keepclassmembers class ** {
|
||||
@androidx.annotation.Keep *;
|
||||
}
|
||||
|
||||
# Garder les classes et les membres annotés avec @Serializable
|
||||
-keep @kotlinx.serialization.Serializable class * { *; }
|
||||
-keepclassmembers class ** {
|
||||
@kotlinx.serialization.Serializable *;
|
||||
}
|
||||
|
||||
# Garder les classes annotées avec @HiltAndroidApp
|
||||
-keep @dagger.hilt.android.HiltAndroidApp class * { *; }
|
||||
|
||||
# Garder les classes et les membres pour Timber
|
||||
-keep class timber.log.Timber { *; }
|
||||
-keep interface timber.log.Timber$Tree { *; }
|
||||
|
||||
-keep class fr.openium.consentium.** { *; }
|
||||
-keepclassmembers class fr.openium.consentium.** { *; }
|
||||
|
@ -1,13 +1,10 @@
|
||||
package fr.openium.consentium
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@ -17,8 +14,6 @@ import org.junit.Assert.*
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("fr.openium.consentium.test", appContext.packageName)
|
||||
assertTrue(true)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -16,7 +16,7 @@ import timber.log.Timber
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class ConsentiumDebugNetworkModule {
|
||||
class DevNetworkModule {
|
||||
|
||||
@Provides
|
||||
fun provideOkHttpBuilder(
|
4
consentium/src/dev/res/values/strings.xml
Normal file
4
consentium/src/dev/res/values/strings.xml
Normal 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>
|
@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
</manifest>
|
@ -2,11 +2,10 @@ package fr.openium.consentium.api
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import fr.openium.consentium.api.model.ConsentState
|
||||
import fr.openium.consentium.api.model.Purpose
|
||||
import fr.openium.consentium.api.model.PurposeChoice
|
||||
import fr.openium.consentium.api.model.PurposeIdentifier
|
||||
import fr.openium.consentium.api.model.PurposeStatus
|
||||
import fr.openium.consentium.api.model.VendorIdentifier
|
||||
import fr.openium.consentium.api.model.VendorStatus
|
||||
import fr.openium.consentium.api.state.FetchConsentiumState
|
||||
import fr.openium.consentium.api.state.SetConsentiumState
|
||||
import fr.openium.consentium.domain.di.RepositoryEntryPoint
|
||||
@ -18,13 +17,14 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class Consentium(
|
||||
context: Context,
|
||||
val applicationId: String,
|
||||
val apiKey: String,
|
||||
val appId: String,
|
||||
) {
|
||||
private val _fetchConsentState = MutableStateFlow<FetchConsentiumState>(FetchConsentiumState.Idle)
|
||||
val fetchConsentState by lazy { _fetchConsentState.asStateFlow() }
|
||||
|
||||
private val _setConsentState = MutableStateFlow<SetConsentiumState>(SetConsentiumState.Idle)
|
||||
val setConsentState by lazy { _setConsentState.asStateFlow() }
|
||||
private val _saveConsentState = MutableStateFlow<SetConsentiumState>(SetConsentiumState.Idle)
|
||||
val saveConsentState by lazy { _saveConsentState.asStateFlow() }
|
||||
|
||||
private val consentiumRepository: ConsentiumRepository by lazy {
|
||||
val appContext = context.applicationContext
|
||||
@ -32,42 +32,14 @@ class Consentium(
|
||||
entryPoint.provideConsentiumRepository()
|
||||
}
|
||||
|
||||
fun getConsentStatus(purpose: PurposeIdentifier): PurposeStatus {
|
||||
return when (val state = fetchConsentState.value) {
|
||||
is FetchConsentiumState.Valid -> {
|
||||
val purposeChoice = state.purposes.find { it.identifier == purpose }
|
||||
purposeChoice?.isAccepted ?: PurposeStatus.UNKNOWN
|
||||
}
|
||||
|
||||
else -> PurposeStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun getConsentStatus(purpose: PurposeIdentifier, vendorIdentifier: VendorIdentifier): VendorStatus {
|
||||
return when (val state = fetchConsentState.value) {
|
||||
is FetchConsentiumState.Valid -> {
|
||||
val vendorChoice = state.purposes.find { it.identifier == purpose }
|
||||
?.vendors?.find { it.identifier == vendorIdentifier }
|
||||
|
||||
when (vendorChoice?.isAccepted) {
|
||||
true -> VendorStatus.ACCEPTED
|
||||
false -> VendorStatus.REJECTED
|
||||
else -> VendorStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
else -> VendorStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchConsents() {
|
||||
_fetchConsentState.value = FetchConsentiumState.Loading
|
||||
try {
|
||||
when (val consentResponse = consentiumRepository.getConsents(applicationId)) {
|
||||
when (val consentResponse = consentiumRepository.getConsents(apiKey)) {
|
||||
ConsentiumRepositoryGetResponse.Error -> _fetchConsentState.value = FetchConsentiumState.Error
|
||||
|
||||
is ConsentiumRepositoryGetResponse.GetConsentsSuccess -> {
|
||||
val areConsentsValid = consentResponse.isValid
|
||||
val areConsentsValid = consentResponse.state == ConsentState.VALID
|
||||
if (areConsentsValid) {
|
||||
_fetchConsentState.value = FetchConsentiumState.Valid(purposes = consentResponse.purposes)
|
||||
} else {
|
||||
@ -75,24 +47,35 @@ class Consentium(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
_fetchConsentState.value = FetchConsentiumState.Error
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setConsents(
|
||||
suspend fun saveConsents(
|
||||
consent: List<PurposeChoice>,
|
||||
) {
|
||||
_setConsentState.value = SetConsentiumState.Loading
|
||||
_saveConsentState.emit(SetConsentiumState.Loading)
|
||||
try {
|
||||
when (consentiumRepository.setConsents(applicationId, consent)) {
|
||||
ConsentiumRepositorySetResponse.Error -> _setConsentState.value = SetConsentiumState.Error
|
||||
ConsentiumRepositorySetResponse.SetConsentsSuccess -> _setConsentState.value = SetConsentiumState.Success
|
||||
}
|
||||
when (consentiumRepository.setConsents(apiKey, consent)) {
|
||||
ConsentiumRepositorySetResponse.Error -> {
|
||||
_saveConsentState.emit(SetConsentiumState.Error)
|
||||
}
|
||||
|
||||
ConsentiumRepositorySetResponse.SetConsentsSuccess -> {
|
||||
_saveConsentState.emit(SetConsentiumState.Success)
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
@ -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
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
package fr.openium.consentium.api.adapter
|
||||
|
||||
import fr.openium.consentium.api.model.Purpose
|
||||
import fr.openium.consentium.api.model.PurposeIdentifier
|
||||
import fr.openium.consentium.data.remote.model.GetConsent
|
||||
|
||||
internal fun GetConsent.PurposeDTO.toPurpose() = Purpose(
|
||||
identifier = PurposeIdentifier.fromString(identifier),
|
||||
isRequired = isRequired,
|
||||
isAccepted = isAccepted.toPurposeStatus(),
|
||||
identifier = identifier,
|
||||
choice = choice.toPurposeStatus(),
|
||||
vendors = vendors?.map { it.toVendor() } ?: emptyList(),
|
||||
)
|
||||
|
@ -4,7 +4,6 @@ import fr.openium.consentium.api.model.PurposeChoice
|
||||
import fr.openium.consentium.data.remote.model.PatchConsent
|
||||
|
||||
internal fun PurposeChoice.toPatchConsentPurposeDTO() = PatchConsent.PurposeDTO(
|
||||
identifier = purposeIdentifier.toString(),
|
||||
isAccepted = isAccepted,
|
||||
vendors = vendors.map { it.toPatchConsentVendorDTO() }
|
||||
identifier = purposeIdentifier,
|
||||
choice = choice.toPurposeStatusDTO(),
|
||||
)
|
@ -2,9 +2,16 @@ package fr.openium.consentium.api.adapter
|
||||
|
||||
import fr.openium.consentium.api.model.PurposeStatus
|
||||
import fr.openium.consentium.data.remote.model.GetConsent
|
||||
import fr.openium.consentium.data.remote.model.PatchConsent
|
||||
|
||||
internal fun GetConsent.PurposeStatusDTO.toPurposeStatus() = when (this) {
|
||||
GetConsent.PurposeStatusDTO.ACCEPTED -> PurposeStatus.ACCEPTED
|
||||
GetConsent.PurposeStatusDTO.REJECTED -> PurposeStatus.REJECTED
|
||||
GetConsent.PurposeStatusDTO.NOT_DEFINED -> PurposeStatus.NOT_DEFINED
|
||||
GetConsent.PurposeStatusDTO.PARTIAL -> PurposeStatus.PARTIAL
|
||||
}
|
||||
|
||||
internal fun PurposeStatus.toPurposeStatusDTO() = when (this) {
|
||||
PurposeStatus.ACCEPTED -> PatchConsent.PurposeStatusDTO.ACCEPTED
|
||||
PurposeStatus.REJECTED -> PatchConsent.PurposeStatusDTO.REJECTED
|
||||
PurposeStatus.PARTIAL -> PatchConsent.PurposeStatusDTO.PARTIAL
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package fr.openium.consentium.api.adapter
|
||||
|
||||
import fr.openium.consentium.api.model.Vendor
|
||||
import fr.openium.consentium.api.model.VendorIdentifier
|
||||
import fr.openium.consentium.data.remote.model.GetConsent
|
||||
|
||||
internal fun GetConsent.VendorDTO.toVendor() = Vendor(
|
||||
identifier = VendorIdentifier.fromString(identifier),
|
||||
identifier = identifier,
|
||||
isAccepted = isAccepted,
|
||||
isRequired = isRequired,
|
||||
)
|
@ -4,6 +4,6 @@ import fr.openium.consentium.api.model.VendorChoice
|
||||
import fr.openium.consentium.data.remote.model.PatchConsent
|
||||
|
||||
internal fun VendorChoice.toPatchConsentVendorDTO() = PatchConsent.VendorDTO(
|
||||
identifier = vendorIdentifier.toString(),
|
||||
identifier = vendorIdentifier,
|
||||
isAccepted = isAccepted,
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
package fr.openium.consentium.api.model
|
||||
|
||||
enum class ConsentState {
|
||||
UNSET,
|
||||
VALID,
|
||||
EXPIRED,
|
||||
NEW_VERSION,
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package fr.openium.consentium.api.model
|
||||
|
||||
data class Purpose(
|
||||
val identifier: PurposeIdentifier,
|
||||
val isRequired: Boolean,
|
||||
val isAccepted: PurposeStatus,
|
||||
val identifier: String,
|
||||
val choice: PurposeStatus,
|
||||
val vendors: List<Vendor>,
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
package fr.openium.consentium.api.model
|
||||
|
||||
data class PurposeChoice(
|
||||
val purposeIdentifier: PurposeIdentifier,
|
||||
val isAccepted: Boolean,
|
||||
val purposeIdentifier: String,
|
||||
val choice: PurposeStatus,
|
||||
val vendors: List<VendorChoice>,
|
||||
)
|
@ -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"
|
||||
}
|
||||
}
|
@ -3,6 +3,5 @@ package fr.openium.consentium.api.model
|
||||
enum class PurposeStatus {
|
||||
ACCEPTED,
|
||||
REJECTED,
|
||||
NOT_DEFINED,
|
||||
UNKNOWN,
|
||||
PARTIAL,
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package fr.openium.consentium.api.model
|
||||
|
||||
data class Vendor(
|
||||
val identifier: VendorIdentifier,
|
||||
val identifier: String,
|
||||
val isAccepted: Boolean,
|
||||
val isRequired: Boolean,
|
||||
)
|
@ -1,6 +1,6 @@
|
||||
package fr.openium.consentium.api.model
|
||||
|
||||
data class VendorChoice(
|
||||
val vendorIdentifier: VendorIdentifier,
|
||||
val vendorIdentifier: String,
|
||||
val isAccepted: Boolean,
|
||||
)
|
@ -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"
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import dagger.Reusable
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import fr.openium.consentium.BuildConfig
|
||||
import fr.openium.consentium.api.mock.ConsentiumMockApi
|
||||
import fr.openium.consentium.data.remote.mock.ConsentiumMockApi
|
||||
import fr.openium.consentium.data.remote.ConsentiumApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Cache
|
||||
|
@ -5,21 +5,21 @@ import fr.openium.consentium.data.remote.model.PatchConsent
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
|
||||
internal interface ConsentiumApi {
|
||||
|
||||
@GET("/consents")
|
||||
@GET("consents")
|
||||
suspend fun getConsents(
|
||||
applicationId: String,
|
||||
installationId: String,
|
||||
@Header("Authorization") token: String,
|
||||
): Response<GetConsent.GetConsentPayloadDTO>
|
||||
|
||||
@PATCH("/consents")
|
||||
@POST("consents")
|
||||
suspend fun setConsents(
|
||||
applicationId: String,
|
||||
@Header("Authorization") token: String,
|
||||
@Body patchConsent: PatchConsent.PatchConsentPayloadDTO,
|
||||
): Response<Any>
|
||||
): Response<Unit>
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package fr.openium.consentium.api.mock
|
||||
package fr.openium.consentium.data.remote.mock
|
||||
|
||||
import fr.openium.consentium.data.remote.ConsentiumApi
|
||||
import fr.openium.consentium.data.remote.model.GetConsent
|
||||
@ -10,42 +10,30 @@ import java.util.UUID
|
||||
internal object ConsentiumMockApi : ConsentiumApi {
|
||||
|
||||
private val consents = GetConsent.GetConsentPayloadDTO(
|
||||
id = UUID.randomUUID().toString(),
|
||||
installationId = UUID.randomUUID().toString(),
|
||||
purposes = listOf(
|
||||
GetConsent.PurposeDTO(
|
||||
identifier = "purpose-audience",
|
||||
isRequired = true,
|
||||
isAccepted = GetConsent.PurposeStatusDTO.ACCEPTED,
|
||||
vendors = listOf(
|
||||
GetConsent.VendorDTO(
|
||||
identifier = "vendor-clarity",
|
||||
isAccepted = true,
|
||||
isRequired = true,
|
||||
),
|
||||
GetConsent.VendorDTO(
|
||||
identifier = "vendor-matomo",
|
||||
isAccepted = true,
|
||||
isRequired = false,
|
||||
)
|
||||
)
|
||||
vendors = null,
|
||||
choice = GetConsent.PurposeStatusDTO.ACCEPTED,
|
||||
),
|
||||
GetConsent.PurposeDTO(
|
||||
identifier = "purpose-required",
|
||||
isRequired = true,
|
||||
isAccepted = GetConsent.PurposeStatusDTO.REJECTED,
|
||||
vendors = null
|
||||
vendors = null,
|
||||
choice = GetConsent.PurposeStatusDTO.REJECTED,
|
||||
),
|
||||
),
|
||||
isValid = true
|
||||
state = GetConsent.ConsentStateDTO.VALID,
|
||||
)
|
||||
|
||||
override suspend fun getConsents(applicationId: String, installationId: String): Response<GetConsent.GetConsentPayloadDTO> {
|
||||
delay(500)
|
||||
override suspend fun getConsents(token: String): Response<GetConsent.GetConsentPayloadDTO> {
|
||||
delay(2000)
|
||||
return Response.success(consents)
|
||||
}
|
||||
|
||||
override suspend fun setConsents(applicationId: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response<Any> {
|
||||
delay(500)
|
||||
override suspend fun setConsents(token: String, patchConsent: PatchConsent.PatchConsentPayloadDTO): Response<Unit> {
|
||||
delay(2000)
|
||||
return Response.success(Unit)
|
||||
}
|
||||
|
||||
|
@ -7,16 +7,17 @@ internal sealed interface GetConsent {
|
||||
|
||||
@Serializable
|
||||
data class GetConsentPayloadDTO(
|
||||
@SerialName("id") val id: String? = null,
|
||||
@SerialName("installationId") val installationId: String,
|
||||
@SerialName("state") val state: ConsentStateDTO? = null,
|
||||
@SerialName("acceptedDate") val acceptedData: Long? = null,
|
||||
@SerialName("purposes") val purposes: List<PurposeDTO>? = null,
|
||||
@SerialName("isValid") val isValid: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PurposeDTO(
|
||||
@SerialName("identifier") val identifier: String,
|
||||
@SerialName("isRequired") val isRequired: Boolean,
|
||||
@SerialName("isAccepted") val isAccepted: PurposeStatusDTO,
|
||||
@SerialName("choice") val choice: PurposeStatusDTO,
|
||||
@SerialName("vendors") val vendors: List<VendorDTO>? = null,
|
||||
)
|
||||
|
||||
@ -29,16 +30,30 @@ internal sealed interface GetConsent {
|
||||
|
||||
@Serializable
|
||||
enum class PurposeStatusDTO {
|
||||
@SerialName("ACCEPTED")
|
||||
@SerialName("accepted")
|
||||
ACCEPTED,
|
||||
|
||||
@SerialName("REJECTED")
|
||||
@SerialName("refused")
|
||||
REJECTED,
|
||||
|
||||
@SerialName("NOT_DEFINED")
|
||||
NOT_DEFINED,
|
||||
@SerialName("partial")
|
||||
PARTIAL,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class ConsentStateDTO {
|
||||
@SerialName("unset")
|
||||
UNSET,
|
||||
|
||||
@SerialName("valid")
|
||||
VALID,
|
||||
|
||||
@SerialName("expired")
|
||||
EXPIRED,
|
||||
|
||||
@SerialName("new_version")
|
||||
NEW_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -7,15 +7,13 @@ internal sealed interface PatchConsent {
|
||||
|
||||
@Serializable
|
||||
data class PatchConsentPayloadDTO(
|
||||
@SerialName("installationId") val installationId: String,
|
||||
@SerialName("purposes") val purposes: List<PurposeDTO>? = null,
|
||||
@SerialName("consentPurposes") val purposes: List<PurposeDTO>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PurposeDTO(
|
||||
@SerialName("identifier") val identifier: String,
|
||||
@SerialName("isAccepted") val isAccepted: Boolean,
|
||||
@SerialName("vendors") val vendors: List<VendorDTO>? = null,
|
||||
@SerialName("choice") val choice: PurposeStatusDTO,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -24,4 +22,16 @@ internal sealed interface PatchConsent {
|
||||
@SerialName("isAccepted") val isAccepted: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class PurposeStatusDTO {
|
||||
@SerialName("accepted")
|
||||
ACCEPTED,
|
||||
|
||||
@SerialName("refused")
|
||||
REJECTED,
|
||||
|
||||
@SerialName("partial")
|
||||
PARTIAL,
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,23 +1,26 @@
|
||||
package fr.openium.consentium.domain.repository
|
||||
|
||||
import fr.openium.consentium.api.adapter.toConsentState
|
||||
import fr.openium.consentium.api.adapter.toPatchConsentPurposeDTO
|
||||
import fr.openium.consentium.api.adapter.toPurpose
|
||||
import fr.openium.consentium.api.model.ConsentState
|
||||
import fr.openium.consentium.api.model.Purpose
|
||||
import fr.openium.consentium.api.model.PurposeChoice
|
||||
import fr.openium.consentium.data.local.ConsentiumDataStore
|
||||
import fr.openium.consentium.data.remote.ConsentiumApi
|
||||
import fr.openium.consentium.data.remote.model.PatchConsent
|
||||
import fr.openium.consentium.domain.useCase.GetAuthTokenUseCase
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ConsentiumRepository @Inject constructor(
|
||||
private val consentiumApi: ConsentiumApi,
|
||||
private val consentiumDataStore: ConsentiumDataStore,
|
||||
private val getAuthTokenUseCase: GetAuthTokenUseCase,
|
||||
) {
|
||||
|
||||
suspend fun getConsents(applicationId: String): ConsentiumRepositoryGetResponse {
|
||||
val installationId = consentiumDataStore.getInstallationUniqueId()
|
||||
val consentsResponse = consentiumApi.getConsents(applicationId, installationId)
|
||||
suspend fun getConsents(apiKey: String): ConsentiumRepositoryGetResponse {
|
||||
return try {
|
||||
val authToken = getAuthTokenUseCase(apiKey)
|
||||
val consentsResponse = consentiumApi.getConsents(authToken)
|
||||
val consentsBody = if (consentsResponse.isSuccessful) {
|
||||
consentsResponse.body() ?: throw Exception()
|
||||
} else {
|
||||
@ -25,7 +28,7 @@ internal class ConsentiumRepository @Inject constructor(
|
||||
}
|
||||
|
||||
return ConsentiumRepositoryGetResponse.GetConsentsSuccess(
|
||||
isValid = consentsBody.isValid,
|
||||
state = consentsBody.state?.toConsentState() ?: ConsentState.UNSET,
|
||||
purposes = consentsBody.purposes?.map { purpose -> purpose.toPurpose() } ?: emptyList()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
@ -34,24 +37,28 @@ internal class ConsentiumRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun setConsents(
|
||||
applicationId: String,
|
||||
apiKey: String,
|
||||
consents: List<PurposeChoice>,
|
||||
): ConsentiumRepositorySetResponse {
|
||||
val installationId = consentiumDataStore.getInstallationUniqueId()
|
||||
|
||||
val setConsentResponse = consentiumApi.setConsents(
|
||||
applicationId = applicationId,
|
||||
patchConsent = PatchConsent.PatchConsentPayloadDTO(
|
||||
installationId = installationId,
|
||||
purposes = consents.map { it.toPatchConsentPurposeDTO() },
|
||||
return try {
|
||||
val authToken = getAuthTokenUseCase(apiKey)
|
||||
val setConsentResponse = consentiumApi.setConsents(
|
||||
token = authToken,
|
||||
patchConsent = PatchConsent.PatchConsentPayloadDTO(
|
||||
purposes = consents.map { it.toPatchConsentPurposeDTO() },
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return if (setConsentResponse.isSuccessful) {
|
||||
ConsentiumRepositorySetResponse.SetConsentsSuccess
|
||||
} else {
|
||||
if (setConsentResponse.isSuccessful) {
|
||||
ConsentiumRepositorySetResponse.SetConsentsSuccess
|
||||
} else {
|
||||
ConsentiumRepositorySetResponse.Error
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.d(e)
|
||||
ConsentiumRepositorySetResponse.Error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -61,7 +68,7 @@ internal interface ConsentiumRepositoryGetResponse {
|
||||
data object Error : ConsentiumRepositoryGetResponse
|
||||
|
||||
data class GetConsentsSuccess(
|
||||
val isValid: Boolean,
|
||||
val state: ConsentState,
|
||||
val purposes: List<Purpose>,
|
||||
) : ConsentiumRepositoryGetResponse
|
||||
|
||||
|
@ -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
Reference in New Issue
Block a user