feat: confirm onchain copy with public-address warning
The Taproot address is derived from the Nostr pubkey, so copying it out is the moment a user is about to share an account-linked, publicly auditable wallet. Gate the Copy button on the Onchain card with the same explanation shown by the Public chip, and offer a "Don't show again" opt-out persisted in the existing UI preferences DataStore.
This commit is contained in:
@@ -52,6 +52,7 @@ data class UiSettings(
|
||||
val showProfileAppRecommendations: Boolean = true,
|
||||
val showProfileZapReceivedFeed: Boolean = true,
|
||||
val showProfileFollowersFeed: Boolean = true,
|
||||
val dontShowOnchainPublicWarning: Boolean = false,
|
||||
)
|
||||
|
||||
enum class ThemeType(
|
||||
|
||||
@@ -52,6 +52,7 @@ class UiSettingsFlow(
|
||||
val showProfileAppRecommendations: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val showProfileZapReceivedFeed: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val showProfileFollowersFeed: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val dontShowOnchainPublicWarning: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
) {
|
||||
val listOfFlows: List<Flow<Any?>> =
|
||||
listOf<Flow<Any?>>(
|
||||
@@ -78,6 +79,7 @@ class UiSettingsFlow(
|
||||
showProfileAppRecommendations,
|
||||
showProfileZapReceivedFeed,
|
||||
showProfileFollowersFeed,
|
||||
dontShowOnchainPublicWarning,
|
||||
)
|
||||
|
||||
// emits at every change in any of the propertyes.
|
||||
@@ -108,6 +110,7 @@ class UiSettingsFlow(
|
||||
flows[20] as Boolean,
|
||||
flows[21] as Boolean,
|
||||
flows[22] as Boolean,
|
||||
flows[23] as Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,6 +139,7 @@ class UiSettingsFlow(
|
||||
showProfileAppRecommendations.value,
|
||||
showProfileZapReceivedFeed.value,
|
||||
showProfileFollowersFeed.value,
|
||||
dontShowOnchainPublicWarning.value,
|
||||
)
|
||||
|
||||
fun update(torSettings: UiSettings): Boolean {
|
||||
@@ -233,6 +237,10 @@ class UiSettingsFlow(
|
||||
showProfileFollowersFeed.tryEmit(torSettings.showProfileFollowersFeed)
|
||||
any = true
|
||||
}
|
||||
if (dontShowOnchainPublicWarning.value != torSettings.dontShowOnchainPublicWarning) {
|
||||
dontShowOnchainPublicWarning.tryEmit(torSettings.dontShowOnchainPublicWarning)
|
||||
any = true
|
||||
}
|
||||
|
||||
return any
|
||||
}
|
||||
@@ -249,6 +257,12 @@ class UiSettingsFlow(
|
||||
}
|
||||
}
|
||||
|
||||
fun dontShowOnchainPublicWarning() {
|
||||
if (!dontShowOnchainPublicWarning.value) {
|
||||
dontShowOnchainPublicWarning.tryEmit(true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun build(uiSettings: UiSettings): UiSettingsFlow =
|
||||
UiSettingsFlow(
|
||||
@@ -275,6 +289,7 @@ class UiSettingsFlow(
|
||||
MutableStateFlow(uiSettings.showProfileAppRecommendations),
|
||||
MutableStateFlow(uiSettings.showProfileZapReceivedFeed),
|
||||
MutableStateFlow(uiSettings.showProfileFollowersFeed),
|
||||
MutableStateFlow(uiSettings.dontShowOnchainPublicWarning),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -116,6 +116,7 @@ class UiSharedPreferences(
|
||||
val UI_SHOW_PROFILE_APP_RECOMMENDATIONS = booleanPreferencesKey("ui.show_profile_app_recommendations")
|
||||
val UI_SHOW_PROFILE_ZAP_RECEIVED_FEED = booleanPreferencesKey("ui.show_profile_zap_received_feed")
|
||||
val UI_SHOW_PROFILE_FOLLOWERS_FEED = booleanPreferencesKey("ui.show_profile_followers_feed")
|
||||
val UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING = booleanPreferencesKey("ui.dont_show_onchain_public_warning")
|
||||
|
||||
suspend fun uiPreferences(context: Context): UiSettings? =
|
||||
try {
|
||||
@@ -150,6 +151,7 @@ class UiSharedPreferences(
|
||||
showProfileAppRecommendations = preferences[UI_SHOW_PROFILE_APP_RECOMMENDATIONS] ?: true,
|
||||
showProfileZapReceivedFeed = preferences[UI_SHOW_PROFILE_ZAP_RECEIVED_FEED] ?: true,
|
||||
showProfileFollowersFeed = preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] ?: true,
|
||||
dontShowOnchainPublicWarning = preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] ?: false,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -197,6 +199,7 @@ class UiSharedPreferences(
|
||||
preferences[UI_SHOW_PROFILE_APP_RECOMMENDATIONS] = sharedSettings.showProfileAppRecommendations
|
||||
preferences[UI_SHOW_PROFILE_ZAP_RECEIVED_FEED] = sharedSettings.showProfileZapReceivedFeed
|
||||
preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] = sharedSettings.showProfileFollowersFeed
|
||||
preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] = sharedSettings.dontShowOnchainPublicWarning
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
+75
-1
@@ -26,6 +26,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -62,6 +63,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.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -360,7 +362,15 @@ private fun ActionRow(
|
||||
) {
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val sharedPrefs = accountViewModel.settings.uiSettingsFlow
|
||||
val dontShowCopyWarning by sharedPrefs.dontShowOnchainPublicWarning.collectAsStateWithLifecycle()
|
||||
var showSendDialog by remember { mutableStateOf(false) }
|
||||
var showCopyWarning by remember { mutableStateOf(false) }
|
||||
|
||||
val copyAddress = {
|
||||
scope.launch { clipboard.setText(address) }
|
||||
Unit
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -368,7 +378,13 @@ private fun ActionRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { scope.launch { clipboard.setText(address) } },
|
||||
onClick = {
|
||||
if (dontShowCopyWarning) {
|
||||
copyAddress()
|
||||
} else {
|
||||
showCopyWarning = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.height(36.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
@@ -409,4 +425,62 @@ private fun ActionRow(
|
||||
onDismiss = { showSendDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showCopyWarning) {
|
||||
CopyPublicAddressDialog(
|
||||
onConfirm = {
|
||||
showCopyWarning = false
|
||||
copyAddress()
|
||||
},
|
||||
onDontShowAgain = {
|
||||
sharedPrefs.dontShowOnchainPublicWarning()
|
||||
showCopyWarning = false
|
||||
copyAddress()
|
||||
},
|
||||
onDismiss = { showCopyWarning = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CopyPublicAddressDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDontShowAgain: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
icon = {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
},
|
||||
title = { Text(stringRes(R.string.wallet_onchain_public_dialog_title)) },
|
||||
text = { Text(stringRes(R.string.wallet_onchain_public_dialog_body)) },
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = onDontShowAgain) {
|
||||
Text(stringRes(R.string.quick_action_dont_show_again_button))
|
||||
}
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.ContentCopy,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringRes(R.string.wallet_onchain_copy_dialog_confirm))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1867,6 +1867,7 @@
|
||||
<string name="wallet_onchain_public_dialog_title">This wallet is public</string>
|
||||
<string name="wallet_onchain_public_dialog_body">Your Taproot address is derived from your Nostr public key, so anyone who knows your npub can see this wallet\'s balance and transaction history on the blockchain.\n\nTo preserve your privacy, fund and drain this wallet from and to non-private accounts, like exchanges. Never mix these funds with your cold wallets, and treat them as money you can lose, since anyone in control of your nsec can spend it.</string>
|
||||
<string name="wallet_onchain_public_dialog_confirm">Got it</string>
|
||||
<string name="wallet_onchain_copy_dialog_confirm">Copy address</string>
|
||||
<string name="route_security_filters">Security Filters</string>
|
||||
<string name="route_import_follows">Import Follows</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user