Skip to content

Commit f3e3e6a

Browse files
committed
fix: surface hw connect failures
1 parent ee99421 commit f3e3e6a

7 files changed

Lines changed: 145 additions & 17 deletions

File tree

app/src/main/java/to/bitkit/services/TrezorTransport.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
4040
import kotlinx.coroutines.flow.SharedFlow
4141
import kotlinx.coroutines.flow.StateFlow
4242
import kotlinx.coroutines.flow.update
43+
import kotlinx.coroutines.sync.Mutex
44+
import kotlinx.coroutines.sync.withLock
4345
import to.bitkit.ext.bluetoothManager
4446
import to.bitkit.ext.usbManager
4547
import to.bitkit.models.TransportType
@@ -205,6 +207,7 @@ class TrezorTransport @Inject constructor(
205207

206208
private val bleConnections = ConcurrentHashMap<String, BleConnection>()
207209
private val discoveredBleDevices = ConcurrentHashMap<String, BluetoothDevice>()
210+
private val optionScopeMutex = Mutex()
208211

209212
private data class UsbOpenDevice(
210213
val connection: UsbDeviceConnection,
@@ -225,20 +228,26 @@ class TrezorTransport @Inject constructor(
225228
@Volatile var writeStatus: Int = BluetoothGatt.GATT_SUCCESS,
226229
)
227230

228-
suspend fun <T> withUsbPermissionRequestsEnabled(enabled: Boolean, block: suspend () -> T): T {
231+
suspend fun <T> withUsbPermissionRequestsEnabled(
232+
enabled: Boolean,
233+
block: suspend () -> T,
234+
): T = optionScopeMutex.withLock {
229235
val previous = requestUsbPermissionEnabled
230236
requestUsbPermissionEnabled = enabled
231-
return try {
237+
try {
232238
block()
233239
} finally {
234240
requestUsbPermissionEnabled = previous
235241
}
236242
}
237243

238-
suspend fun <T> withBluetoothScanningEnabled(enabled: Boolean, block: suspend () -> T): T {
244+
suspend fun <T> withBluetoothScanningEnabled(
245+
enabled: Boolean,
246+
block: suspend () -> T,
247+
): T = optionScopeMutex.withLock {
239248
val previous = bluetoothScanningEnabled
240249
bluetoothScanningEnabled = enabled
241-
return try {
250+
try {
242251
block()
243252
} finally {
244253
bluetoothScanningEnabled = previous

app/src/main/java/to/bitkit/ui/sheets/hardware/HardwareSheet.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ fun HardwareSheet(
8888
}
8989
}
9090
val blePermissions = rememberMultiplePermissionsState(bluetoothPermissions) { results ->
91-
if (results.values.all { it }) enableBleScanning()
91+
if (results.values.all { it }) {
92+
enableBleScanning()
93+
} else {
94+
showBlePermissionDialog = true
95+
}
9296
}
9397
val requestBleAccess: () -> Unit = {
9498
when {
@@ -135,7 +139,10 @@ fun HardwareSheet(
135139
)
136140
}
137141
composableWithDefaultTransitions<HardwareRoute.Searching> {
138-
HwSearchingSheet(onCancel = appViewModel::hideSheet)
142+
HwSearchingSheet(
143+
errorMessage = uiState.errorMessage,
144+
onCancel = appViewModel::hideSheet,
145+
)
139146
}
140147
composableWithDefaultTransitions<HardwareRoute.Found> { backStackEntry ->
141148
val route = backStackEntry.toRoute<HardwareRoute.Found>()
@@ -151,6 +158,7 @@ fun HardwareSheet(
151158
HwFoundSheet(
152159
deviceModel = deviceModel,
153160
isConnecting = uiState.isConnecting,
161+
errorMessage = uiState.errorMessage,
154162
onConnect = { viewModel.onConnectClick(route.deviceId) },
155163
onCancel = appViewModel::hideSheet,
156164
)
@@ -199,7 +207,12 @@ private fun ConnectEffectHandler(
199207
viewModel.effects.collect { effect ->
200208
when (effect) {
201209
HwConnectEffect.NavigateToSearching -> navController.navigateTo(HardwareRoute.Searching)
202-
HwConnectEffect.NavigateToFound -> navController.navigateTo(HardwareRoute.Found())
210+
is HwConnectEffect.NavigateToFound -> navController.navigateTo(
211+
HardwareRoute.Found(
212+
deviceId = effect.deviceId,
213+
deviceModel = effect.deviceModel,
214+
),
215+
)
203216
HwConnectEffect.NavigateToPairCode -> navController.navigateTo(HardwareRoute.PairCode)
204217
HwConnectEffect.NavigateToPaired -> navController.navigateTo(HardwareRoute.Paired)
205218
HwConnectEffect.Dismiss -> appViewModel.hideSheet()

app/src/main/java/to/bitkit/ui/sheets/hardware/HwConnectViewModel.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package to.bitkit.ui.sheets.hardware
22

3+
import android.content.Context
34
import androidx.compose.runtime.Immutable
45
import androidx.lifecycle.ViewModel
56
import androidx.lifecycle.viewModelScope
67
import com.synonym.bitkitcore.TrezorFeatures
78
import dagger.hilt.android.lifecycle.HiltViewModel
9+
import dagger.hilt.android.qualifiers.ApplicationContext
810
import kotlinx.coroutines.Job
911
import kotlinx.coroutines.delay
1012
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow
1416
import kotlinx.coroutines.flow.update
1517
import kotlinx.coroutines.isActive
1618
import kotlinx.coroutines.launch
19+
import to.bitkit.R
1720
import to.bitkit.repositories.HwWalletRepo
1821
import to.bitkit.repositories.HwWalletRepo.Companion.DEVICE_LABEL_MAX_LENGTH
1922
import to.bitkit.repositories.resolveHwWalletName
@@ -30,6 +33,7 @@ import kotlin.time.Duration.Companion.seconds
3033
@HiltViewModel
3134
class HwConnectViewModel @Inject constructor(
3235
private val hwWalletRepo: HwWalletRepo,
36+
@ApplicationContext private val context: Context,
3337
) : ViewModel() {
3438
companion object {
3539
/** Delay between scan attempts while searching for a nearby device. */
@@ -54,6 +58,7 @@ class HwConnectViewModel @Inject constructor(
5458

5559
fun onIntroContinue(includeBluetooth: Boolean = true) {
5660
includeBluetoothInScan = includeBluetooth
61+
_uiState.update { it.copy(errorMessage = null) }
5762
setEffect(HwConnectEffect.NavigateToSearching)
5863
startSearching()
5964
}
@@ -71,6 +76,7 @@ class HwConnectViewModel @Inject constructor(
7176
isSearching = false,
7277
foundDeviceId = deviceId,
7378
deviceModel = deviceModel.ifBlank { resolveHwWalletName(label = null, model = null) },
79+
errorMessage = null,
7480
)
7581
}
7682
scanUsbBeforeConnect = true
@@ -81,14 +87,21 @@ class HwConnectViewModel @Inject constructor(
8187
val deviceId = deviceIdOverride ?: state.foundDeviceId ?: return
8288
val shouldScanUsbBeforeConnect = scanUsbBeforeConnect
8389
searchJob?.cancel()
84-
_uiState.update { it.copy(isConnecting = true) }
90+
_uiState.update { it.copy(isConnecting = true, errorMessage = null) }
8591
viewModelScope.launch {
8692
if (shouldScanUsbBeforeConnect) {
8793
hwWalletRepo.scan(includeBluetooth = false)
8894
}
8995
hwWalletRepo.connect(deviceId)
9096
.onSuccess { onConnected(deviceId, it) }
91-
.onFailure { _uiState.update { state -> state.copy(isConnecting = false) } }
97+
.onFailure {
98+
_uiState.update { state ->
99+
state.copy(
100+
isConnecting = false,
101+
errorMessage = context.getString(R.string.hardware__connect_error),
102+
)
103+
}
104+
}
92105
}
93106
}
94107

@@ -118,20 +131,30 @@ class HwConnectViewModel @Inject constructor(
118131
private fun startSearching() {
119132
if (searchJob?.isActive == true) return
120133
scanUsbBeforeConnect = false
121-
_uiState.update { it.copy(isSearching = true) }
134+
_uiState.update { it.copy(isSearching = true, errorMessage = null) }
122135
searchJob = viewModelScope.launch {
123136
while (isActive) {
124-
hwWalletRepo.scan(includeBluetooth = includeBluetoothInScan)
137+
val scanResult = hwWalletRepo.scan(includeBluetooth = includeBluetoothInScan)
138+
if (scanResult.isFailure) {
139+
_uiState.update {
140+
it.copy(errorMessage = context.getString(R.string.hardware__search_error))
141+
}
142+
delay(SCAN_INTERVAL)
143+
continue
144+
}
145+
_uiState.update { it.copy(errorMessage = null) }
125146
val device = hwWalletRepo.deviceState.value.nearbyDevices.firstOrNull()
126147
if (device != null) {
148+
val deviceModel = resolveHwWalletName(label = null, model = device.model)
127149
_uiState.update {
128150
it.copy(
129151
isSearching = false,
130152
foundDeviceId = device.id,
131-
deviceModel = resolveHwWalletName(label = null, model = device.model),
153+
deviceModel = deviceModel,
154+
errorMessage = null,
132155
)
133156
}
134-
setEffect(HwConnectEffect.NavigateToFound)
157+
setEffect(HwConnectEffect.NavigateToFound(device.id, deviceModel))
135158
return@launch
136159
}
137160
delay(SCAN_INTERVAL)
@@ -147,6 +170,7 @@ class HwConnectViewModel @Inject constructor(
147170
pairedDeviceId = deviceId,
148171
deviceName = name,
149172
labelInput = if (labelInitialized) it.labelInput else name,
173+
errorMessage = null,
150174
)
151175
}
152176
labelInitialized = true
@@ -191,11 +215,12 @@ data class HwConnectUiState(
191215
val deviceModel: String = "",
192216
val balanceSats: ULong = 0uL,
193217
val labelInput: String = "",
218+
val errorMessage: String? = null,
194219
)
195220

196221
sealed interface HwConnectEffect {
197222
data object NavigateToSearching : HwConnectEffect
198-
data object NavigateToFound : HwConnectEffect
223+
data class NavigateToFound(val deviceId: String, val deviceModel: String) : HwConnectEffect
199224
data object NavigateToPairCode : HwConnectEffect
200225
data object NavigateToPaired : HwConnectEffect
201226
data object Dismiss : HwConnectEffect

app/src/main/java/to/bitkit/ui/sheets/hardware/HwFoundSheet.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package to.bitkit.ui.sheets.hardware
22

3+
import androidx.compose.animation.AnimatedVisibility
34
import androidx.compose.foundation.Image
45
import androidx.compose.foundation.layout.Arrangement
56
import androidx.compose.foundation.layout.Box
@@ -20,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
2021
import androidx.compose.ui.unit.dp
2122
import to.bitkit.R
2223
import to.bitkit.ui.components.BodyM
24+
import to.bitkit.ui.components.BodyS
2325
import to.bitkit.ui.components.BottomSheetPreview
2426
import to.bitkit.ui.components.Display
2527
import to.bitkit.ui.components.PrimaryButton
@@ -37,12 +39,14 @@ fun HwFoundSheet(
3739
deviceModel: String,
3840
modifier: Modifier = Modifier,
3941
isConnecting: Boolean = false,
42+
errorMessage: String? = null,
4043
onConnect: () -> Unit = {},
4144
onCancel: () -> Unit = {},
4245
) {
4346
Content(
4447
deviceModel = deviceModel,
4548
isConnecting = isConnecting,
49+
errorMessage = errorMessage,
4650
onConnect = onConnect,
4751
onCancel = onCancel,
4852
modifier = modifier
@@ -54,6 +58,7 @@ private fun Content(
5458
deviceModel: String,
5559
modifier: Modifier = Modifier,
5660
isConnecting: Boolean = false,
61+
errorMessage: String? = null,
5762
onConnect: () -> Unit = {},
5863
onCancel: () -> Unit = {},
5964
) {
@@ -73,6 +78,16 @@ private fun Content(
7378
Display(stringResource(R.string.hardware__found_header).withAccent(accentColor = Colors.Blue))
7479
VerticalSpacer(8.dp)
7580
BodyM(stringResource(R.string.hardware__found_text, deviceModel), color = Colors.White64)
81+
AnimatedVisibility(visible = errorMessage != null) {
82+
Column {
83+
VerticalSpacer(16.dp)
84+
BodyS(
85+
text = errorMessage.orEmpty(),
86+
color = Colors.Red,
87+
modifier = Modifier.testTag("HwFoundError")
88+
)
89+
}
90+
}
7691
}
7792
Box(
7893
contentAlignment = Alignment.Center,

app/src/main/java/to/bitkit/ui/sheets/hardware/HwSearchingSheet.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package to.bitkit.ui.sheets.hardware
22

3+
import androidx.compose.animation.AnimatedVisibility
34
import androidx.compose.animation.core.InfiniteTransition
45
import androidx.compose.animation.core.LinearEasing
56
import androidx.compose.animation.core.animateFloat
@@ -26,6 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview
2627
import androidx.compose.ui.unit.dp
2728
import to.bitkit.R
2829
import to.bitkit.ui.components.BodyM
30+
import to.bitkit.ui.components.BodyS
2931
import to.bitkit.ui.components.BottomSheetPreview
3032
import to.bitkit.ui.components.Display
3133
import to.bitkit.ui.components.SecondaryButton
@@ -55,9 +57,11 @@ private const val RING_SPIN_MS = 2000
5557
@Composable
5658
fun HwSearchingSheet(
5759
modifier: Modifier = Modifier,
60+
errorMessage: String? = null,
5861
onCancel: () -> Unit = {},
5962
) {
6063
Content(
64+
errorMessage = errorMessage,
6165
onCancel = onCancel,
6266
modifier = modifier
6367
)
@@ -66,6 +70,7 @@ fun HwSearchingSheet(
6670
@Composable
6771
private fun Content(
6872
modifier: Modifier = Modifier,
73+
errorMessage: String? = null,
6974
onCancel: () -> Unit = {},
7075
) {
7176
Column(
@@ -84,6 +89,16 @@ private fun Content(
8489
Display(stringResource(R.string.hardware__connect_header).withAccent(accentColor = Colors.Blue))
8590
VerticalSpacer(8.dp)
8691
BodyM(stringResource(R.string.hardware__connect_text), color = Colors.White64)
92+
AnimatedVisibility(visible = errorMessage != null) {
93+
Column {
94+
VerticalSpacer(16.dp)
95+
BodyS(
96+
text = errorMessage.orEmpty(),
97+
color = Colors.Red,
98+
modifier = Modifier.testTag("HwSearchingError")
99+
)
100+
}
101+
}
87102
}
88103
Box(
89104
contentAlignment = Alignment.Center,

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
<string name="hardware__bluetooth_permission_settings">Open Settings</string>
168168
<string name="hardware__bluetooth_permission_text">Bitkit needs the nearby-devices (Bluetooth) permission to find your hardware wallet. Enable it in Settings, then try again.</string>
169169
<string name="hardware__bluetooth_permission_title">Bluetooth access needed</string>
170+
<string name="hardware__connect_error">Could not connect to your Trezor. Check that it is unlocked and try again.</string>
170171
<string name="hardware__connect_header">Searching for &lt;accent&gt;devices&lt;/accent&gt;</string>
171172
<string name="hardware__connect_text">Please connect your hardware wallet now via USB or Bluetooth.</string>
172173
<string name="hardware__connect_title">Connect Device</string>
@@ -192,6 +193,7 @@
192193
<string name="hardware__remove_dialog_text">Don\'t worry, your funds are safe and your coins won\'t be deleted. Bitkit will simply stop displaying the amounts in the wallet.</string>
193194
<string name="hardware__remove_dialog_title">Remove %1$s</string>
194195
<string name="hardware__remove_error">Could not remove the hardware wallet. Please try again.</string>
196+
<string name="hardware__search_error">Could not search for hardware wallets. Check your connection and try again.</string>
195197
<string name="lightning__availability__text">Funds transfer to savings is usually instant, but settlement may take up to &lt;accent&gt;14 days&lt;/accent&gt; under certain network conditions.</string>
196198
<string name="lightning__availability__title">Funds\n&lt;accent&gt;availability&lt;/accent&gt;</string>
197199
<string name="lightning__balance">Balance</string>

0 commit comments

Comments
 (0)