Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
48f8cd8
Bump Android and iOS build numbers
praveenperera Sep 7, 2026
8b3554a
Count confirmed cloud backups as recovery coverage
praveenperera Sep 9, 2026
5421e5e
Own cloud-only wallet dialogs per action button
praveenperera Sep 9, 2026
b77fedb
Preserve coverage after wallet-set changes
praveenperera Sep 10, 2026
cbad43b
Invalidate coverage on integrity downgrade
praveenperera Sep 10, 2026
d94c06a
Bump iOS build number to 118
praveenperera Sep 11, 2026
9b3c722
Confirm trusted local cloud inventory with provider
praveenperera Sep 11, 2026
23c7425
Fix wipe-PIN launch after full wipe
praveenperera Sep 11, 2026
6cc6d4e
Preserve setup flag across full wipe
praveenperera Sep 11, 2026
ca8482e
Reject redirected AASA fetches in iOS xtask
praveenperera Sep 12, 2026
5aa910c
Require user verification for Android passkeys
praveenperera Sep 12, 2026
5f8160f
Add passkey request timing diagnostics on iOS
praveenperera Sep 12, 2026
e059422
Handoff cloud backup actions after prompt dismiss
praveenperera Sep 12, 2026
a155067
Harden cloud backup passkey match and restore
praveenperera Sep 12, 2026
9e0f9ef
Isolate Rust test databases under temp dirs
praveenperera Sep 12, 2026
86f0217
Cover wallet clear cancel and send-flow cleanup
praveenperera Sep 12, 2026
de24a2d
Bump iOS build number to 119
praveenperera Sep 12, 2026
1736f7e
Add just recipe to upload TestFlight without bump
praveenperera Sep 12, 2026
2b0359e
Add tf and utf aliases for TestFlight recipes
praveenperera Sep 12, 2026
ecbb502
Initialize Rust test storage before first access
praveenperera Sep 12, 2026
d85e58e
Remove wallet session data during deletion
praveenperera Sep 12, 2026
f929b80
Clear wallet managers and sensitive sessions
praveenperera Sep 12, 2026
1553f23
Wait for iOS prompts before starting actions
praveenperera Sep 12, 2026
b912fe8
Release iOS passkey requests on the main thread
praveenperera Sep 12, 2026
47c9c75
Bump iOS build number to 121
praveenperera Sep 12, 2026
49889d3
Simplify xtask argument parsing and ownership
praveenperera Sep 12, 2026
f42d1dd
Automate beta release distribution
praveenperera Sep 12, 2026
3def203
Preserve cloud backup passkey request errors
praveenperera Sep 12, 2026
b9c54c9
Cancel stale actions when a new prompt is queued
praveenperera Sep 12, 2026
9dfe4ea
Preserve backup coverage after cancelled checks
praveenperera Sep 12, 2026
05e8565
Remove timing from lifecycle preparation test
praveenperera Sep 12, 2026
00e0ff2
Reuse settled iCloud metadata generations
praveenperera Sep 14, 2026
8a4e68b
Keep restore download order with open slots
praveenperera Sep 14, 2026
b146470
Compose iOS TestFlight from bump, bir, upload
praveenperera Sep 14, 2026
5b89d5d
Move Google Play release into xtask
praveenperera Sep 14, 2026
a22ef84
Bump iOS build and Android versionCode
praveenperera Sep 14, 2026
8fb65c1
Adopt matching leftover Keychain on restore
praveenperera Sep 17, 2026
330e82f
Report local Keychain conflicts on restore
praveenperera Sep 17, 2026
7f87a42
Soften cancelled passkey request copy
praveenperera Sep 17, 2026
89ff22a
Hide deleted iCloud paths from metadata
praveenperera Sep 18, 2026
e75328c
Bump iOS build number to 123
praveenperera Sep 18, 2026
c157895
Bump iOS build number to 124
praveenperera Sep 18, 2026
14530a9
Reuse mock passkey auth default result
praveenperera Sep 18, 2026
57222d6
Record coordinator-resolved iCloud delete URLs
praveenperera Sep 18, 2026
a4ad23e
Add after-save hook to mock keychain
praveenperera Sep 18, 2026
ed21562
Commit restore keychain and state together
praveenperera Sep 18, 2026
2db9afa
Make wallet conflict copy platform-neutral
praveenperera Sep 18, 2026
962fe7c
Harden legacy test dir cleanup logic
praveenperera Sep 18, 2026
8fdc3e6
Keep sticky auth default out of queue
praveenperera Sep 18, 2026
8c25c51
Extract simplified cloud restore module
praveenperera Sep 18, 2026
c65f5cc
Bundle restore namespace commit args
praveenperera Sep 18, 2026
905dd0d
Drop legacy home test dir cleanup
praveenperera Sep 18, 2026
3aadc74
Share wallet test helpers in test_support
praveenperera Sep 18, 2026
a5b2892
Isolate payjoin tests from lifecycle wipe
praveenperera Sep 18, 2026
a61dd97
Raise iOS local backup read test timeout
praveenperera Sep 18, 2026
13a46a6
Make TestFlight mock sockets blocking
praveenperera Sep 18, 2026
8a1b333
Roll back failed restore namespace commit
praveenperera Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export COVE_KEYSTORE_PASSWORD="your-keystore-password-here"
export COVE_KEY_ALIAS="upload"
export COVE_KEY_PASSWORD="your-key-password-here"

# Google Play internal releases: just rela (requires fastlane)
# grant this service account access to testing releases for org.bitcoinppl.cove
# export GOOGLE_PLAY_JSON_KEY_PATH="$HOME/.secrets/cove-google-play.json"

# iOS device aliases for: just brall --device main|se
# export IOS_DEVICE_MAIN="00008120-0006243420214032"
# export IOS_DEVICE_SE="00008xxx-xxxxxxxxxxxxxxxx"
Expand Down
8 changes: 6 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ Set `ASC_API_KEY_PATH`, `ASC_API_KEY_ID`, and `ASC_API_ISSUER_ID`. The API key m
### Android

```bash
just build-android-release # alias: just bar
just release-android # alias: just rela
```

Then build a signed APK/AAB via Android Studio (Build → Generate Signed Bundle/APK).
This bumps the Android versionCode, rebuilds the release Android bindings, builds the signed Play bundle, and uploads it to Google Play internal testing.

Set `GOOGLE_PLAY_JSON_KEY_PATH` to a readable Google Play service account JSON file. Install fastlane (`brew install fastlane`). The service account must have access to testing releases for `org.bitcoinppl.cove`.

Use `just upload-google-play` (`just ugp`) when the versionCode was already bumped and the signed bundle already exists.

## Development Workflow

Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ android {
applicationId = "org.bitcoinppl.cove"
minSdk = 33
targetSdk = 36
versionCode = 39
versionCode = 41
versionName = "1.4.0"
manifestPlaceholders["keyTeleportAppLinksEnabled"] = "false"

Expand Down
56 changes: 54 additions & 2 deletions android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -369,22 +369,74 @@ class AppManager private constructor() : FfiReconcile {
* clears all cached data and reinitializes
*/
fun reset() {
clearSessionForReset()
resetRustProjection()
}

private fun clearSessionForReset() {
// close managers before clearing them
clearWalletManager()
clearKeyTeleportManager()
corruptedWalletDeletionRetry = null
}

private fun resetRustProjection(): Boolean {
database = Database()
needsOnboarding =
withRustOr(needsOnboarding) {
needsOnboarding()
}

val routerState =
withRustOr(null) {
withRustOr<AppState?>(null) {
state()
} ?: return false

router.reset(routerState.router)
return true
}

/**
* Reset the app session after a successful full wipe
*
* Rust publishes the empty database and direct route through reconciliation. This clears
* Android-owned presentation state before authentication is released
*/
internal fun resetAfterWipe() {
val projectionReset = resetProjectionAfterWipe()
if (!projectionReset) {
router.reconcileDefaultRouteChanged(
Route.NewWallet(NewWalletRoute.Select),
emptyList(),
)
}

router.isSidebarVisible = false
wallets = emptyList()
isLoading = false
alertState = null
sheetState = null
}

private fun resetProjectionAfterWipe(): Boolean {
val resetResult =
runCatching {
clearSessionForReset()
resetRustProjection()
}
router.reset(routerState?.router)

if (resetResult.getOrNull() == true) return true

resetResult.exceptionOrNull()?.let { error ->
Log.e(tag, "failed to reset app session after wipe", error)
}

val projectionResult = runCatching { resetRustProjection() }
projectionResult.exceptionOrNull()?.let { error ->
Log.e(tag, "failed to reset app projection after wipe", error)
}

return projectionResult.getOrDefault(false)
}

val currentRoute: Route
Expand Down
87 changes: 63 additions & 24 deletions android/app/src/main/java/org/bitcoinppl/cove/AuthManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ sealed interface WipePresentationState {
data object Idle : WipePresentationState
data object Running : WipePresentationState
data class ShutdownBlocked(val attemptId: ShutdownAttemptId) : WipePresentationState
data class Failed(val message: String) : WipePresentationState
data object Failed : WipePresentationState
}

internal const val WIPE_FAILURE_TITLE = "Unable to Open Cove"
internal const val WIPE_FAILURE_MESSAGE = "Please try again."

/**
* auth manager - manages authentication state
* ported from iOS AuthManager.swift
Expand All @@ -72,8 +75,7 @@ class AuthManager internal constructor(
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val wipeCommand = OwnerScopedCommand<UnlockMode>(mainScope)

private var rust: RustAuthManager = RustAuthManager()
private set
private val rust = RustAuthManager()
private val isRustClosed = AtomicBoolean(false)
private val rustGuard =
RustHandleGuard(
Expand Down Expand Up @@ -190,9 +192,7 @@ class AuthManager internal constructor(
}

internal fun completeMainBiometricAuthentication() {
if (isInDecoyMode()) {
switchToMainMode()
}
if (isInDecoyMode() && !switchToMainMode()) return

recordMainCredentialAuthentication()
unlock()
Expand Down Expand Up @@ -258,8 +258,8 @@ class AuthManager internal constructor(
}

private fun unlockWithMainPin(): UnlockMode {
if (Database().globalConfig().isInDecoyMode()) {
switchToMainMode()
if (Database().globalConfig().isInDecoyMode() && !switchToMainMode()) {
return UnlockMode.LOCKED
}

recordMainCredentialAuthentication()
Expand Down Expand Up @@ -318,49 +318,88 @@ class AuthManager internal constructor(
}
}

result.exceptionOrNull()?.let { error ->
val wipeError = result.exceptionOrNull()
wipeError?.let { error ->
val lifecycle = (error as? AppException.WalletLifecycle)?.v1
if (lifecycle is WalletLifecycleFailure.ShutdownBlocked) {
wipePresentationState = WipePresentationState.ShutdownBlocked(lifecycle.attemptId)
} else {
android.util.Log.e(tag, "failed to wipe all data", error)
wipePresentationState =
WipePresentationState.Failed(error.message ?: "Unable to remove local data")
wipePresentationState = WipePresentationState.Failed
}
}

return UnlockMode.LOCKED
if (wipeError == null) {
runCatching { App.resetAfterWipe() }
.onFailure { error ->
android.util.Log.e(tag, "failed to reset app projection after wipe", error)
}

runCatching { refreshAuthStateAfterWipe() }
.onFailure { error ->
android.util.Log.e(tag, "failed to refresh authentication after wipe", error)
}

unlock()
wipePresentationState = WipePresentationState.Idle
}

val oldRust = rust
rust = RustAuthManager()
rustGuard.markOpen()
rust.listenForUpdates(this)
oldRust.close()
unlock()
type = AuthType.NONE
wipePresentationState = WipePresentationState.Idle
App.reset()
return if (wipeError == null) {
UnlockMode.WIPE
} else {
UnlockMode.LOCKED
}
}

return UnlockMode.WIPE
private fun refreshAuthStateAfterWipe() {
val nextType = readAuthStateAfterWipe("auth type", AuthType.NONE) { authType() }
val nextWipeDataPinEnabled =
readAuthStateAfterWipe("wipe PIN state", false) {
isWipeDataPinEnabled()
}
val nextDecoyPinEnabled =
readAuthStateAfterWipe("decoy PIN state", false) {
isDecoyPinEnabled()
}

type = nextType
isWipeDataPinEnabled = nextWipeDataPinEnabled
isDecoyPinEnabled = nextDecoyPinEnabled
isUsingBiometrics = false
}

private fun <T> readAuthStateAfterWipe(
name: String,
defaultValue: T,
read: RustAuthManager.() -> T,
): T =
runCatching { withRust(read) }
.getOrElse { error ->
android.util.Log.e(tag, "failed to refresh $name after wipe; using safe default", error)
defaultValue
}

private fun recordMainCredentialAuthentication() {
mainCredentialGeneration += 1
}

/**
* switch to main mode from decoy mode
*
* returns false when the switch failed so callers keep the app locked
* instead of unlocking into the decoy projection with the main credential
*/
fun switchToMainMode() {
fun switchToMainMode(): Boolean =
try {
withRust {
switchToMainMode()
}
resetAppAndSelectWallet()
true
} catch (e: Exception) {
android.util.Log.e(tag, "failed to switch to main mode", e)
false
}
}

override fun reconcile(message: AuthManagerReconcileMessage) {
logDebug("reconcile: $message")
Expand Down
7 changes: 4 additions & 3 deletions android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,10 @@ class MainActivity : FragmentActivity() {
return@setContent
}

if (bdkMigrationWarning != null) {
val app = remember { AppManager.getInstance() }
val auth = remember { AuthManager.getInstance() }

if (bdkMigrationWarning != null && auth.wipePresentationState != WipePresentationState.Running) {
AlertDialog(
onDismissRequest = { bdkMigrationWarning = null },
title = { Text("Encryption Migration Issue") },
Expand All @@ -506,8 +509,6 @@ class MainActivity : FragmentActivity() {
)
}

val app = remember { AppManager.getInstance() }
val auth = remember { AuthManager.getInstance() }
val snackbarHostState = remember { SnackbarHostState() }
var startupMode by remember {
mutableStateOf(resolveStartupMode(app.needsOnboarding))
Expand Down
Loading
Loading