feat: update PrivacyScreen, add Firestore test scripts, gitleaks audit artifacts

This commit is contained in:
null 2026-06-19 03:45:53 -05:00
parent 9e587a23dd
commit 803b681d06
8 changed files with 1308 additions and 8 deletions

View File

@ -3,20 +3,27 @@ package app.closer.ui.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
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.CardDefaults
import androidx.compose.material3.Divider
@ -32,11 +39,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.closer.core.navigation.ExternalLinks
import app.closer.ui.theme.CloserPalette
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -72,14 +81,129 @@ fun PrivacyScreen(
.verticalScroll(rememberScrollState())
.padding(padding)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
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,
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(
modifier = Modifier.fillMaxWidth(),
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 = "Answer text and messages are private by design and are never shared with third parties.",
style = MaterialTheme.typography.bodySmall,
color = SettingsMuted,
modifier = Modifier.padding(horizontal = 4.dp)
text = title,
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
color = SettingsInk
)
}
}
@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
)
}
}

View File

@ -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";
}

View File

@ -0,0 +1,3 @@
export default async function () {
// Nothing to tear down — the emulator manages its own lifecycle.
}

View File

@ -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

View File

@ -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"]
}

42
gitleaks-current.json Normal file
View File

@ -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"
}
]

1
gitleaks-history.json Normal file
View File

@ -0,0 +1 @@
[]