diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index b7f89682da..c1d970ce05 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.map import kotlinx.serialization.Serializable import to.bitkit.data.serializers.SettingsSerializer import to.bitkit.env.Env +import to.bitkit.ext.runSuspendCatching import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.CoinSelectionPreference import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING @@ -49,8 +50,9 @@ class SettingsStore @Inject constructor( private set suspend fun restoreFromBackup(payload: SettingsBackupV1) = - runCatching { + runSuspendCatching { val data = payload.settings.resetPin() + .copy(ignoresSwitchUnitToast = false, ignoresHideBalanceToast = false) .withDefaultPaykitPaymentMethods() .withRequiredNativeSegwitMonitoring() store.updateData { data } @@ -67,6 +69,32 @@ class SettingsStore @Inject constructor( store.updateData { transform(it).withRequiredNativeSegwitMonitoring() } } + suspend fun switchBalanceUnit(): BalanceUnitSwitch? { + var firstSwitch: BalanceUnitSwitch? = null + store.updateData { settings -> + val nextDisplay = settings.primaryDisplay.not() + if (!settings.ignoresSwitchUnitToast) { + firstSwitch = BalanceUnitSwitch(settings.primaryDisplay, nextDisplay, settings.selectedCurrency) + } + settings.copy(primaryDisplay = nextDisplay, ignoresSwitchUnitToast = true) + } + return firstSwitch + } + + suspend fun toggleHideBalanceFromSwipe(): Boolean { + var firstHide = false + store.updateData { settings -> + if (!settings.enableSwipeToHideBalance) return@updateData settings + val hideBalance = !settings.hideBalance + firstHide = hideBalance && !settings.ignoresHideBalanceToast + settings.copy( + hideBalance = hideBalance, + ignoresHideBalanceToast = settings.ignoresHideBalanceToast || firstHide, + ) + } + return firstHide + } + suspend fun setIsPaykitEnabled(value: Boolean) { localStore.edit { it[PAYKIT_ENABLED_KEY] = value } } @@ -150,6 +178,8 @@ data class SettingsData( val enableSwipeToHideBalance: Boolean = true, val hideBalance: Boolean = false, val hideBalanceOnOpen: Boolean = false, + val ignoresSwitchUnitToast: Boolean = false, + val ignoresHideBalanceToast: Boolean = false, val enableAutoReadClipboard: Boolean = false, val enableSendAmountWarning: Boolean = false, val backupVerified: Boolean = false, @@ -170,6 +200,12 @@ data class SettingsData( val pendingRestoreAddressTypePrune: Boolean = false, ) +data class BalanceUnitSwitch( + val previousDisplay: PrimaryDisplay, + val newDisplay: PrimaryDisplay, + val selectedCurrency: String, +) + fun SettingsData.resetPin() = this.copy( isPinEnabled = false, isPinForPaymentsEnabled = false, diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index f344344b33..d2a9560ae0 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -25,6 +25,7 @@ import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher import to.bitkit.env.Env +import to.bitkit.ext.runSuspendCatching import to.bitkit.models.BTC_SCALE import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.ConvertedAmount @@ -163,6 +164,10 @@ class CurrencyRepo @Inject constructor( settingsStore.update { it.copy(primaryDisplay = it.primaryDisplay.not()) } } + suspend fun switchBalanceUnit() = withContext(bgDispatcher) { + runSuspendCatching { settingsStore.switchBalanceUnit() } + } + override suspend fun switchUnit(unit: PrimaryDisplay): PrimaryDisplay = withContext(bgDispatcher) { unit.not().also { nextValue -> setPrimaryDisplayUnit(nextValue) diff --git a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt index 69c1cff806..f4b2b7d544 100644 --- a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt +++ b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt @@ -72,6 +72,7 @@ fun BalanceHeaderView( showEyeIcon = showEyeIcon, onClick = {}, onToggleHideBalance = {}, + onRevealBalance = {}, testTag = testTag, modifier = modifier, ) @@ -110,8 +111,9 @@ fun BalanceHeaderView( hideBalance = shouldHideBalance, isSwipeToHideEnabled = allowSwipeToHide, showEyeIcon = showEyeIcon, - onClick = onClick ?: { currency.switchUnit() }, - onToggleHideBalance = { settings.setHideBalance(!hideBalance) }, + onClick = onClick ?: { currency.switchBalanceUnit() }, + onToggleHideBalance = { settings.toggleHideBalanceFromSwipe() }, + onRevealBalance = { settings.setHideBalance(false) }, testTag = testTag, modifier = modifier, ) @@ -137,6 +139,7 @@ fun BalanceHeader( isSwipeToHideEnabled: Boolean = false, showEyeIcon: Boolean = false, onToggleHideBalance: () -> Unit = {}, + onRevealBalance: () -> Unit = {}, testTag: String? = null, ) { val smallRowState = remember( @@ -254,7 +257,7 @@ fun BalanceHeader( tint = Colors.White64, modifier = Modifier .size(24.dp) - .clickableAlpha { onToggleHideBalance() } + .clickableAlpha { onRevealBalance() } .testTag("ShowBalance") ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/CurrencyViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/CurrencyViewModel.kt index 72afc36d76..b28d2991f0 100644 --- a/app/src/main/java/to/bitkit/viewmodels/CurrencyViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/CurrencyViewModel.kt @@ -9,12 +9,16 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import to.bitkit.R import to.bitkit.appwidget.ui.weather.WeatherGlanceWidget import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.ConvertedAmount import to.bitkit.models.PrimaryDisplay +import to.bitkit.models.Toast import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.CurrencyState +import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.utils.Logger import javax.inject.Inject @HiltViewModel @@ -22,6 +26,9 @@ class CurrencyViewModel @Inject constructor( @ApplicationContext private val context: Context, private val currencyRepo: CurrencyRepo, ) : ViewModel() { + private companion object { + const val TAG = "CurrencyViewModel" + } val uiState: StateFlow = currencyRepo.currencyState @@ -37,6 +44,33 @@ class CurrencyViewModel @Inject constructor( } } + fun switchBalanceUnit() { + viewModelScope.launch { + currencyRepo.switchBalanceUnit().onSuccess { switch -> + if (switch == null) return@onSuccess + val newUnit = if (switch.newDisplay == PrimaryDisplay.BITCOIN) { + context.getString(R.string.settings__general__unit_bitcoin) + } else { + switch.selectedCurrency + } + val previousUnit = if (switch.previousDisplay == PrimaryDisplay.BITCOIN) { + context.getString(R.string.settings__general__unit_bitcoin) + } else { + switch.selectedCurrency + } + ToastEventBus.send( + type = Toast.ToastType.INFO, + title = context.getString(R.string.wallet__balance_unit_switched_title, newUnit), + description = context.getString(R.string.wallet__balance_unit_switched_message, previousUnit), + visibilityTime = 5000L, + testTag = "BalanceUnitSwitchedToast", + ) + }.onFailure { + Logger.error("Failed to switch balance unit", it, context = TAG) + } + } + } + fun setPrimaryDisplayUnit(unit: PrimaryDisplay) { viewModelScope.launch { currencyRepo.setPrimaryDisplayUnit(unit) diff --git a/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt index 4afc065826..6527dd7f1c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt @@ -23,6 +23,7 @@ import to.bitkit.data.WidgetsStore import to.bitkit.data.hasPaykitState import to.bitkit.data.hasPublicPaykitPublicationState import to.bitkit.data.paykitDisabled +import to.bitkit.ext.runSuspendCatching import to.bitkit.flags.PaykitFeatureFlags import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed @@ -384,6 +385,25 @@ class SettingsViewModel @Inject constructor( } } + fun toggleHideBalanceFromSwipe() { + viewModelScope.launch { + runSuspendCatching { settingsStore.toggleHideBalanceFromSwipe() } + .onSuccess { firstHide -> + if (!firstHide) return@onSuccess + ToastEventBus.send( + type = Toast.ToastType.INFO, + title = context.getString(R.string.wallet__balance_hidden_title), + description = context.getString(R.string.wallet__balance_hidden_message), + visibilityTime = 5000L, + testTag = "BalanceHiddenToast", + ) + } + .onFailure { + Logger.error("Failed to hide balance from swipe", it, context = TAG) + } + } + } + val hideBalanceOnOpen = settingsStore.data.map { it.hideBalanceOnOpen } .asStateFlow(initialValue = false) diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 9875e2dec1..ad8f6f8449 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -829,6 +829,10 @@ A ahorros A Gastos ID de la transacción + Desliza tu saldo para verlo nuevamente. + Saldo de billetera oculto + Toque el saldo de su monedero para cambiarlo de nuevo a %1$s. + Cambiado a %1$s Impulsar Reducir comisión Bitkit no pudo impulsar la transacción. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 5cbb378717..14b6925182 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -829,6 +829,10 @@ A estalvis A despesa ID de transacció + Llisca el saldo de la teva cartera per revelar-lo de nou. + Saldo de la cartera amagat + Toca el saldo de la teva cartera per tornar-lo a %1$s. + Canviat a %1$s Impulsar Reduir tarifa Bitkit no ha pogut impulsar la transacció. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index aa791ddbfb..21d6cb7b94 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -829,6 +829,10 @@ Do úspor Do útrat ID transakce + Přejeďte prstem po zůstatku v peněžence a znovu jej odkryjte. + Skrytý zůstatek peněženky + Klepnutím na zůstatek v peněžence jej přepnete zpět na %1$s. + Přepnuto na %1$s Posílit Snížit poplatek Bitkit se nepodařilo posílit transakci. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e25e3d018b..c6cdbe01f6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -952,6 +952,10 @@ AusgabenKonto <accent>Sende\nBitcoin</accent>\nauf dein\nAusgabenkonto Eingehender Transfer: + Streiche über dein Wallet-Guthaben, um es erneut anzuzeigen. + Wallet-Guthaben versteckt + Tippe auf dein Wallet-Guthaben, um es zurück zu %1$s zu wechseln. + Gewechselt zu %1$s Beschleunigen Transaktion beschleunigen Beschleunigt! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5d4b6e25ad..9a2f10016a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -829,6 +829,10 @@ Σε αποταμιεύσεις Σε δαπάνες ID συναλλαγής + Σύρετε το υπόλοιπο του πορτοφολιού σας για να το αποκαλύψετε ξανά. + Υπόλοιπο Πορτοφολιού Κρυμμένο + Πατήστε στο υπόλοιπο του πορτοφολιού σας για να το αλλάξετε πίσω σε %1$s. + Αλλαγή σε %1$s Ενίσχυση Μείωση τέλους Το Bitkit δεν μπόρεσε να ενισχύσει τη συναλλαγή. diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 889b8cadeb..a2a6a373f4 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -829,6 +829,10 @@ A ahorros A gasto ID de la transacción + Deslice el balance del monedero para mostrarlo de nuevo. + Saldo del Monedero Oculto + Toca tu saldo del monedero para cambiarlo de nuevo a %1$s. + Cambió a %1$s Impulso Reducir comisión Bitkit no pudo potenciar la transacción. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 513aa64745..5dda8a5680 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -946,6 +946,10 @@ <accent>Envía\nbitcoin</accent>\na tu\nsaldo de ahorros Gasto <accent>Envía\nbitcoin</accent>\na tu\nsaldo de gastos + Deslice el balance del monedero para mostrarlo de nuevo. + Saldo del Monedero Oculto + Toca tu saldo del monedero para cambiarlo de nuevo a %1$s. + Cambió a %1$s Impulso Potenciar Transacción Impulsada! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9a34593d38..289933cb79 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -942,6 +942,10 @@ Dépenses <accent>Envoyer \nles bitcoins</accent>\nsur votre \nsolde Dépenses Transfert entrant : + Faites glisser votre solde pour l’afficher à nouveau. + Balance du wallet cachée + Appuyez sur votre solde pour le repasser en %1$s. + Passé en %1$s Boost Boost Transaction Boostée ! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a5010eb8a0..e37253325a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -829,6 +829,10 @@ Ai Risparmi A Saldo Spendibile ID Transazione + Scorri il saldo del tuo portafoglio per rivelarlo di nuovo. + Saldo del portafoglio nascosto + Tocca il saldo del tuo portafoglio per ripristinarlo %1$s. + Passato a %1$s Potenzia Riduci commissione Bitkit non è stato in grado di potenziare la transazione. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ad63610848..f2b64fbcf7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -829,6 +829,10 @@ Naar spaargeld Naar bestedingssaldo Transactie-ID + Veeg uw saldo om deze weer te onthullen. + Saldo Wallet Verborgen + Tik op uw saldo om het terug te zetten naar %1$s. + Overgeschakeld naar %1$s Boost Vergoeding verlagen Bitkit kon de transactie niet boosten. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e636256b03..b61eae2aa2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -942,6 +942,10 @@ Wydatki <accent>Wyślij\nbitcoin</accent>\nna Twoje\nsaldo wydatków Transfer przychodzący: + Przeciągnij saldo portfela, aby je ponownie wyświetlić. + Ukryte saldo portfela + Dotknij salda portfela, aby przełączyć je z powrotem na %1$s. + Przełączono na %1$s Przyśpiesz Przyśpiesz transakcję Przyśpieszona! diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 959c0affac..e57747438c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -829,6 +829,10 @@ Para Poupança Para o Saldo de Gastos ID da transação + Deslize o seu saldo para revelá-lo novamente. + Saldo da Carteira Oculto + Toque no seu saldo para visualizá-lo em %1$s. + Saldo exibido em %1$s Impulsionar Reduzir taxa A Bitkit não conseguiu acelerar a transação. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 63e6813b4a..b25246e6a5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -846,6 +846,10 @@ В сбережения В Расходы ID Транзакции + Смахните ваш баланс кошелька, чтобы снова его показать. + Баланс Кошелька Скрыт + Нажмите на баланс кошелька, чтобы переключить обратно на %1$s. + Переключено на %1$s Ускорить Уменьшить комиссию Bitkit не удалось ускорить транзакцию. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d6db4c959..ae592b20be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1214,6 +1214,10 @@ To Savings To Spending Transaction ID + Swipe your wallet balance to reveal it again. + Wallet Balance Hidden + Tap your wallet balance to switch it back to %1$s. + Switched to %1$s Boost Reduce fee Bitkit was unable to boost the transaction. diff --git a/app/src/test/java/to/bitkit/data/SettingsStoreTest.kt b/app/src/test/java/to/bitkit/data/SettingsStoreTest.kt new file mode 100644 index 0000000000..8ead23dad4 --- /dev/null +++ b/app/src/test/java/to/bitkit/data/SettingsStoreTest.kt @@ -0,0 +1,86 @@ +package to.bitkit.data + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import to.bitkit.models.PrimaryDisplay +import to.bitkit.models.SettingsBackupV1 +import to.bitkit.test.BaseUnitTest +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@Config(application = Application::class, sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class SettingsStoreTest : BaseUnitTest() { + private val context = ApplicationProvider.getApplicationContext() + private val sut = SettingsStore(context) + + @Before + fun setUp() = runBlocking { sut.reset() } + + @After + fun tearDown() = runBlocking { sut.reset() } + + @Test + fun `balance unit switch reports only the first transition and persists its flag`() = test { + sut.update { it.copy(selectedCurrency = "EUR") } + + val firstSwitch = sut.switchBalanceUnit() + assertEquals(PrimaryDisplay.BITCOIN, firstSwitch?.previousDisplay) + assertEquals(PrimaryDisplay.FIAT, firstSwitch?.newDisplay) + assertEquals("EUR", firstSwitch?.selectedCurrency) + assertTrue(sut.data.first().ignoresSwitchUnitToast) + + assertNull(SettingsStore(context).switchBalanceUnit()) + assertEquals(PrimaryDisplay.BITCOIN, sut.data.first().primaryDisplay) + } + + @Test + fun `swipe reports only the first visible to hidden transition`() = test { + sut.update { it.copy(hideBalance = true) } + + assertFalse(sut.toggleHideBalanceFromSwipe()) + assertFalse(sut.data.first().ignoresHideBalanceToast) + + assertTrue(sut.toggleHideBalanceFromSwipe()) + assertTrue(sut.data.first().hideBalance) + assertTrue(sut.data.first().ignoresHideBalanceToast) + + assertFalse(SettingsStore(context).toggleHideBalanceFromSwipe()) + assertFalse(sut.data.first().hideBalance) + } + + @Test + fun `disabled swipe does not hide the balance or consume the first toast`() = test { + sut.update { it.copy(enableSwipeToHideBalance = false) } + + assertFalse(sut.toggleHideBalanceFromSwipe()) + assertFalse(sut.data.first().hideBalance) + assertFalse(sut.data.first().ignoresHideBalanceToast) + } + + @Test + fun `restoring settings allows first gesture guidance on this device`() = test { + val backup = SettingsBackupV1( + createdAt = 0L, + settings = SettingsData( + ignoresSwitchUnitToast = true, + ignoresHideBalanceToast = true, + ), + ) + + assertTrue(sut.restoreFromBackup(backup).isSuccess) + assertFalse(sut.data.first().ignoresSwitchUnitToast) + assertFalse(sut.data.first().ignoresHideBalanceToast) + } +} diff --git a/changelog.d/next/1272.fixed.md b/changelog.d/next/1272.fixed.md new file mode 100644 index 0000000000..c8078435e9 --- /dev/null +++ b/changelog.d/next/1272.fixed.md @@ -0,0 +1 @@ +Bitkit now shows helpful guidance the first time you switch the displayed balance unit or swipe to hide your wallet balance.