From 14a3f805fb7ceec8221e477ece52a56305b906fe Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Fri, 12 Jan 2024 16:21:24 +1100 Subject: [PATCH 1/3] feat: add privacyPolicyStep to the onboarding. closes #10379 Signed-off-by: KaiserBh --- .../more/onboarding/PrivacyPolicyStep.kt | 222 ++++++++++++++++++ .../commonMain/resources/MR/base/strings.xml | 7 + 2 files changed, 229 insertions(+) create mode 100644 app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt new file mode 100644 index 0000000000..574417fa20 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt @@ -0,0 +1,222 @@ +package eu.kanade.presentation.more.onboarding + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.rememberScrollState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +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.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.material3.Text +import androidx.compose.runtime.derivedStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.sp +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource + +class PrivacyPolicyStep : OnboardingStep { + private var _isComplete by mutableStateOf(false) + + override val isComplete: Boolean + get() = _isComplete + + @Composable + override fun Content() { + var isAgreed by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + val isScrolledToEnd = remember(scrollState) { + derivedStateOf { + val maxScroll = scrollState.maxValue + scrollState.value >= maxScroll + } + } + + Column(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier + .height(500.dp) + .padding(16.dp) + .verticalScroll(scrollState)) { + + Column { + PrivacyPolicyIntro() + PrivacyInformationCollectionUse() + } + } + + // Agreement Checkbox + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + stringResource(resource = MR.strings.onboarding_privacy_agree_to_terms), + style = TextStyle( + fontSize = 18.sp, + ), + ) + Checkbox( + enabled = isScrolledToEnd.value, + checked = isAgreed, + onCheckedChange = { + isAgreed = it + _isComplete = it + } + ) + } + } + } + + @Composable + fun HeadingText(text: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = text, + style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold) + ) + } + + @Composable + fun BodyText(text: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = text, + style = TextStyle(fontSize = 16.sp) + ) + } + + @Composable + fun HorizontalDivider() { + HorizontalDivider(color = Color.Gray) + } + + @Composable + fun Spacer(size: Int, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.height(size.dp)) + } + + @Composable + fun BulletPoint(text: String, url: String, modifier: Modifier= Modifier) { + val uriHandler = LocalUriHandler.current + + val annotatedString = buildAnnotatedString { + withStyle(style = SpanStyle(color = Color(0xFF0645AD), fontSize = 16.sp)) { + append("• $text") + } + addStringAnnotation( + tag = "URL", + annotation = url, + start = 2, + end = 2 + text.length + ) + } + + ClickableText( + modifier = modifier, + text = annotatedString, + onClick = { offset -> + annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { annotation -> + uriHandler.openUri(annotation.item) + } + }, + style = TextStyle(fontSize = 16.sp) + ) + } + + @Composable + fun MoreInfoText(url: String, modifier: Modifier = Modifier) { + val uriHandler = LocalUriHandler.current + + val annotatedString = buildAnnotatedString { + withStyle(style = SpanStyle(color = Color(0xFF0645AD), fontSize = 16.sp)) { + append(stringResource(resource = MR.strings.learn_more)) + } + addStringAnnotation( + tag = "URL", + annotation = url, + start = 0, + end = stringResource(resource = MR.strings.learn_more).length, + ) + } + + ClickableText( + text = annotatedString, + modifier = modifier, + onClick = { offset -> + annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { annotation -> + uriHandler.openUri(annotation.item) + } + }, + style = TextStyle(fontSize = 16.sp) + ) + } + + @Composable + fun PrivacyPolicyIntro(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + HeadingText(stringResource(resource = MR.strings.privacy_policy)) + Spacer(20) + + BodyText(stringResource(resource = MR.strings.onboarding_privacy_intro)) + Spacer(10) + + BodyText(stringResource(resource = MR.strings.onboarding_privacy_info_overview)) + Spacer(10) + + BodyText(stringResource(resource = MR.strings.onboarding_privacy_agreement_acknowledgement)) + Spacer(20) + + HorizontalDivider() + Spacer(20) + } + } + + @Composable + fun PrivacyInformationCollectionUse(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + HeadingText(stringResource(resource = MR.strings.onboarding_privacy_information_collection_use)) + Spacer(20) + + BodyText(stringResource(resource = MR.strings.onboarding_privacy_information_collection_use_intro)) + Spacer(10) + + BodyText( stringResource(resource = MR.strings.onboarding_privacy_information_collection_use_data)) + Spacer(10) + + BulletPoint("Sentry", "https://sentry.io/privacy/") + BulletPoint("Firebase Analytics", "https://www.google.com/analytics/terms/") + + Spacer(20) + HorizontalDivider() + Spacer(20) + + MoreInfoText("https://tachiyomi.org/privacy/") + } + } +} diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index ccb9e8721d..68e9b81ba2 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -189,6 +189,13 @@ Grant New to %s? We recommend checking out the getting started guide. Reinstalling %s? + I agree to the Privacy Policy + Tachiyomi is an Open Source app. This SERVICE is provided at no cost and is intended for use as is. + This page details our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service. + If you choose to use our Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy. + Information Collection and Use + For a better experience, while using our Service, we may require you to provide us with certain personally identifiable information. The information that we request will be retained by us and used as described in this privacy policy. + Links to the privacy policy of third-party service providers used by the app: From 74815f308fa939b44f842f76a3a555b8e12f4334 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Fri, 12 Jan 2024 16:22:09 +1100 Subject: [PATCH 2/3] refactor: disable going back to privacy policy. Once it's accepted they have to uninstall the app if they change their mind. Signed-off-by: KaiserBh --- .../eu/kanade/presentation/more/onboarding/OnboardingScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt index c5fd8c2faf..c8af6cc801 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt @@ -34,6 +34,7 @@ fun OnboardingScreen( var currentStep by rememberSaveable { mutableIntStateOf(0) } val steps = remember { listOf( + PrivacyPolicyStep(), ThemeStep(), StorageStep(), PermissionStep(), @@ -42,7 +43,7 @@ fun OnboardingScreen( } val isLastStep = currentStep == steps.lastIndex - BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) + BackHandler(enabled = currentStep != 1, onBack = { currentStep-- }) InfoScreen( icon = Icons.Outlined.RocketLaunch, From d78c6cce3ffc7244e0e9e01982b169b1a70049e6 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Fri, 12 Jan 2024 16:23:06 +1100 Subject: [PATCH 3/3] chore: lint Signed-off-by: KaiserBh --- .../more/onboarding/PrivacyPolicyStep.kt | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt index 574417fa20..1155044aa6 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PrivacyPolicyStep.kt @@ -14,16 +14,15 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Checkbox import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.material3.Text -import androidx.compose.runtime.derivedStateOf import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.SpanStyle @@ -31,6 +30,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource @@ -54,11 +54,12 @@ class PrivacyPolicyStep : OnboardingStep { } Column(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier - .height(500.dp) - .padding(16.dp) - .verticalScroll(scrollState)) { - + Box( + modifier = Modifier + .height(500.dp) + .padding(16.dp) + .verticalScroll(scrollState), + ) { Column { PrivacyPolicyIntro() PrivacyInformationCollectionUse() @@ -71,7 +72,7 @@ class PrivacyPolicyStep : OnboardingStep { .fillMaxWidth() .padding(16.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, ) { Text( stringResource(resource = MR.strings.onboarding_privacy_agree_to_terms), @@ -85,7 +86,7 @@ class PrivacyPolicyStep : OnboardingStep { onCheckedChange = { isAgreed = it _isComplete = it - } + }, ) } } @@ -96,7 +97,7 @@ class PrivacyPolicyStep : OnboardingStep { Text( modifier = modifier, text = text, - style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold) + style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold), ) } @@ -105,7 +106,7 @@ class PrivacyPolicyStep : OnboardingStep { Text( modifier = modifier, text = text, - style = TextStyle(fontSize = 16.sp) + style = TextStyle(fontSize = 16.sp), ) } @@ -120,7 +121,7 @@ class PrivacyPolicyStep : OnboardingStep { } @Composable - fun BulletPoint(text: String, url: String, modifier: Modifier= Modifier) { + fun BulletPoint(text: String, url: String, modifier: Modifier = Modifier) { val uriHandler = LocalUriHandler.current val annotatedString = buildAnnotatedString { @@ -131,7 +132,7 @@ class PrivacyPolicyStep : OnboardingStep { tag = "URL", annotation = url, start = 2, - end = 2 + text.length + end = 2 + text.length, ) } @@ -144,7 +145,7 @@ class PrivacyPolicyStep : OnboardingStep { uriHandler.openUri(annotation.item) } }, - style = TextStyle(fontSize = 16.sp) + style = TextStyle(fontSize = 16.sp), ) } @@ -173,7 +174,7 @@ class PrivacyPolicyStep : OnboardingStep { uriHandler.openUri(annotation.item) } }, - style = TextStyle(fontSize = 16.sp) + style = TextStyle(fontSize = 16.sp), ) } @@ -206,7 +207,7 @@ class PrivacyPolicyStep : OnboardingStep { BodyText(stringResource(resource = MR.strings.onboarding_privacy_information_collection_use_intro)) Spacer(10) - BodyText( stringResource(resource = MR.strings.onboarding_privacy_information_collection_use_data)) + BodyText(stringResource(resource = MR.strings.onboarding_privacy_information_collection_use_data)) Spacer(10) BulletPoint("Sentry", "https://sentry.io/privacy/")