diff --git a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt index d003dca2..e5288a0b 100644 --- a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt +++ b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt @@ -187,7 +187,8 @@ private fun AnswerRevealContent( partnerAnswer = state.partnerAnswer, question = state.question, onHistory = onHistory, - onHome = onHome + onHome = onHome, + wasSealed = state.answer.schemaVersion == 3 ) if (state.followUpOptions.isNotEmpty()) { FollowUpSection( @@ -436,7 +437,7 @@ private fun WaitingForPartnerState( overflow = TextOverflow.Ellipsis ) Text( - text = "Your answer key is released. Once your partner opens their reveal, both answers will appear here.", + text = "Your answer key is released. You'll get a notification when your partner completes the reveal — no need to keep checking.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 4, @@ -480,12 +481,19 @@ private fun LostLocalKeyState(onHome: () -> Unit) { overflow = TextOverflow.Ellipsis ) Text( - text = "The sealed answer key was stored on the device you originally answered on. Open the app on that device to complete the reveal.", + text = "The sealed answer key is stored on the device you originally answered on. Open the app on that device to complete the reveal.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 5, overflow = TextOverflow.Ellipsis ) + Text( + text = "If you no longer have that device, this answer cannot be recovered.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error.copy(alpha = 0.75f), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) OutlinedButton( onClick = onHome, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), @@ -503,12 +511,13 @@ private fun RevealedState( partnerAnswer: LocalAnswer?, question: Question?, onHistory: () -> Unit, - onHome: () -> Unit + onHome: () -> Unit, + wasSealed: Boolean = false ) { RevealMessageCard { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - RevealPill("Revealed") + RevealPill(if (wasSealed) "Sealed reveal" else "Revealed") RevealPill(answer.category.displayCategoryName()) RevealPill(answer.answerType.displayQuestionType()) } diff --git a/firestore.rules b/firestore.rules index a739b6bd..c56c80ab 100644 --- a/firestore.rules +++ b/firestore.rules @@ -72,15 +72,18 @@ service cloud.firestore { // Sealed-answer helpers (schemaVersion 3, partner-proof reveal). function isSealedPayload(value) { - return value is string && value.matches('^sealed:v1:'); + // sealed:v1: + URL-safe base64 no-padding body; 80 chars minimum rules out trivially short values + return value is string && value.matches('^sealed:v1:[A-Za-z0-9_-]{80,}$'); } function isKeybox(value) { - return value is string && value.matches('^keybox:v1:'); + // keybox:v1: + URL-safe base64 no-padding; ECIES-P256 wrapping a 32-byte key is ~174 chars + return value is string && value.matches('^keybox:v1:[A-Za-z0-9_-]{120,}$'); } function isCommitmentHash(value) { - return value is string && value.matches('^sha256:'); + // sha256: + URL-safe base64 no-padding of a 32-byte digest = exactly 43 chars + return value is string && value.matches('^sha256:[A-Za-z0-9_-]{43}$'); } // Returns true when the incoming data satisfies the sealed-answer create shape.