feat: update PrivacyScreen, add Firestore test scripts, gitleaks audit artifacts
This commit is contained in:
parent
9e587a23dd
commit
803b681d06
|
|
@ -3,20 +3,27 @@ package app.closer.ui.settings
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
|
|
@ -32,11 +39,13 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.closer.core.navigation.ExternalLinks
|
import app.closer.core.navigation.ExternalLinks
|
||||||
|
import app.closer.ui.theme.CloserPalette
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -72,14 +81,129 @@ fun PrivacyScreen(
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Your data stays between the two of you. These documents explain exactly what we collect, how we use it, and what rights you have.",
|
text = "Closer is built on one rule: answers stay private until both of you have answered. Here's exactly what that means.",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = SettingsMuted
|
color = SettingsMuted
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── What your partner can see ─────────────────────────────────────
|
||||||
|
PrivacySectionHeader(
|
||||||
|
icon = Icons.Default.CheckCircle,
|
||||||
|
iconTint = CloserPalette.PurpleDeep,
|
||||||
|
title = "What your partner can see"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Daily question answers",
|
||||||
|
body = "Only after you've both answered. Until then, your partner sees \"waiting for you\" — not your answer."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Game results",
|
||||||
|
body = "Revealed together at the end of a round — This or That matches, How Well Do You Know Me scores, and Desire Sync overlaps."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Desire Sync: shared yes answers only",
|
||||||
|
body = "Questions where only one of you said yes are never shown to either partner. Only mutual overlap is revealed."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Discussion messages and reactions",
|
||||||
|
body = "Messages you send in question threads are visible to your partner in real time."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Shared game history",
|
||||||
|
body = "Both partners can replay past rounds from Past Games. The replay shows the same answers both of you gave."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Streak and shared wins",
|
||||||
|
body = "Your couple's streak count and challenge completions are shared between both of you."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── What stays private ────────────────────────────────────────────
|
||||||
|
PrivacySectionHeader(
|
||||||
|
icon = Icons.Default.Lock,
|
||||||
|
iconTint = CloserPalette.Romantic,
|
||||||
|
title = "What stays private"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Answers before your partner answers",
|
||||||
|
body = "Your partner cannot see what you said until they've answered too. No peeking."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Unanswered desire prompts",
|
||||||
|
body = "In Desire Sync, prompts where only one of you tapped yes are never surfaced to either person."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Time capsule contents",
|
||||||
|
body = "What's inside a capsule stays sealed until the unlock date both partners agreed on."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Notification settings",
|
||||||
|
body = "Your notification preferences are yours alone. Your partner cannot see or change them."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Deleting your account ─────────────────────────────────────────
|
||||||
|
PrivacySectionHeader(
|
||||||
|
icon = Icons.Default.VisibilityOff,
|
||||||
|
iconTint = SettingsDanger,
|
||||||
|
title = "Deleting your account"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||||
|
PrivacyRow(
|
||||||
|
title = "Immediate and permanent",
|
||||||
|
body = "Deleting your account removes your profile and sign-in instantly. Your partner is unpaired and can start fresh. This cannot be undone."
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
|
PrivacyRow(
|
||||||
|
title = "No data export",
|
||||||
|
body = "Closer does not currently offer a data export. Your answers and history are deleted along with your account."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// ── Legal docs ────────────────────────────────────────────────────
|
||||||
|
Text(
|
||||||
|
text = "Legal documents",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = SettingsInk,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
|
@ -112,13 +236,56 @@ fun PrivacyScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PrivacySectionHeader(
|
||||||
|
icon: ImageVector,
|
||||||
|
iconTint: Color,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
.background(iconTint.copy(alpha = 0.12f), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = null, modifier = Modifier.size(16.dp), tint = iconTint)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Answer text and messages are private by design and are never shared with third parties.",
|
text = title,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = SettingsMuted,
|
color = SettingsInk
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PrivacyRow(title: String, body: String) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = SettingsInk
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = body,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = SettingsMuted
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Runs once before the full test suite.
|
||||||
|
// The Firestore emulator must already be running on port 8080 before running tests.
|
||||||
|
// Start it with: firebase emulators:start --only firestore
|
||||||
|
export default async function () {
|
||||||
|
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default async function () {
|
||||||
|
// Nothing to tear down — the emulator manages its own lifecycle.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "closer-firestore-rules-tests",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --runInBand",
|
||||||
|
"test:watch": "jest --runInBand --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@firebase/rules-unit-testing": "^4.0.1",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"firebase": "^11.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testTimeout": 30000,
|
||||||
|
"globalSetup": "./jest.globalSetup.ts",
|
||||||
|
"globalTeardown": "./jest.globalTeardown.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Description": "Generic API Key",
|
||||||
|
"StartLine": 31,
|
||||||
|
"EndLine": 31,
|
||||||
|
"StartColumn": 21,
|
||||||
|
"EndColumn": 67,
|
||||||
|
"Match": "key\": \"REDACTED\"",
|
||||||
|
"Secret": "REDACTED",
|
||||||
|
"File": "app/google-services.json",
|
||||||
|
"SymlinkFile": "",
|
||||||
|
"Commit": "",
|
||||||
|
"Entropy": 4.7851515,
|
||||||
|
"Author": "",
|
||||||
|
"Email": "",
|
||||||
|
"Date": "",
|
||||||
|
"Message": "",
|
||||||
|
"Tags": [],
|
||||||
|
"RuleID": "generic-api-key",
|
||||||
|
"Fingerprint": "app/google-services.json:generic-api-key:31"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Description": "Generic API Key",
|
||||||
|
"StartLine": 68,
|
||||||
|
"EndLine": 68,
|
||||||
|
"StartColumn": 21,
|
||||||
|
"EndColumn": 67,
|
||||||
|
"Match": "key\": \"REDACTED\"",
|
||||||
|
"Secret": "REDACTED",
|
||||||
|
"File": "app/google-services.json",
|
||||||
|
"SymlinkFile": "",
|
||||||
|
"Commit": "",
|
||||||
|
"Entropy": 4.7851515,
|
||||||
|
"Author": "",
|
||||||
|
"Email": "",
|
||||||
|
"Date": "",
|
||||||
|
"Message": "",
|
||||||
|
"Tags": [],
|
||||||
|
"RuleID": "generic-api-key",
|
||||||
|
"Fingerprint": "app/google-services.json:generic-api-key:68"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
Loading…
Reference in New Issue