diff --git a/CHANGELOG.md b/CHANGELOG.md
index f173dae..a92ec64 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## Unreleased
+
+- The check code stays on screen once enrolment finishes, labelled and with a reminder that it
+ should match the board and Sapwood, instead of disappearing as soon as the board's answer is
+ confirmed. It clears when the owner leaves the screen.
+- Release builds no longer embed git metadata, so the APK built by F-Droid matches the one
+ published here whatever checkout it was built from.
+
## 0.7.0 (2026-09-26)
- Adding a phone now scans Sapwood's own invite QR instead of the other way round: "Add a phone"
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e138cdc..1c9f176 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -49,6 +49,10 @@ android {
buildTypes {
release {
isMinifyEnabled = false
+ // Keep git metadata out of the APK: a build from a worktree or a tarball records
+ // NO_VALID_GIT_FOUND where F-Droid's clone records the commit, and that one file was
+ // enough to fail F-Droid's reproducible-build check for 0.5.0.
+ vcsInfo.include = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
if (releaseSigning.getProperty("storeFile") != null) {
signingConfig = signingConfigs.getByName("release")
diff --git a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt
index 9a147f4..958f1f1 100644
--- a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt
+++ b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt
@@ -285,7 +285,7 @@ class UnlockEnrolActivity : AppCompatActivity() {
conflicted = true
binding.enrolBatteryButton.isVisible = false
binding.enrolStatus.text = getString(R.string.enrol_conflict_after, storedId, handOff.id)
- binding.enrolCheckCode.isVisible = false
+ showCheckCode(false)
}
handOff.wipe()
return
@@ -300,7 +300,7 @@ class UnlockEnrolActivity : AppCompatActivity() {
enrolSecretHex = null
binding.enrolConfirmButton.isVisible = false
binding.enrolStatus.text = getString(R.string.enrol_conflict, first.id, handOff.id)
- binding.enrolCheckCode.isVisible = false
+ showCheckCode(false)
}
handOff.wipe()
return
@@ -319,7 +319,7 @@ class UnlockEnrolActivity : AppCompatActivity() {
binding.enrolRetryButton.isVisible = false
binding.enrolStatus.text = getString(R.string.enrol_received)
binding.enrolCheckCode.text = checkCode(envelope.ephemeralPubkeyHex).orEmpty()
- binding.enrolCheckCode.isVisible = true
+ showCheckCode(true)
binding.enrolConfirmButton.isVisible = true
lifecycleScope.launch {
delay(PENDING_TIMEOUT_MILLIS)
@@ -330,7 +330,7 @@ class UnlockEnrolActivity : AppCompatActivity() {
enrolSecretHex = null
binding.enrolConfirmButton.isVisible = false
binding.enrolStatus.text = getString(R.string.enrol_timed_out, handOff.id)
- binding.enrolCheckCode.isVisible = false
+ showCheckCode(false)
}
}
promptSeal()
@@ -393,8 +393,9 @@ class UnlockEnrolActivity : AppCompatActivity() {
PairingStore(this).setKeepAliveEnabled(true)
HeartwoodKeepAliveService.start(this)
+ // Left showing on Done: the owner may still be comparing it with Sapwood or the board's
+ // own card at this point, and it stays harmless to see once the slot secret is sealed.
binding.enrolConfirmButton.isVisible = false
- binding.enrolCheckCode.isVisible = false
binding.enrolStatus.text = getString(R.string.enrol_done, handOff.id)
binding.enrolBatteryButton.isVisible = !isIgnoringBatteryOptimisations(this)
binding.enrolDoneButton.setText(R.string.enrol_finished)
@@ -412,6 +413,13 @@ class UnlockEnrolActivity : AppCompatActivity() {
private fun shortLabel(pairing: Pairing): String =
pairing.label?.takeIf { it.isNotBlank() } ?: pairing.displayLabel().let { if (it.length > 20) it.take(12) + "…" else it }
+ /** The check code and its label/hint always show or hide together. */
+ private fun showCheckCode(visible: Boolean) {
+ binding.enrolCheckCodeLabel.isVisible = visible
+ binding.enrolCheckCode.isVisible = visible
+ binding.enrolCheckCodeHint.isVisible = visible
+ }
+
private fun showOnly(message: String) {
binding.enrolBody.isVisible = false
binding.enrolScanButton.isVisible = false
diff --git a/app/src/main/res/layout/activity_unlock_enrol.xml b/app/src/main/res/layout/activity_unlock_enrol.xml
index 6c20b18..c6129b5 100644
--- a/app/src/main/res/layout/activity_unlock_enrol.xml
+++ b/app/src/main/res/layout/activity_unlock_enrol.xml
@@ -140,12 +140,24 @@
android:textColor="@color/on_background"
tools:text="Waiting for the board…" />
+
+
+
+