fix: reveal screen UX and rules hardening (batch v1.0.20)
This commit is contained in:
parent
737514d18f
commit
9c1fbf60a0
|
|
@ -187,7 +187,8 @@ private fun AnswerRevealContent(
|
||||||
partnerAnswer = state.partnerAnswer,
|
partnerAnswer = state.partnerAnswer,
|
||||||
question = state.question,
|
question = state.question,
|
||||||
onHistory = onHistory,
|
onHistory = onHistory,
|
||||||
onHome = onHome
|
onHome = onHome,
|
||||||
|
wasSealed = state.answer.schemaVersion == 3
|
||||||
)
|
)
|
||||||
if (state.followUpOptions.isNotEmpty()) {
|
if (state.followUpOptions.isNotEmpty()) {
|
||||||
FollowUpSection(
|
FollowUpSection(
|
||||||
|
|
@ -436,7 +437,7 @@ private fun WaitingForPartnerState(
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
|
|
@ -480,12 +481,19 @@ private fun LostLocalKeyState(onHome: () -> Unit) {
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 5,
|
maxLines = 5,
|
||||||
overflow = TextOverflow.Ellipsis
|
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(
|
OutlinedButton(
|
||||||
onClick = onHome,
|
onClick = onHome,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
|
||||||
|
|
@ -503,12 +511,13 @@ private fun RevealedState(
|
||||||
partnerAnswer: LocalAnswer?,
|
partnerAnswer: LocalAnswer?,
|
||||||
question: Question?,
|
question: Question?,
|
||||||
onHistory: () -> Unit,
|
onHistory: () -> Unit,
|
||||||
onHome: () -> Unit
|
onHome: () -> Unit,
|
||||||
|
wasSealed: Boolean = false
|
||||||
) {
|
) {
|
||||||
RevealMessageCard {
|
RevealMessageCard {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
RevealPill("Revealed")
|
RevealPill(if (wasSealed) "Sealed reveal" else "Revealed")
|
||||||
RevealPill(answer.category.displayCategoryName())
|
RevealPill(answer.category.displayCategoryName())
|
||||||
RevealPill(answer.answerType.displayQuestionType())
|
RevealPill(answer.answerType.displayQuestionType())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,18 @@ service cloud.firestore {
|
||||||
// Sealed-answer helpers (schemaVersion 3, partner-proof reveal).
|
// Sealed-answer helpers (schemaVersion 3, partner-proof reveal).
|
||||||
|
|
||||||
function isSealedPayload(value) {
|
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) {
|
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) {
|
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.
|
// Returns true when the incoming data satisfies the sealed-answer create shape.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue