diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index b90861d506..35761beaea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -58,6 +58,11 @@ data class UiSettings( val fontFamily: FontFamilyType = FontFamilyType.SYSTEM, val fontSize: FontSizeType = FontSizeType.NORMAL, val composeSignature: String = "", + // Whether the on-chain (Bitcoin/Taproot) wallet is shown across the app: the + // "Bitcoin" card on the wallet screen, the on-chain chip on profiles, and the + // on-chain rail in the Send Payment screen. Defaults to true (shown) so the + // behavior is unchanged for everyone who doesn't turn it off. + val showOnchainWallet: Boolean = true, ) enum class ThemeType( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index f84459a560..947270eb6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -58,6 +58,7 @@ class UiSettingsFlow( val fontFamily: MutableStateFlow = MutableStateFlow(FontFamilyType.SYSTEM), val fontSize: MutableStateFlow = MutableStateFlow(FontSizeType.NORMAL), val composeSignature: MutableStateFlow = MutableStateFlow(""), + val showOnchainWallet: MutableStateFlow = MutableStateFlow(true), ) { val listOfFlows: List> = listOf>( @@ -90,6 +91,7 @@ class UiSettingsFlow( fontFamily, fontSize, composeSignature, + showOnchainWallet, ) // emits at every change in any of the propertyes. @@ -126,6 +128,7 @@ class UiSettingsFlow( flows[26] as FontFamilyType, flows[27] as FontSizeType, flows[28] as String, + flows[29] as Boolean, ) } @@ -160,6 +163,7 @@ class UiSettingsFlow( fontFamily.value, fontSize.value, composeSignature.value, + showOnchainWallet.value, ) fun update(torSettings: UiSettings): Boolean { @@ -281,6 +285,10 @@ class UiSettingsFlow( composeSignature.tryEmit(torSettings.composeSignature) any = true } + if (showOnchainWallet.value != torSettings.showOnchainWallet) { + showOnchainWallet.tryEmit(torSettings.showOnchainWallet) + any = true + } return any } @@ -335,6 +343,7 @@ class UiSettingsFlow( MutableStateFlow(uiSettings.fontFamily), MutableStateFlow(uiSettings.fontSize), MutableStateFlow(uiSettings.composeSignature), + MutableStateFlow(uiSettings.showOnchainWallet), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index c101e4bc58..f35b3124ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -127,6 +127,7 @@ class UiSharedPreferences( val UI_FONT_FAMILY = stringPreferencesKey("ui.font_family") val UI_FONT_SIZE = stringPreferencesKey("ui.font_size") val UI_COMPOSE_SIGNATURE = stringPreferencesKey("ui.compose_signature") + val UI_SHOW_ONCHAIN_WALLET = booleanPreferencesKey("ui.show_onchain_wallet") suspend fun uiPreferences(context: Context): UiSettings? = try { @@ -168,6 +169,7 @@ class UiSharedPreferences( fontFamily = preferences[UI_FONT_FAMILY]?.let { FontFamilyType.valueOf(it) } ?: FontFamilyType.SYSTEM, fontSize = preferences[UI_FONT_SIZE]?.let { FontSizeType.valueOf(it) } ?: FontSizeType.NORMAL, composeSignature = preferences[UI_COMPOSE_SIGNATURE] ?: "", + showOnchainWallet = preferences[UI_SHOW_ONCHAIN_WALLET] ?: true, ) } catch (e: Exception) { if (e is CancellationException) throw e @@ -221,6 +223,7 @@ class UiSharedPreferences( preferences[UI_FONT_FAMILY] = sharedSettings.fontFamily.name preferences[UI_FONT_SIZE] = sharedSettings.fontSize.name preferences[UI_COMPOSE_SIGNATURE] = sharedSettings.composeSignature + preferences[UI_SHOW_ONCHAIN_WALLET] = sharedSettings.showOnchainWallet } } catch (e: Exception) { if (e is CancellationException) throw e diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 7a4067d6b8..8ffc1fcec7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -2067,11 +2067,16 @@ fun ZapAmountChoicePopup( val cashuEntries by cashuState.tokenEntries.collectAsStateWithLifecycle() val recipientInfo = author?.let { observeUserInfo(it, accountViewModel).value } val nutzapInfo = author?.let { observeNoteEvent(it.nutzapInfoNote, accountViewModel).value } + // Honors the user's "show on-chain wallet" preference: off hides the on-chain + // rail from the zap chips too, matching the wallet screen, profile chips, and + // Send Payment screen. + val showOnchainWallet by accountViewModel.settings.uiSettingsFlow.showOnchainWallet + .collectAsStateWithLifecycle() val railCapability = - remember(baseNote, onchainSupported, cashuMints, cashuEntries, recipientInfo, nutzapInfo) { + remember(baseNote, onchainSupported, showOnchainWallet, cashuMints, cashuEntries, recipientInfo, nutzapInfo) { val rc = RailCapabilityResolver.peek(baseNote, cashuState) - if (onchainSupported) rc else rc.copy(hasOnchain = false) + if (onchainSupported && showOnchainWallet) rc else rc.copy(hasOnchain = false) } val amountChoices = remember(zapAmountChoices) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 8c29e8e01c..6a62b3c88b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -71,6 +71,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.core.net.toUri import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -180,6 +181,11 @@ fun ZapCustomDialog( // payment method, no "go back to Lightning" is offered here. var sendOnchain by remember { mutableStateOf(false) } + // Hides the "Send on-chain instead" hand-off when the user has turned the + // on-chain wallet off, matching every other on-chain surface. + val showOnchainWallet by accountViewModel.settings.uiSettingsFlow.showOnchainWallet + .collectAsStateWithLifecycle() + Dialog( onDismissRequest = { onClose() }, properties = @@ -353,16 +359,18 @@ fun ZapCustomDialog( // Hand off to the on-chain dialog with the entered amount + // message prefilled. Disabled while the amount is empty so the // user can't open a sheet that immediately gates on its own - // empty field. - TextButton( - onClick = { sendOnchain = true }, - enabled = postViewModel.canSend() && !baseNote.isDraft(), - modifier = - Modifier - .fillMaxWidth() - .padding(top = 4.dp), - ) { - Text(text = stringRes(id = R.string.send_onchain_instead)) + // empty field. Hidden entirely when the on-chain wallet is off. + if (showOnchainWallet) { + TextButton( + onClick = { sendOnchain = true }, + enabled = postViewModel.canSend() && !baseNote.isDraft(), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 4.dp), + ) { + Text(text = stringRes(id = R.string.send_onchain_instead)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt index 43bf6693b3..b64b7bc3f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons @@ -103,7 +104,9 @@ fun DisplayPaymentRailChips( ?.target ?.mintUrl } - val onchainAvailable = LocalCache.onchainBackend != null + val showOnchainWallet by accountViewModel.settings.uiSettingsFlow.showOnchainWallet + .collectAsStateWithLifecycle() + val onchainAvailable = showOnchainWallet && LocalCache.onchainBackend != null val address = remember(baseUser.pubkeyHex) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index 9363aa9567..d9a2fc3da0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -160,7 +160,11 @@ private fun SendPaymentLoaded( // Cheap read, intentionally not remembered: the chain backend is wired up // asynchronously at boot and a frozen `remember {}` would hide the // on-chain rail for the whole composition if the screen won the race. - val onchainAvailable = LocalCache.onchainBackend != null + // Also honors the user's "show on-chain wallet" preference — off hides the + // rail everywhere, matching the wallet screen and profile chips. + val showOnchainWallet by accountViewModel.settings.uiSettingsFlow.showOnchainWallet + .collectAsStateWithLifecycle() + val onchainAvailable = showOnchainWallet && LocalCache.onchainBackend != null // When navigated from a `bitcoin` payment target, the on-chain rail pays // that announced address directly (plain send, no NIP-BC receipt) instead // of the recipient's pubkey-derived Taproot address. Re-validated here in diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt index c39794a696..137ffacbe3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt @@ -92,6 +92,7 @@ fun ProfileUiSettingsContent(accountViewModel: AccountViewModel) { val showAppRecommendations by ui.showProfileAppRecommendations.collectAsStateWithLifecycle() val showZapReceived by ui.showProfileZapReceivedFeed.collectAsStateWithLifecycle() val showFollowers by ui.showProfileFollowersFeed.collectAsStateWithLifecycle() + val showOnchainWallet by ui.showOnchainWallet.collectAsStateWithLifecycle() Column( modifier = @@ -136,6 +137,13 @@ fun ProfileUiSettingsContent(accountViewModel: AccountViewModel) { ) HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp)) + ProfileUiSwitchRow( + title = stringRes(R.string.profile_ui_setting_onchain_wallet), + checked = showOnchainWallet, + onCheckedChange = { ui.showOnchainWallet.tryEmit(it) }, + ) + HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp)) + Column(modifier = Modifier.padding(vertical = 12.dp, horizontal = Size20dp)) { GalleryChoice(ui) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt index 4d5db6d518..829c98f12a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -103,6 +103,8 @@ fun WalletScreen( // the first composition pass. init() is idempotent — just sets refs. cashuWalletViewModel.init(accountViewModel) + val showOnchainWallet by accountViewModel.settings.uiSettingsFlow.showOnchainWallet + .collectAsState() val hasNwcWallet by walletViewModel.hasWalletSetup.collectAsState() val cashuWalletEvent by cashuWalletViewModel.walletEvent.collectAsState() val hasCashuWallet = cashuWalletEvent != null @@ -147,11 +149,13 @@ fun WalletScreen( }, ) { padding -> Column(modifier = Modifier.padding(padding)) { - OnchainSection( - accountViewModel = accountViewModel, - nav = nav, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) + if (showOnchainWallet) { + OnchainSection( + accountViewModel = accountViewModel, + nav = nav, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } if (!hasWallet) { NoWalletSetup( modifier = Modifier, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 96d1287e7d..40ab075e01 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2608,6 +2608,7 @@ App Recommendations Zap Received Feed Followers Feed + Bitcoin (on-chain) Wallet Reaction Row Configure which reaction buttons are shown, their order, and whether to display counters. Enabled