diff --git a/core/domain/src/commonMain/kotlin/de/tabmates/core/domain/legal/LegalUrls.kt b/core/domain/src/commonMain/kotlin/de/tabmates/core/domain/legal/LegalUrls.kt new file mode 100644 index 00000000..aeb7199b --- /dev/null +++ b/core/domain/src/commonMain/kotlin/de/tabmates/core/domain/legal/LegalUrls.kt @@ -0,0 +1,11 @@ +package de.tabmates.core.domain.legal + +/** + * Public legal pages on the marketing site. Deliberately not derived from + * `BuildKonfig.BASE_URL_PUBLIC`: that points at the app host (app.tabmates.de) and, via the + * environment switcher, can point at a custom backend — the privacy policy must always resolve to + * the production site regardless of which server the app talks to. + */ +object LegalUrls { + const val PRIVACY_POLICY = "https://tabmates.de/privacy/" +} diff --git a/features/authentication/presentation/src/commonMain/composeResources/values-de/string.xml b/features/authentication/presentation/src/commonMain/composeResources/values-de/string.xml index 4b8cf93e..67808147 100644 --- a/features/authentication/presentation/src/commonMain/composeResources/values-de/string.xml +++ b/features/authentication/presentation/src/commonMain/composeResources/values-de/string.xml @@ -70,6 +70,12 @@ Ein Gastkonto existiert nur auf diesem Gerät — wenn du dich abmeldest oder das Gerät wechselst, verlierst du den Zugriff auf deine Gruppen. Du kannst es jederzeit im Profil in ein vollwertiges Konto umwandeln und alles behalten. + + Mit dem Erstellen eines Kontos stimmst du unserer + Wenn du fortfährst, stimmst du unserer + Datenschutzerklärung + zu. + Umgebung: %1$s Server-Umgebung diff --git a/features/authentication/presentation/src/commonMain/composeResources/values/string.xml b/features/authentication/presentation/src/commonMain/composeResources/values/string.xml index fe33e82a..bb928433 100644 --- a/features/authentication/presentation/src/commonMain/composeResources/values/string.xml +++ b/features/authentication/presentation/src/commonMain/composeResources/values/string.xml @@ -71,6 +71,12 @@ A guest account lives only on this device — sign out or switch devices and you lose access to your groups. You can turn it into a full account any time under Profile, and keep everything. + + By creating an account you agree to our + By continuing you agree to our + Privacy Policy + . + Environment: %1$s Server environment diff --git a/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/components/PrivacyNotice.kt b/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/components/PrivacyNotice.kt new file mode 100644 index 00000000..ae0f864d --- /dev/null +++ b/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/components/PrivacyNotice.kt @@ -0,0 +1,52 @@ +package de.tabmates.features.authentication.presentation.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.style.TextAlign +import de.tabmates.core.designsystem.preview.PreviewThemes +import de.tabmates.core.designsystem.text.TabMatesInlineLinkText +import de.tabmates.core.designsystem.theme.TabMatesTheme +import de.tabmates.core.domain.legal.LegalUrls +import org.jetbrains.compose.resources.stringResource +import tabmatesapp.features.authentication.presentation.generated.resources.Res +import tabmatesapp.features.authentication.presentation.generated.resources.privacy_notice_register_prefix +import tabmatesapp.features.authentication.presentation.generated.resources.privacy_notice_suffix +import tabmatesapp.features.authentication.presentation.generated.resources.privacy_policy_link + +/** + * Fine print shown wherever an account is created, so the privacy policy is one tap away at the + * moment data is actually collected. Informational only — no consent checkbox, because sign-up + * data is processed to perform the contract, not on the basis of consent. + * + * [prefix] is the sentence up to the link; the trailing part comes from `privacy_notice_suffix`, + * which German needs for the separable verb particle. + */ +@Composable +fun PrivacyNotice( + prefix: String, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + TabMatesInlineLinkText( + modifier = modifier, + textBeforeLink = prefix, + linkText = stringResource(Res.string.privacy_policy_link), + textAfterLink = stringResource(Res.string.privacy_notice_suffix), + onLinkClick = { uriHandler.openUri(LegalUrls.PRIVACY_POLICY) }, + textStyle = MaterialTheme.typography.bodySmall.copy(textAlign = TextAlign.Center), + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) +} + +@PreviewThemes +@Composable +private fun PrivacyNoticePreview() { + TabMatesTheme { + Surface { + PrivacyNotice(prefix = stringResource(Res.string.privacy_notice_register_prefix)) + } + } +} diff --git a/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/register/RegisterScreen.kt b/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/register/RegisterScreen.kt index 832cc294..cf08b00a 100644 --- a/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/register/RegisterScreen.kt +++ b/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/register/RegisterScreen.kt @@ -36,6 +36,7 @@ import de.tabmates.core.designsystem.textfields.TabMatesTextField import de.tabmates.core.designsystem.theme.TabMatesTheme import de.tabmates.core.designsystem.theme.headlineLargeBold import de.tabmates.core.presentation.util.ObserveAsEvents +import de.tabmates.features.authentication.presentation.components.PrivacyNotice import de.tabmates.features.authentication.presentation.di.AuthPresentationModule import de.tabmates.features.authentication.presentation.navigation.Login import de.tabmates.features.authentication.presentation.navigation.Register @@ -46,6 +47,7 @@ import org.koin.compose.KoinApplicationPreview import org.koin.compose.viewmodel.koinViewModel import org.koin.plugin.module.dsl.modules import tabmatesapp.features.authentication.presentation.generated.resources.Res +import tabmatesapp.features.authentication.presentation.generated.resources.privacy_notice_register_prefix import tabmatesapp.features.authentication.presentation.generated.resources.register_already_have_account_prefix import tabmatesapp.features.authentication.presentation.generated.resources.register_confirm_password_hint import tabmatesapp.features.authentication.presentation.generated.resources.register_email_hint @@ -201,6 +203,11 @@ private fun RegisterScreen( enabled = !state.isRegistering, isLoading = state.isRegistering, ) + VerticalSpacer(12.dp) + PrivacyNotice( + prefix = stringResource(Res.string.privacy_notice_register_prefix), + modifier = Modifier.widthIn(max = 300.dp), + ) VerticalSpacer(16.dp) TabMatesInlineLinkText( textBeforeLink = stringResource(Res.string.register_already_have_account_prefix), diff --git a/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/registerguest/RegisterGuestScreen.kt b/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/registerguest/RegisterGuestScreen.kt index 1a27297e..feb6b9ab 100644 --- a/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/registerguest/RegisterGuestScreen.kt +++ b/features/authentication/presentation/src/commonMain/kotlin/de/tabmates/features/authentication/presentation/registerguest/RegisterGuestScreen.kt @@ -30,9 +30,11 @@ import de.tabmates.core.designsystem.textfields.TabMatesTextField import de.tabmates.core.designsystem.theme.TabMatesTheme import de.tabmates.core.presentation.util.ObserveAsEvents import de.tabmates.core.presentation.util.UiText +import de.tabmates.features.authentication.presentation.components.PrivacyNotice import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import tabmatesapp.features.authentication.presentation.generated.resources.Res +import tabmatesapp.features.authentication.presentation.generated.resources.privacy_notice_guest_prefix import tabmatesapp.features.authentication.presentation.generated.resources.register_guest_desc import tabmatesapp.features.authentication.presentation.generated.resources.register_username_hint import tabmatesapp.features.authentication.presentation.generated.resources.welcome_button_guest @@ -112,6 +114,11 @@ private fun RegisterGuestScreen( onClick = onGuestClick, isLoading = isRegistering, ) + VerticalSpacer(12.dp) + PrivacyNotice( + prefix = stringResource(Res.string.privacy_notice_guest_prefix), + modifier = Modifier.widthIn(max = 300.dp), + ) } } diff --git a/features/tabgroup/presentation/src/commonMain/composeResources/drawable/ic_shield.xml b/features/tabgroup/presentation/src/commonMain/composeResources/drawable/ic_shield.xml new file mode 100644 index 00000000..f1604e45 --- /dev/null +++ b/features/tabgroup/presentation/src/commonMain/composeResources/drawable/ic_shield.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/features/tabgroup/presentation/src/commonMain/composeResources/values-de/string.xml b/features/tabgroup/presentation/src/commonMain/composeResources/values-de/string.xml index 67593a31..ef318a5c 100644 --- a/features/tabgroup/presentation/src/commonMain/composeResources/values-de/string.xml +++ b/features/tabgroup/presentation/src/commonMain/composeResources/values-de/string.xml @@ -318,6 +318,8 @@ ERSCHEINUNGSBILD ÜBER Version + Datenschutzerklärung + Öffnet tabmates.de in deinem Browser Open-Source-Lizenzen Bibliotheken von Drittanbietern Open-Source-Lizenzen diff --git a/features/tabgroup/presentation/src/commonMain/composeResources/values/string.xml b/features/tabgroup/presentation/src/commonMain/composeResources/values/string.xml index 5a1063c7..4086c9dc 100644 --- a/features/tabgroup/presentation/src/commonMain/composeResources/values/string.xml +++ b/features/tabgroup/presentation/src/commonMain/composeResources/values/string.xml @@ -319,6 +319,8 @@ APPEARANCE ABOUT Version + Privacy policy + Opens tabmates.de in your browser Open-source licenses Third-party libraries Open-source licenses diff --git a/features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/profile/ProfileScreen.kt b/features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/profile/ProfileScreen.kt index 30481cc8..690b07f0 100644 --- a/features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/profile/ProfileScreen.kt +++ b/features/tabgroup/presentation/src/commonMain/kotlin/de/tabmates/features/tabgroup/presentation/navigation/profile/ProfileScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue 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.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -39,6 +40,7 @@ import de.tabmates.core.data.AppBuildInfo import de.tabmates.core.designsystem.spacer.HorizontalSpacer import de.tabmates.core.designsystem.spacer.VerticalSpacer import de.tabmates.core.designsystem.text.SectionLabel +import de.tabmates.core.domain.legal.LegalUrls import de.tabmates.core.domain.preferences.ThemeMode import de.tabmates.core.presentation.util.ObserveAsEvents import de.tabmates.features.tabgroup.presentation.navigation.groupoverview.UserAvatar @@ -59,6 +61,7 @@ import tabmatesapp.features.tabgroup.presentation.generated.resources.ic_notific import tabmatesapp.features.tabgroup.presentation.generated.resources.ic_palette import tabmatesapp.features.tabgroup.presentation.generated.resources.ic_person_add import tabmatesapp.features.tabgroup.presentation.generated.resources.ic_settings +import tabmatesapp.features.tabgroup.presentation.generated.resources.ic_shield import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_account_email import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_account_password import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_account_username @@ -75,6 +78,8 @@ import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_op import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_oss_licenses import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_oss_licenses_caption import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_password_subtitle +import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_privacy_policy +import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_privacy_policy_caption import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_section_about import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_section_account import tabmatesapp.features.tabgroup.presentation.generated.resources.profile_section_appearance @@ -421,6 +426,7 @@ private fun SettingsTwoPane( SettingsSection.ABOUT -> { SectionLabel(stringResource(Res.string.profile_section_about)) AppVersionRow() + PrivacyPolicyRow() AccountRow( iconRes = Res.drawable.ic_info, title = stringResource(Res.string.profile_oss_licenses), @@ -629,6 +635,7 @@ private fun ProfileAccountAndAppearance( VerticalSpacer(4.dp) SectionLabel(stringResource(Res.string.profile_section_about)) AppVersionRow() + PrivacyPolicyRow() AccountRow( iconRes = Res.drawable.ic_info, title = stringResource(Res.string.profile_oss_licenses), @@ -651,6 +658,23 @@ private fun AppVersionRow() { ) } +/** + * The policy is hosted on the marketing site, so this row leaves the app — which the chevron alone + * cannot say. The subtitle does, so the row still looks like its siblings without misleading. + */ +@Composable +private fun PrivacyPolicyRow() { + val uriHandler = LocalUriHandler.current + AccountRow( + iconRes = Res.drawable.ic_shield, + title = stringResource(Res.string.profile_privacy_policy), + subtitle = stringResource(Res.string.profile_privacy_policy_caption), + onClick = { uriHandler.openUri(LegalUrls.PRIVACY_POLICY) }, + showChevron = true, + modifier = Modifier.fillMaxWidth(), + ) +} + @Composable private fun ProfileHeaderCard(state: ProfileState) { Surface(