Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6bc179c
Fix fee tier calculation inflating rates in low-fee environments (#605)
praveenperera Feb 25, 2026
78e156d
Respect API minimum_fee in fee rates
praveenperera Feb 25, 2026
28f1a9b
Fix descriptor export for Sparrow compatibility (#609)
praveenperera Mar 3, 2026
eef61d8
Set iOS target and switch reqwest feature
praveenperera Mar 4, 2026
483010e
Remove cove-cspp from workspace deps
praveenperera Mar 4, 2026
ddcf9a6
Use rand::RngExt in cove-util
praveenperera Mar 6, 2026
e371aff
Fix rand 0.10 trait imports across crates
praveenperera Mar 6, 2026
aaf3616
Fix Android HotWalletKeyMissing nav loop, button order, and manager c…
praveenperera Mar 6, 2026
782133a
Add rustls and install ring provider
praveenperera Mar 6, 2026
2cd342b
Bump app version to 1.2.2
praveenperera Mar 6, 2026
eb04317
Use webpki-roots for reqwest TLS instead of rustls-platform-verifier
praveenperera Mar 6, 2026
c494959
Improve scroll-based toolbar color tracking with stable coordinates
praveenperera Mar 14, 2026
b7c3182
Fix sidebar not closing when tapping buttons with slight finger movement
praveenperera Mar 14, 2026
19e0c0c
Defer route reset & fix color normalization
praveenperera Mar 16, 2026
49a661f
Replace .principal toolbar with UIKit titleView to fix iOS 26 freeze
praveenperera Mar 16, 2026
25d5cac
Replace remaining .principal toolbar items with .navigationTitle
praveenperera Mar 16, 2026
fe2a4df
Bump project CURRENT_PROJECT_VERSION to 65
praveenperera Mar 16, 2026
a0d58d8
Fix Android app for GrapheneOS and degoogled phones
praveenperera Mar 17, 2026
e22f6ca
Propagate initial wallet load state from Rust
praveenperera Mar 17, 2026
cfd9e4c
Use UIColor.label for popover tint
praveenperera Mar 17, 2026
45c55e0
Bump iOS project version to 66
praveenperera Mar 17, 2026
8006659
Update proguard-rules.pro
praveenperera Mar 17, 2026
7956555
Update build.gradle.kts
praveenperera Mar 17, 2026
70ae098
Merge master into release/v1.2.2
praveenperera Mar 27, 2026
f715054
Fix wallet manager startup and teardown
praveenperera Mar 28, 2026
adc9d5b
Fix validated review findings
praveenperera Mar 28, 2026
b3200b6
Enforce bootstrap-first app initialization
praveenperera Mar 29, 2026
17c85b2
Update Cargo.lock: bump deps and uniffi rev
praveenperera Mar 30, 2026
63c0e01
Use factory function for TapSignerReader FFI creation
praveenperera Mar 30, 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
10 changes: 10 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,13 @@ This pattern keeps business logic and validation centralized in Rust while givin
- Swift bindings land in `ios/CoveCore/Sources/CoveCore/generated/`; Kotlin bindings live (after copy) under `android/app/src/main/java/org/bitcoinppl/cove_core/`.
- Database file defaults to `$ROOT_DATA_DIR/cove.db` (see `cove_common::consts::ROOT_DATA_DIR`).
- Hardware / NFC helpers: `rust/crates/cove-device`, `rust/src/tap_card/`, and platform shims in `ios/Cove/FFI/` plus Android's `org.bitcoinppl.cove_core` package.

---

## iOS 26 SwiftUI Bugs & Workarounds

### ToolbarItem(placement: .principal) — DO NOT USE

SwiftUI has a bug on iOS 26 where `ToolbarPlacementEnvironment.updateValue()` enters an infinite loop during `_UINavigationParallaxTransition` (back button / swipe back) when a `.principal` toolbar item exists at large accessibility font sizes (specifically `accessibilityExtraExtraLarge`). This causes 100% CPU freeze.

**Workaround:** For screens that need custom title content (icons, context menus, dynamic colors), use `.navigationTitleView { }` from `ios/Cove/Views/NavigationTitleView.swift`. This hosts SwiftUI content inside UIKit's `navigationItem.titleView`, bypassing SwiftUI's toolbar placement system entirely. For screens with plain static text titles, use `.navigationTitle("Title").navigationBarTitleDisplayMode(.inline)` instead.
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ dependencies {
implementation("androidx.camera:camera-lifecycle:1.5.1")
implementation("androidx.camera:camera-view:1.5.1")
implementation("com.google.accompanist:accompanist-permissions:0.37.3")
implementation("com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1")
implementation("com.google.mlkit:barcode-scanning:17.3.0")
implementation("com.google.zxing:core:3.5.4")

// Coil for image loading (including SVG support)
Expand Down
3 changes: 3 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
volatile <fields>;
}

# ML Kit bundled variant: keep registrar constructors used via reflection by ComponentDiscovery
-keep class com.google.mlkit.** { *; }

# Preserve line numbers for debugging crash reports
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
12 changes: 11 additions & 1 deletion android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -670,9 +670,19 @@ class AppManager private constructor() : FfiReconcile {
*/
private const val MIN_LOADING_VISIBILITY_MS = 200L

private fun requireBootstrapComplete(owner: String) {
val step = bootstrapProgress()
check(step == BootstrapStep.COMPLETE) {
"$owner initialized before bootstrap completed: $step"
}
}

fun getInstance(): AppManager =
instance ?: synchronized(this) {
instance ?: AppManager().also { instance = it }
instance ?: run {
requireBootstrapComplete("AppManager")
AppManager().also { instance = it }
}
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion android/app/src/main/java/org/bitcoinppl/cove/AuthManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,19 @@ class AuthManager private constructor() : AuthManagerReconciler {
@Volatile
private var instance: AuthManager? = null

private fun requireBootstrapComplete(owner: String) {
val step = bootstrapProgress()
check(step == BootstrapStep.COMPLETE) {
"$owner initialized before bootstrap completed: $step"
}
}

fun getInstance(): AuthManager =
instance ?: synchronized(this) {
instance ?: AuthManager().also { instance = it }
instance ?: run {
requireBootstrapComplete("AuthManager")
AuthManager().also { instance = it }
}
}
}

Expand Down
12 changes: 6 additions & 6 deletions android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -546,16 +546,12 @@ private fun GlobalAlertDialog(
Column(horizontalAlignment = Alignment.End) {
TextButton(onClick = {
onDismiss()
app.pushRoute(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWELVE, ImportType.MANUAL))))
app.loadAndReset(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWELVE, ImportType.MANUAL))))
}) { Text("Import 12 Words") }
TextButton(onClick = {
onDismiss()
app.pushRoute(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWENTY_FOUR, ImportType.MANUAL))))
app.loadAndReset(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWENTY_FOUR, ImportType.MANUAL))))
}) { Text("Import 24 Words") }
TextButton(onClick = {
onDismiss()
app.alertState = TaggedItem(AppAlertState.ConfirmWatchOnly)
}) { Text("Use as Watch Only") }
TextButton(onClick = {
onDismiss()
try {
Expand All @@ -571,6 +567,10 @@ private fun GlobalAlertDialog(
)
}
}) { Text("Use with Hardware Wallet") }
TextButton(onClick = {
onDismiss()
app.alertState = TaggedItem(AppAlertState.ConfirmWatchOnly)
}) { Text("Use as Watch Only") }
}
},
)
Expand Down
47 changes: 31 additions & 16 deletions android/app/src/main/java/org/bitcoinppl/cove/Security.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package org.bitcoinppl.cove

import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import org.bitcoinppl.cove_core.device.DeviceAccess
import org.bitcoinppl.cove_core.device.KeychainAccess
import org.bitcoinppl.cove_core.device.KeychainException
import java.security.GeneralSecurityException
import java.util.TimeZone

class KeychainAccessor(
Expand All @@ -15,23 +18,15 @@ class KeychainAccessor(
private val sharedPreferences: SharedPreferences

init {
// create or retrieve the master key for encryption
val masterKey =
MasterKey
.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setRequestStrongBoxBacked(true)
.build()

// create encrypted shared preferences
val useStrongBox = hasStrongBox(context)
sharedPreferences =
EncryptedSharedPreferences.create(
context,
"cove_secure_storage",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
try {
createEncryptedPrefs(context, requestStrongBox = useStrongBox)
} catch (e: GeneralSecurityException) {
if (!useStrongBox) throw e // not a StrongBox issue, no fallback available
Log.w("KeychainAccessor", "StrongBox-backed prefs failed, falling back to TEE", e)
createEncryptedPrefs(context, requestStrongBox = false)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

override fun save(key: String, value: String) {
Expand All @@ -55,6 +50,26 @@ class KeychainAccessor(
.commit()
}

private fun hasStrongBox(context: Context): Boolean =
context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)

private fun createEncryptedPrefs(context: Context, requestStrongBox: Boolean): SharedPreferences {
val masterKey =
MasterKey
.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setRequestStrongBoxBacked(requestStrongBox)
.build()

return EncryptedSharedPreferences.create(
context,
"cove_secure_storage",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
}

class DeviceAccessor : DeviceAccess {
override fun timezone(): String = TimeZone.getDefault().id
}
13 changes: 12 additions & 1 deletion android/app/src/main/java/org/bitcoinppl/cove/WalletManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ class WalletManager :
this.walletMetadata = metadata
this.unsignedTransactions = runCatching { rustManager.getUnsignedTransactions() }.getOrElse { emptyList() }

// set initial load state from Rust cached data
loadState = when (val rustLoadState = rustManager.initialLoadState()) {
is org.bitcoinppl.cove_core.WalletLoadState.Loading -> WalletLoadState.LOADING
is org.bitcoinppl.cove_core.WalletLoadState.Scanning -> WalletLoadState.SCANNING(rustLoadState.v1)
is org.bitcoinppl.cove_core.WalletLoadState.Loaded -> WalletLoadState.LOADED(rustLoadState.v1)
}

rustManager.listenForUpdates(this)
}

Expand Down Expand Up @@ -202,7 +209,11 @@ class WalletManager :
private fun apply(message: WalletManagerReconcileMessage) {
when (message) {
is WalletManagerReconcileMessage.StartedInitialFullScan -> {
loadState = WalletLoadState.LOADING
when (val current = loadState) {
is WalletLoadState.SCANNING -> if (current.txns.isEmpty()) loadState = WalletLoadState.LOADING
is WalletLoadState.LOADED -> loadState = WalletLoadState.SCANNING(current.txns)
else -> loadState = WalletLoadState.LOADING
}
}

is WalletManagerReconcileMessage.StartedExpandedFullScan -> {
Expand Down
Loading
Loading