Skip to content

Commit aaef6d2

Browse files
committed
Release 5.2.0
1 parent 3d2210f commit aaef6d2

File tree

28 files changed

+246
-135
lines changed

28 files changed

+246
-135
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

6+
## [5.2.0] - 2026-03-02
7+
8+
#### Added
9+
- Swap between chain assets
10+
- Biometrics choice dialog during Jade setup
11+
12+
#### Changed
13+
- Bump GDK to version 0.76.3
14+
- Improve watch-only login flow for hardware wallets
15+
16+
#### Fixed
17+
- Fix ANR caused by a coroutine being blocked in UI main thread
18+
- Fix genuine pin check in device scan flow
19+
- Fix error messages in receive amount validation
20+
- Fix auto-save when using paste/clear buttons in settings
21+
- Add retry logic for BLE notification setup
22+
- Show BTC and LBTC by default on home view
23+
- Enforce PIN setup after manual backup
24+
625
## [5.1.4] - 2025-12-10
726

827
#### Added

androidApp/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ android {
4545
defaultConfig {
4646
minSdk = libs.versions.androidMinSdk.get().toInt()
4747
targetSdk = libs.versions.androidTargetSdk.get().toInt()
48-
versionCode = 514
49-
versionName = "5.1.4"
48+
versionCode = 520
49+
versionName = "5.2.0"
5050

5151
base.archivesName = "BlockstreamGreen-v$versionName"
5252
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")

compose/src/commonMain/composeResources/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,4 +1979,5 @@
19791979
<string name="id_this_is_not_the_recipient_address">This is not the recipient address.</string>
19801980
<string name="id_you_cannot_disable_while_a_swap">You cannot disable while a swap is in progress.</string>
19811981
<string name="id_unlock_jade_and_scan_this_qr">Unlock Jade and scan this QR to enable swaps on this phone and generate a retrieval key in case of swap failure.</string>
1982+
<string name="id_invoice_already_paid">Invoice already paid</string>
19821983
</resources>

compose/src/commonMain/kotlin/com/blockstream/compose/components/SwapComponent.kt

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import androidx.compose.material3.TextButton
2727
import androidx.compose.material3.TextFieldDefaults
2828
import androidx.compose.runtime.Composable
2929
import androidx.compose.runtime.LaunchedEffect
30-
import androidx.compose.runtime.SideEffect
3130
import androidx.compose.runtime.getValue
3231
import androidx.compose.runtime.mutableStateOf
3332
import androidx.compose.runtime.remember
@@ -41,11 +40,9 @@ import androidx.compose.ui.focus.onFocusChanged
4140
import androidx.compose.ui.graphics.Color
4241
import androidx.compose.ui.graphics.SolidColor
4342
import androidx.compose.ui.layout.ContentScale
44-
import androidx.compose.ui.text.TextRange
4543
import androidx.compose.ui.text.TextStyle
4644
import androidx.compose.ui.text.input.ImeAction
4745
import androidx.compose.ui.text.input.KeyboardType
48-
import androidx.compose.ui.text.input.TextFieldValue
4946
import androidx.compose.ui.text.style.TextAlign
5047
import androidx.compose.ui.text.style.TextDecoration
5148
import androidx.compose.ui.text.style.TextOverflow
@@ -108,7 +105,7 @@ fun SwapComponent(
108105
onToAccountClick: () -> Unit,
109106
onToAssetClick: () -> Unit,
110107
onTogglePairsClick: () -> Unit,
111-
onDenominationClick: () -> Unit
108+
onDenominationClick: (Boolean) -> Unit
112109
) {
113110
var isFromFocused by remember { mutableStateOf(false) }
114111
var isToFocused by remember { mutableStateOf(false) }
@@ -164,7 +161,9 @@ fun SwapComponent(
164161
},
165162
onAccountClick = onFromAccountClick,
166163
onAssetClick = onFromAssetClick,
167-
onDenominationClick = onDenominationClick
164+
onDenominationClick = {
165+
onDenominationClick(true)
166+
}
168167
)
169168

170169
SwapCard(
@@ -189,7 +188,9 @@ fun SwapComponent(
189188
},
190189
onAccountClick = onToAccountClick,
191190
onAssetClick = onToAssetClick,
192-
onDenominationClick = onDenominationClick
191+
onDenominationClick = {
192+
onDenominationClick(false)
193+
}
193194
)
194195
}
195196
}
@@ -241,33 +242,6 @@ private fun SwapCard(
241242
)
242243
}
243244

244-
// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
245-
// of the composition.
246-
var textFieldValueState by remember {
247-
mutableStateOf(
248-
TextFieldValue(
249-
text = value, selection = TextRange(value.length)
250-
)
251-
)
252-
}
253-
254-
// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
255-
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
256-
// composition.
257-
val textFieldValue = textFieldValueState.copy(
258-
text = value,
259-
selection = if (textFieldValueState.text.length != value.length) TextRange(value.length) else textFieldValueState.selection
260-
)
261-
262-
SideEffect {
263-
if (textFieldValue.selection != textFieldValueState.selection ||
264-
textFieldValue.text != textFieldValueState.text ||
265-
textFieldValue.composition != textFieldValueState.composition
266-
) {
267-
textFieldValueState = textFieldValue
268-
}
269-
}
270-
271245
GreenCard(padding = 0) {
272246
Column(
273247
modifier = modifier
@@ -346,11 +320,9 @@ private fun SwapCard(
346320
) {
347321

348322
BasicTextField(
349-
value = textFieldValueState,
323+
value = value,
350324
onValueChange = {
351-
textFieldValueState = formatter.cleanup(it).also {
352-
onValueChange(it.text)
353-
}
325+
onValueChange(formatter.cleanup(it))
354326
},
355327
textStyle = textStyle,
356328
singleLine = true,

compose/src/commonMain/kotlin/com/blockstream/compose/models/assetaccounts/AssetAccountDetailsViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.blockstream.data.data.EnrichedAsset
2424
import com.blockstream.data.data.GreenWallet
2525
import com.blockstream.data.extensions.hasUnconfirmedTransactions
2626
import com.blockstream.data.extensions.ifConnected
27+
import com.blockstream.data.extensions.isPolicyAsset
2728
import com.blockstream.data.extensions.launchSafe
2829
import com.blockstream.data.gdk.data.Account
2930
import com.blockstream.data.gdk.data.AccountAsset
@@ -88,7 +89,13 @@ abstract class AssetAccountDetailsViewModelAbstract(
8889
}
8990

9091
fun onReceive() {
91-
postEvent(NavigateDestinations.ReceiveChooseAsset(greenWallet = greenWallet, accountAsset = accountAsset.value))
92+
accountAsset.value?.takeIf {
93+
!it.network.isLiquid || !it.assetId.isPolicyAsset(session)
94+
}?.also {
95+
postEvent(NavigateDestinations.Receive(greenWallet = greenWallet, accountAsset = it))
96+
} ?: run {
97+
postEvent(NavigateDestinations.ReceiveChooseAsset(greenWallet = greenWallet, accountAsset = accountAsset.value))
98+
}
9299
}
93100

94101
fun onSwap() {

compose/src/commonMain/kotlin/com/blockstream/compose/models/receive/ReceiveChooseAssetViewModel.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,19 @@ class ReceiveChooseAssetViewModel(
6868
throw Exception("id_insufficient_funds")
6969
}
7070

71-
if (accounts.size == 1) {
71+
val account = accountAsset.value?.account?.let { account ->
72+
accounts.find {
73+
it.account == account
74+
}
75+
} ?: accounts.firstOrNull()?.takeIf {
76+
accounts.size == 1
77+
}
78+
79+
if (account != null) {
7280
SideEffects.NavigateTo(
7381
NavigateDestinations.Receive(
7482
greenWallet = greenWallet,
75-
accountAsset = accounts.first()
83+
accountAsset = account
7684
)
7785
)
7886
} else {

compose/src/commonMain/kotlin/com/blockstream/compose/models/send/BumpViewModel.kt

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package com.blockstream.compose.models.send
33
import androidx.lifecycle.viewModelScope
44
import blockstream_green.common.generated.resources.Res
55
import blockstream_green.common.generated.resources.id_increase_fee
6+
import com.blockstream.compose.events.Event
7+
import com.blockstream.compose.extensions.launchIn
8+
import com.blockstream.compose.extensions.previewAccountAsset
9+
import com.blockstream.compose.extensions.previewWallet
10+
import com.blockstream.compose.navigation.NavData
611
import com.blockstream.data.TransactionSegmentation
712
import com.blockstream.data.TransactionType
813
import com.blockstream.data.data.Denomination
@@ -17,13 +22,9 @@ import com.blockstream.data.gdk.data.Transaction
1722
import com.blockstream.data.gdk.params.CreateTransactionParams
1823
import com.blockstream.data.utils.feeRateWithUnit
1924
import com.blockstream.data.utils.toAmountLook
20-
import com.blockstream.compose.events.Event
21-
import com.blockstream.compose.extensions.launchIn
22-
import com.blockstream.compose.extensions.previewAccountAsset
23-
import com.blockstream.compose.extensions.previewWallet
24-
import com.blockstream.compose.navigation.NavData
2525
import com.blockstream.utils.Loggable
2626
import kotlinx.coroutines.Dispatchers
27+
import kotlinx.coroutines.delay
2728
import kotlinx.coroutines.flow.MutableStateFlow
2829
import kotlinx.coroutines.flow.StateFlow
2930
import kotlinx.coroutines.flow.asStateFlow
@@ -124,16 +125,20 @@ class BumpViewModel(
124125
),
125126
broadcast = event.broadcastTransaction
126127
)
127-
} else if (event is LocalEvents.BroadcastTransaction) {
128-
signAndSendTransaction(
129-
params = createTransactionParams.value,
130-
originalTransaction = createTransaction.value,
131-
segmentation = TransactionSegmentation(
132-
transactionType = TransactionType.BUMP
133-
),
134-
psbt = event.psbt,
135-
broadcast = event.broadcastTransaction
136-
)
128+
} else if (event is LocalEvents.BroadcastPsbtTransaction) {
129+
// Hack to solve a screen/jade being stuck in screen transition
130+
viewModelScope.launch {
131+
delay(500L)
132+
signAndSendTransaction(
133+
params = createTransactionParams.value,
134+
originalTransaction = createTransaction.value,
135+
segmentation = TransactionSegmentation(
136+
transactionType = TransactionType.BUMP
137+
),
138+
psbt = event.psbt,
139+
broadcast = event.broadcastTransaction
140+
)
141+
}
137142
}
138143
}
139144

compose/src/commonMain/kotlin/com/blockstream/compose/models/send/CreateTransactionViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ abstract class CreateTransactionViewModelAbstract(
148148
data class SetCustomFeeRate(val amount: String) : Event
149149
data class SetAddressInputType(val inputType: AddressInputType) : Event
150150
data class SignTransaction(val broadcastTransaction: Boolean = true, val createPsbt: Boolean = false) : Event
151-
data class BroadcastTransaction(val broadcastTransaction: Boolean = true, val psbt: String) : Event
151+
data class BroadcastPsbtTransaction(val psbt: String, val broadcastTransaction: Boolean = true) : Event
152152
}
153153

154154
class LocalSideEffects {

compose/src/commonMain/kotlin/com/blockstream/compose/models/send/SendConfirmViewModel.kt

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.blockstream.data.gdk.data.AccountAsset
2727
import com.blockstream.data.gdk.data.UtxoView
2828
import com.blockstream.data.transaction.TransactionConfirmation
2929
import com.blockstream.utils.Loggable
30+
import kotlinx.coroutines.delay
3031
import kotlinx.coroutines.flow.MutableStateFlow
3132
import kotlinx.coroutines.flow.StateFlow
3233
import kotlinx.coroutines.flow.asStateFlow
@@ -148,15 +149,19 @@ class SendConfirmViewModel constructor(
148149
}
149150
}
150151

151-
is CreateTransactionViewModelAbstract.LocalEvents.BroadcastTransaction -> {
152-
session.pendingTransaction?.also {
153-
signAndSendTransaction(
154-
params = it.params,
155-
originalTransaction = it.transaction,
156-
segmentation = it.segmentation,
157-
psbt = event.psbt,
158-
broadcast = event.broadcastTransaction,
159-
)
152+
is CreateTransactionViewModelAbstract.LocalEvents.BroadcastPsbtTransaction -> {
153+
// Hack to solve a screen/jade being stuck in screen transition
154+
viewModelScope.launch {
155+
delay(500L)
156+
session.pendingTransaction?.also {
157+
signAndSendTransaction(
158+
params = it.params,
159+
originalTransaction = it.transaction,
160+
segmentation = it.segmentation,
161+
psbt = event.psbt,
162+
broadcast = event.broadcastTransaction,
163+
)
164+
}
160165
}
161166
}
162167

@@ -313,6 +318,14 @@ class SendConfirmViewModelPreview(
313318
fun previewLBTCSwap() = SendConfirmViewModelPreview(
314319
previewWallet(), transactionConfirmation = TransactionConfirmation(
315320
from = previewAccountAsset(),
321+
utxos = listOf(
322+
UtxoView(
323+
address = "bc1qaqtq80759n35gk6ftc57vh7du83nwvt5lgkznu",
324+
assetId = BTC_POLICY_ASSET,
325+
amount = "2.123 BTC",
326+
amountExchange = "45.123 USD"
327+
)
328+
),
316329
amount = "2.123 BTC",
317330
amountFiat = "43.312 USD",
318331
fee = "0.0123 BTC",

compose/src/commonMain/kotlin/com/blockstream/compose/models/swap/SwapViewModel.kt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.blockstream.data.TransactionSegmentation
1717
import com.blockstream.data.TransactionType
1818
import com.blockstream.data.banner.Banner
1919
import com.blockstream.data.data.DenominatedValue
20+
import com.blockstream.data.data.FeePriority
2021
import com.blockstream.data.data.GreenWallet
2122
import com.blockstream.data.extensions.ifConnected
2223
import com.blockstream.data.extensions.isNotBlank
@@ -34,6 +35,7 @@ import com.blockstream.data.utils.feeRateWithUnit
3435
import com.blockstream.data.utils.ifNotNull
3536
import com.blockstream.data.utils.toAmountLook
3637
import com.blockstream.domain.swap.SwapUseCase
38+
import com.blockstream.jade.Loggable
3739
import kotlinx.coroutines.Dispatchers
3840
import kotlinx.coroutines.ExperimentalCoroutinesApi
3941
import kotlinx.coroutines.FlowPreview
@@ -86,6 +88,8 @@ abstract class SwapViewModelAbstract(
8688

8789
abstract fun onAmountChanged(amount: String, isSendQuoteMode: Boolean)
8890

91+
abstract fun onQuoteModeChanged(isSendQuoteMode: Boolean)
92+
8993
abstract fun onAccountClick(isFrom: Boolean)
9094
abstract fun setAccount(accountAssetBalance: AccountAssetBalance)
9195
}
@@ -259,11 +263,23 @@ class SwapViewModel(
259263

260264
_network.onEach {
261265
_showFeeSelector.value = sendUseCase.showFeeSelectorUseCase(session = session, network = it)
266+
// Reset fee priority, this is important as can be changed by the user and persisted in liquid
267+
_feePriority.value = FeePriority.Low()
262268
}.launchIn(this)
263269

264270
bootstrap()
265271
}
266272

273+
override fun onQuoteModeChanged(isSendQuoteMode: Boolean) {
274+
uiState.update {
275+
if (isSendQuoteMode) {
276+
it.copy(quoteMode = QuoteMode.SEND)
277+
} else {
278+
it.copy(quoteMode = QuoteMode.RECEIVE)
279+
}
280+
}
281+
}
282+
267283
override fun onAmountChanged(amount: String, isSendQuoteMode: Boolean) {
268284
uiState.update {
269285
if (isSendQuoteMode) {
@@ -403,9 +419,15 @@ class SwapViewModel(
403419
override fun setDenominatedValue(denominatedValue: DenominatedValue) {
404420
_denomination.value = denominatedValue.denomination
405421
uiState.update { uiState ->
406-
uiState.copy(
407-
amountFrom = denominatedValue.asInput ?: ""
408-
)
422+
if (uiState.quoteMode.isSend) {
423+
uiState.copy(
424+
amountFrom = denominatedValue.asInput ?: ""
425+
)
426+
} else {
427+
uiState.copy(
428+
amountTo = denominatedValue.asInput ?: ""
429+
)
430+
}
409431
}
410432
}
411433

@@ -421,6 +443,8 @@ class SwapViewModel(
421443
}
422444
}
423445
}
446+
447+
companion object : Loggable()
424448
}
425449

426450
class SwapViewModelPreview(greenWallet: GreenWallet) :
@@ -431,6 +455,7 @@ class SwapViewModelPreview(greenWallet: GreenWallet) :
431455

432456
override fun createSwap() {}
433457
override fun onAmountChanged(amount: String, isFromInput: Boolean) {}
458+
override fun onQuoteModeChanged(isSendQuoteMode: Boolean) {}
434459

435460
override fun onAccountClick(isFrom: Boolean) {}
436461

0 commit comments

Comments
 (0)