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(