Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
8e34516
WIP
praveenperera Nov 24, 2025
57672ca
WIP
praveenperera Nov 25, 2025
ada149b
Fix android side bar drag open
praveenperera Nov 25, 2025
a83d190
Fix android side bar drag open
praveenperera Nov 25, 2025
53ea223
Fix TransactionsDetailScreen android background colors
praveenperera Nov 25, 2025
f61c45e
Fix android sidebar buttons select
praveenperera Nov 25, 2025
71406f6
WIP
praveenperera Nov 25, 2025
bdf8f9b
Fix back button and side bar interaction on android
praveenperera Nov 25, 2025
4498d2f
Styling fixes and fix `loadAndReset` route on android
praveenperera Nov 26, 2025
92ebafa
Update CoveNavDisplay.kt
praveenperera Nov 26, 2025
768c41a
Make sidebar work with back navigation
praveenperera Nov 26, 2025
1e8dec9
Fixes
praveenperera Nov 26, 2025
b7f9695
Fix UTXO sort buttons and continue button state
praveenperera Nov 26, 2025
0d61edf
Create BitcoinShieldIcon.kt
praveenperera Nov 26, 2025
14482c5
Update ic_launcher_foreground.xml
praveenperera Nov 26, 2025
df7e296
Alert fixes, AutoSize fixes, other random UI fixes
praveenperera Nov 26, 2025
4e17020
Create a pin screen for Android.
praveenperera Nov 27, 2025
1c216a1
WIP
praveenperera Nov 28, 2025
3aa085b
Fix biometric prompt and reset stuck biometric flag
praveenperera Nov 28, 2025
4caad85
Move WalletManager FFI writes to background thread
praveenperera Nov 28, 2025
78fe49d
Add ioScope threading to Android managers
praveenperera Nov 28, 2025
db65b4f
Fix android locking and cover
praveenperera Nov 28, 2025
515aeaa
Update justfile
praveenperera Nov 28, 2025
eee0db0
Don't use dispatch in already dispatch
praveenperera Nov 28, 2025
3e6e352
WIP autosizetext
praveenperera Nov 28, 2025
b857257
WIP padding changes
praveenperera Nov 28, 2025
98a052d
Improve AutoSizeText accuracy and fix edge cases
praveenperera Nov 28, 2025
65834ac
Fix Android text spacing to match iOS
praveenperera Nov 28, 2025
240e56f
Avoid dispatch call in validate_metadata
praveenperera Nov 28, 2025
7772b62
Fix Android send flow unit switching (sats/btc)
praveenperera Nov 28, 2025
1a9639b
Fix privacy cover false positives from internal popups
praveenperera Nov 29, 2025
c0993bc
Add AutoSizeTextField for send amount input
praveenperera Nov 29, 2025
55224a1
Shorten justfile say commands
praveenperera Nov 29, 2025
6e3c523
Add fiat/BTC toggle and secondary unit to send flow
praveenperera Dec 2, 2025
b7ac233
Fix Android SendFlow button visibility and match iOS styling
praveenperera Dec 2, 2025
bd214f4
Fix Android send flow cursor position and add dynamic clear button
praveenperera Dec 2, 2025
ba19382
Fix custom fee rate sheet to match iOS
praveenperera Dec 2, 2025
4f9c938
Fix custom fee option not showing after Done pressed
praveenperera Dec 2, 2025
6c34c06
Show clear button only when field is focused
praveenperera Dec 2, 2025
0f35514
Add isShowingAlert property for alert dismissal handling
praveenperera Dec 2, 2025
e4162c6
Add colored dots to fee duration capsules and refine selector styling
praveenperera Dec 2, 2025
39f59eb
Clean up redundant iOS reference comments in SendFlowContainer
praveenperera Dec 2, 2025
4b126cd
Update HardwareExportScreen to match iOS styling
praveenperera Dec 2, 2025
8c91151
Add Typography.title3 extension and fix Android/iOS font parity
praveenperera Dec 2, 2025
626aa6d
Control QR code safe buffer entirely using padding
praveenperera Dec 2, 2025
0f60327
Set consistent font weight for transaction date
praveenperera Dec 2, 2025
72f3139
Set consistent font weight for transaction date
praveenperera Dec 2, 2025
0e59c0a
Update chain code pattern horizontal SVG
praveenperera Dec 2, 2025
d9bd48f
Add validation alert dialog to send flow
praveenperera Dec 2, 2025
618d93f
Add unsigned transactions widget to wallet screen
praveenperera Dec 2, 2025
f79025c
Remove duplicate approximate prefix from fiat amounts
praveenperera Dec 2, 2025
24cd68d
Fix HardwareExportScreen dark mode text and button padding
praveenperera Dec 2, 2025
add2547
Document Android/iOS platform behavior differences
praveenperera Dec 2, 2025
f03be24
Minor code formatting cleanup
praveenperera Dec 2, 2025
67e0a95
Fix Android transaction label editing and display
praveenperera Dec 2, 2025
2ddfbbf
Fix currency symbol in wallet header to respect settings
praveenperera Dec 2, 2025
2768327
Restyle Android settings screens with AOSP-style section headers
praveenperera Dec 2, 2025
0434fa2
Add NFC scanning feedback and wallet import success dialog
praveenperera Dec 2, 2025
dcd8a52
Fix NFC hardware wallet import to match iOS flow
praveenperera Dec 2, 2025
d084898
Refactor ImageButton with AutoSizeText and dynamic icon scaling
praveenperera Dec 2, 2025
8126f19
Fix HardwareExportScreen amount centering with dynamic offset
praveenperera Dec 2, 2025
780e097
Clean up SettingsScreen and change decoy PIN icon
praveenperera Dec 2, 2025
e4c30b8
Minor formatting: adjust color selector padding and fix imports
praveenperera Dec 2, 2025
99ba083
Remove network change tracking from AppManager
praveenperera Dec 2, 2025
432e3d8
Simplify ImageButton: remove dynamic icon scaling
praveenperera Dec 2, 2025
efcfbf3
Fix redirect to existing wallet after network change
praveenperera Dec 2, 2025
f9153ca
Android UI polish and add Compose iOS parity docs
praveenperera Dec 3, 2025
b2736f1
Update TapSigner flow to use EnterPin with Derive action
praveenperera Dec 3, 2025
02935ff
Fix styling on left aligned text buttons android
praveenperera Dec 4, 2025
1458c75
Use state machine in rust for verify words animation
praveenperera Dec 4, 2025
8c897f6
Fix merge conflicts and formatting
praveenperera Dec 4, 2025
bc7b565
Remove isPreview conditionals and preview code from Android views
praveenperera Dec 4, 2025
c54ee3d
Migrate Android QR scanning to Rust QrScanner state machine
praveenperera Dec 4, 2025
78a61fe
Update generated UniFFI bindings for QrScanner
praveenperera Dec 5, 2025
8a08d07
Fix `cove_ur` generation for android
praveenperera Dec 5, 2025
6345034
Fix android sidebar `Add Wallet` button
praveenperera Dec 5, 2025
2420eb4
Pre-load sidebar wallets on network and decoy mode changes
praveenperera Dec 5, 2025
e5a271c
Fix Android TapSigner PIN entry and add NFC scanning overlay
praveenperera Dec 5, 2025
8a5b98f
Rename parity doc and add NFC platform differences
praveenperera Dec 5, 2025
65fdef0
Fix NfcReader to call enableReaderMode on UI thread
praveenperera Dec 5, 2025
b8061e9
Use app-level QR/NFC scanners for hot wallet import
praveenperera Dec 5, 2025
d34ef38
Add error handling for invalid QR/NFC in iOS import screen
praveenperera Dec 5, 2025
010ab26
Add keyboard toolbar with Max button to Send Flow
praveenperera Dec 5, 2025
795bf87
Polish Android hot wallet and transaction UI
praveenperera Dec 5, 2025
f9e1240
Add reservedWidth to AutoSizeTextField
praveenperera Dec 5, 2025
fb75b9b
Replace tryhard with backon for retry logic
praveenperera Dec 4, 2025
1810dd0
Make fiat price lookup non-blocking
praveenperera Dec 4, 2025
a4a9cb2
Remove artificial delays when loading wallet
praveenperera Dec 4, 2025
650ae04
Show cached transactions immediately when switching wallets
praveenperera Dec 5, 2025
7054143
Update balance when fiat rates change
praveenperera Dec 5, 2025
c55dde6
Reduce loading delays on `load_and_reset`
praveenperera Dec 5, 2025
21ee671
Unify wallet mode change delay to 250ms
praveenperera Dec 5, 2025
317e59b
Add custom keyboard toolbar for iOS
praveenperera Dec 5, 2025
6fd3492
Bump iOS project version to 49
praveenperera Dec 8, 2025
b80ce5a
Merge branch 'master' into ui-bug-fix-import-wallet
praveenperera Dec 11, 2025
d7b5096
Update KeyboardObserver.swift
praveenperera Dec 11, 2025
c7c7a1c
Rebuild
praveenperera Dec 11, 2025
80270c6
Fix merge conflicts
praveenperera Dec 11, 2025
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
108 changes: 54 additions & 54 deletions android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1402,8 +1402,6 @@ external fun uniffi_cove_checksum_method_rustwalletmanager_amount_in_fiat(
): Short
external fun uniffi_cove_checksum_method_rustwalletmanager_balance(
): Short
external fun uniffi_cove_checksum_method_rustwalletmanager_balance_in_fiat(
): Short
external fun uniffi_cove_checksum_method_rustwalletmanager_broadcast_transaction(
): Short
external fun uniffi_cove_checksum_method_rustwalletmanager_convert_and_display_fiat(
Expand Down Expand Up @@ -2338,12 +2336,10 @@ external fun uniffi_cove_fn_constructor_rustwalletmanager_try_new_from_xpub(`xpu
): Long
external fun uniffi_cove_fn_method_rustwalletmanager_address_at(`ptr`: Long,`index`: Int,
): Long
external fun uniffi_cove_fn_method_rustwalletmanager_amount_in_fiat(`ptr`: Long,`amount`: Long,
): Long
external fun uniffi_cove_fn_method_rustwalletmanager_amount_in_fiat(`ptr`: Long,`amount`: Long,uniffi_out_err: UniffiRustCallStatus,
): RustBuffer.ByValue
external fun uniffi_cove_fn_method_rustwalletmanager_balance(`ptr`: Long,
): Long
external fun uniffi_cove_fn_method_rustwalletmanager_balance_in_fiat(`ptr`: Long,
): Long
external fun uniffi_cove_fn_method_rustwalletmanager_broadcast_transaction(`ptr`: Long,`signedTransaction`: Long,
): Long
external fun uniffi_cove_fn_method_rustwalletmanager_convert_and_display_fiat(`ptr`: Long,`amount`: Long,`prices`: Long,`withSuffix`: Byte,uniffi_out_err: UniffiRustCallStatus,
Expand Down Expand Up @@ -3328,7 +3324,7 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
if (lib.uniffi_cove_checksum_method_ffiapp_listen_for_updates() != 31459.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_cove_checksum_method_ffiapp_load_and_reset_default_route() != 39591.toShort()) {
if (lib.uniffi_cove_checksum_method_ffiapp_load_and_reset_default_route() != 12168.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_cove_checksum_method_ffiapp_load_and_reset_default_route_after() != 60004.toShort()) {
Expand Down Expand Up @@ -3757,15 +3753,12 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
if (lib.uniffi_cove_checksum_method_rustwalletmanager_address_at() != 13093.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_cove_checksum_method_rustwalletmanager_amount_in_fiat() != 35706.toShort()) {
if (lib.uniffi_cove_checksum_method_rustwalletmanager_amount_in_fiat() != 61774.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_cove_checksum_method_rustwalletmanager_balance() != 14970.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_cove_checksum_method_rustwalletmanager_balance_in_fiat() != 19007.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
if (lib.uniffi_cove_checksum_method_rustwalletmanager_broadcast_transaction() != 63043.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
Expand Down Expand Up @@ -8917,7 +8910,7 @@ public interface FfiAppInterface {
fun `listenForUpdates`(`updater`: FfiReconcile)

/**
* Load and reset the default route after 800ms delay
* Load and reset the default route after default delay
*/
fun `loadAndResetDefaultRoute`(`route`: Route)

Expand Down Expand Up @@ -9323,7 +9316,7 @@ open class FfiApp: Disposable, AutoCloseable, FfiAppInterface


/**
* Load and reset the default route after 800ms delay
* Load and reset the default route after default delay
*/override fun `loadAndResetDefaultRoute`(`route`: Route)
=
callWithHandle {
Expand Down Expand Up @@ -17894,12 +17887,13 @@ public interface RustWalletManagerInterface {
*/
suspend fun `addressAt`(`index`: kotlin.UInt): AddressInfo

suspend fun `amountInFiat`(`amount`: Amount): kotlin.Double
/**
* Sync method using cached prices, returns None if no cached prices
*/
fun `amountInFiat`(`amount`: Amount): kotlin.Double?

suspend fun `balance`(): Balance

suspend fun `balanceInFiat`(): kotlin.Double

suspend fun `broadcastTransaction`(`signedTransaction`: BitcoinTransaction)

fun `convertAndDisplayFiat`(`amount`: Amount, `prices`: PriceResponse, `withSuffix`: kotlin.Boolean = true): kotlin.String
Expand Down Expand Up @@ -18128,25 +18122,20 @@ open class RustWalletManager: Disposable, AutoCloseable, RustWalletManagerInterf
}


@Throws(WalletManagerException::class)
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
override suspend fun `amountInFiat`(`amount`: Amount) : kotlin.Double {
return uniffiRustCallAsync(
callWithHandle { uniffiHandle ->
UniffiLib.uniffi_cove_fn_method_rustwalletmanager_amount_in_fiat(
uniffiHandle,
FfiConverterTypeAmount.lower(`amount`),
)
},
{ future, callback, continuation -> UniffiLib.ffi_cove_rust_future_poll_f64(future, callback, continuation) },
{ future, continuation -> UniffiLib.ffi_cove_rust_future_complete_f64(future, continuation) },
{ future -> UniffiLib.ffi_cove_rust_future_free_f64(future) },
// lift function
{ FfiConverterDouble.lift(it) },
// Error FFI converter
WalletManagerException.ErrorHandler,
/**
* Sync method using cached prices, returns None if no cached prices
*/override fun `amountInFiat`(`amount`: Amount): kotlin.Double? {
return FfiConverterOptionalDouble.lift(
callWithHandle {
uniffiRustCall() { _status ->
UniffiLib.uniffi_cove_fn_method_rustwalletmanager_amount_in_fiat(
it,
FfiConverterTypeAmount.lower(`amount`),_status)
}
}
)
}



@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
Expand All @@ -18169,27 +18158,6 @@ open class RustWalletManager: Disposable, AutoCloseable, RustWalletManagerInterf
}


@Throws(WalletManagerException::class)
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
override suspend fun `balanceInFiat`() : kotlin.Double {
return uniffiRustCallAsync(
callWithHandle { uniffiHandle ->
UniffiLib.uniffi_cove_fn_method_rustwalletmanager_balance_in_fiat(
uniffiHandle,

)
},
{ future, callback, continuation -> UniffiLib.ffi_cove_rust_future_poll_f64(future, callback, continuation) },
{ future, continuation -> UniffiLib.ffi_cove_rust_future_complete_f64(future, continuation) },
{ future -> UniffiLib.ffi_cove_rust_future_free_f64(future) },
// lift function
{ FfiConverterDouble.lift(it) },
// Error FFI converter
WalletManagerException.ErrorHandler,
)
}


@Throws(WalletManagerException::class)
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
override suspend fun `broadcastTransaction`(`signedTransaction`: BitcoinTransaction) {
Expand Down Expand Up @@ -42547,6 +42515,38 @@ public object FfiConverterOptionalFloat: FfiConverterRustBuffer<kotlin.Float?> {



/**
* @suppress
*/
public object FfiConverterOptionalDouble: FfiConverterRustBuffer<kotlin.Double?> {
override fun read(buf: ByteBuffer): kotlin.Double? {
if (buf.get().toInt() == 0) {
return null
}
return FfiConverterDouble.read(buf)
}

override fun allocationSize(value: kotlin.Double?): ULong {
if (value == null) {
return 1UL
} else {
return 1UL + FfiConverterDouble.allocationSize(value)
}
}

override fun write(value: kotlin.Double?, buf: ByteBuffer) {
if (value == null) {
buf.put(0)
} else {
buf.put(1)
FfiConverterDouble.write(value, buf)
}
}
}




/**
* @suppress
*/
Expand Down
21 changes: 19 additions & 2 deletions ios/Cove.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
AC85E3B52EE343C300BCEE4B /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = AC85E3B42EE343C300BCEE4B /* SwiftUIIntrospect */; };
AC9AB5792C34956C00BAE3BE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9AB5782C34956C00BAE3BE /* Logger.swift */; };
ACA4CBEF2C2365430008F63E /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = ACA4CBEE2C2365430008F63E /* KeychainSwift */; };
ACC61E2F2ECBD8BE00D13947 /* MijickPopups in Frameworks */ = {isa = PBXBuildFile; productRef = ACC61E2E2ECBD8BE00D13947 /* MijickPopups */; };
Expand Down Expand Up @@ -54,6 +55,7 @@
ACD0053C2DB5309A003DC493 /* CoveCore in Frameworks */,
ACA4CBEF2C2365430008F63E /* KeychainSwift in Frameworks */,
ACC61E2F2ECBD8BE00D13947 /* MijickPopups in Frameworks */,
AC85E3B52EE343C300BCEE4B /* SwiftUIIntrospect in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -103,6 +105,7 @@
ACA4CBEE2C2365430008F63E /* KeychainSwift */,
ACD0053B2DB5309A003DC493 /* CoveCore */,
ACC61E2E2ECBD8BE00D13947 /* MijickPopups */,
AC85E3B42EE343C300BCEE4B /* SwiftUIIntrospect */,
);
productName = Cove;
productReference = D0245F4C2C0F7B4E0042B447 /* Cove.app */;
Expand Down Expand Up @@ -136,6 +139,7 @@
ACA4CBED2C2365430008F63E /* XCRemoteSwiftPackageReference "keychain-swift" */,
ACD0053A2DB5309A003DC493 /* XCLocalSwiftPackageReference "CoveCore" */,
ACC61E2D2ECBD8BE00D13947 /* XCRemoteSwiftPackageReference "Popups" */,
AC85E3B32EE343C300BCEE4B /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
);
productRefGroup = D0245F4D2C0F7B4E0042B447 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -302,7 +306,7 @@
CODE_SIGN_ENTITLEMENTS = Cove/Cove.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_ASSET_PATHS = "\"Cove/Preview Content\"";
DEVELOPMENT_TEAM = Q8UP8C53Y8;
ENABLE_APP_SANDBOX = YES;
Expand Down Expand Up @@ -352,7 +356,7 @@
CODE_SIGN_ENTITLEMENTS = Cove/Cove.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_ASSET_PATHS = "\"Cove/Preview Content\"";
DEVELOPMENT_TEAM = Q8UP8C53Y8;
ENABLE_APP_SANDBOX = YES;
Expand Down Expand Up @@ -424,6 +428,14 @@
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
AC85E3B32EE343C300BCEE4B /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/siteline/swiftui-introspect";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 26.0.0;
};
};
ACA4CBED2C2365430008F63E /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/evgenyneu/keychain-swift";
Expand All @@ -443,6 +455,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
AC85E3B42EE343C300BCEE4B /* SwiftUIIntrospect */ = {
isa = XCSwiftPackageProductDependency;
package = AC85E3B32EE343C300BCEE4B /* XCRemoteSwiftPackageReference "swiftui-introspect" */;
productName = SwiftUIIntrospect;
};
ACA4CBEE2C2365430008F63E /* KeychainSwift */ = {
isa = XCSwiftPackageProductDependency;
package = ACA4CBED2C2365430008F63E /* XCRemoteSwiftPackageReference "keychain-swift" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion ios/Cove/AppManager.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Observation
import SwiftUI

private let walletModeChangeDelayMs = 250

@Observable final class AppManager: FfiReconcile {
static let shared = AppManager()

Expand Down Expand Up @@ -239,6 +241,7 @@ import SwiftUI

case let .fiatPricesChanged(prices):
self.prices = prices
self.walletManager?.updateFiatBalance()

case let .feesChanged(fees):
self.fees = fees
Expand All @@ -262,7 +265,7 @@ import SwiftUI
self.loadWallets()

Task {
try? await Task.sleep(for: .milliseconds(200))
try? await Task.sleep(for: .milliseconds(walletModeChangeDelayMs))
await MainActor.run {
withAnimation { self.isLoading = false }
}
Expand Down
49 changes: 46 additions & 3 deletions ios/Cove/Components/KeyboardAccessory/KeyboardAccessoryHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,36 @@ final class KeyboardAccessoryController: ObservableObject {
private var container: UIView?
private weak var currentResponder: UIView?
private var isAttached: Bool = false
private var notificationObserver: NSObjectProtocol?
private var didBecomeActiveObserver: NSObjectProtocol?
private var keyboardDidShowObserver: NSObjectProtocol?
private var keyboardWillHideObserver: NSObjectProtocol?

init() {
notificationObserver = NotificationCenter.default.addObserver(
didBecomeActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.reattachIfNeeded()
}

keyboardDidShowObserver = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.reattachOnKeyboardShow()
}

keyboardWillHideObserver = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: .main
) { [weak self] _ in
// mark as detached so we know to reattach when keyboard shows again
self?.isAttached = false
self?.currentResponder = nil
}
}

/// Reattach the accessory view when app returns from background/notification center.
Expand All @@ -66,8 +86,31 @@ final class KeyboardAccessoryController: ObservableObject {
}
}

/// Reattach accessory when keyboard appears after focus transitions.
/// Handles the case where first responder changes between text fields.
private func reattachOnKeyboardShow() {
guard let container = self.container else { return }

// re-capture first responder
UIResponder.captureCurrentFirstResponder(from: nil)
guard let responder = UIResponder.currentFirstResponderView else { return }

// always reattach when keyboard shows (we track hide state via keyboardWillHide)
// force rebuild: clear first, then re-set
self.setAccessory(on: responder, accessoryView: nil, forceReload: false)
self.setAccessory(on: responder, accessoryView: container, forceReload: true)
self.currentResponder = responder
self.isAttached = true
}

deinit {
if let observer = notificationObserver {
if let observer = didBecomeActiveObserver {
NotificationCenter.default.removeObserver(observer)
}
if let observer = keyboardDidShowObserver {
NotificationCenter.default.removeObserver(observer)
}
if let observer = keyboardWillHideObserver {
NotificationCenter.default.removeObserver(observer)
}
}
Expand Down
Loading
Loading