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.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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