diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 739b415353..5eac5e817f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -240,7 +240,7 @@ fun uriToRoute( val nip47Uri = url.getQueryParameter("value")?.firstOrNull() if (nip47Uri != null) { Nip47WalletConnect.parse(nip47Uri) - return Route.Nip47NWCSetup(nip47Uri) + return Route.WalletAddNwc(nip47Uri) } } catch (e: Exception) { if (e is CancellationException) throw e @@ -249,7 +249,7 @@ fun uriToRoute( try { Nip47WalletConnect.parse(uri) - return Route.Nip47NWCSetup(uri) + return Route.WalletAddNwc(uri) } catch (e: Exception) { if (e is CancellationException) throw e } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index f7b8ac0192..55d93b3008 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -310,7 +310,7 @@ fun BuildNavigation( composableFromEnd { OnchainTransactionsScreen(accountViewModel, nav) } composableFromEndArgs { WalletDetailScreen(it.walletId, accountViewModel, nav) } composableFromEnd { AddWalletScreen(accountViewModel, nav) } - composableFromEnd { AddNwcWalletScreen(accountViewModel, nav) } + composableFromEndArgs { AddNwcWalletScreen(accountViewModel, nav, it.nip47) } composableFromEnd { AddCashuWalletScreen(accountViewModel, nav) } composableFromEnd { CashuWalletScreen(accountViewModel, nav) } composableFromEnd { CashuWalletSettingsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index af2d44aa01..37b685c1fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -200,7 +200,9 @@ sealed class Route { @Serializable object WalletAdd : Route() - @Serializable object WalletAddNwc : Route() + @Serializable data class WalletAddNwc( + val nip47: String? = null, + ) : Route() @Serializable object WalletAddCashu : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index e13781f6c4..136bafafe4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -20,31 +20,18 @@ */ package com.vitorpamplona.amethyst.ui.note -import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.content.Intent import android.os.Build import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.togetherWith import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -61,33 +48,21 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -96,21 +71,14 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer -import com.vitorpamplona.amethyst.ui.components.util.getText -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier -import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -139,15 +107,9 @@ fun UpdateZapAmountContentPreview() { fun UpdateZapAmountContent( postViewModel: UpdateZapAmountViewModel, onClose: () -> Unit, - nip47uri: String? = null, accountViewModel: AccountViewModel, trailingContent: @Composable ColumnScope.() -> Unit = {}, ) { - val context = LocalContext.current - val clipboardManager = LocalClipboard.current - val uri = LocalUriHandler.current - val scope = rememberCoroutineScope() - val zapTypes = listOf( Triple( @@ -177,36 +139,6 @@ fun UpdateZapAmountContent( zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() } - LaunchedEffect(accountViewModel, nip47uri) { - if (nip47uri != null) { - try { - postViewModel.updateNIP47(nip47uri) - } catch (e: IllegalArgumentException) { - if (e.message != null) { - accountViewModel.toastManager.toast( - stringRes(context, R.string.error_parsing_nip47_title), - stringRes(context, R.string.error_parsing_nip47, nip47uri, e.message!!), - ) - } else { - accountViewModel.toastManager.toast( - stringRes(context, R.string.error_parsing_nip47_title), - stringRes(context, R.string.error_parsing_nip47_no_error, nip47uri), - ) - } - } - } - } - - var qrScanning by remember { mutableStateOf(false) } - - // Expand manual config automatically when a wallet connection exists - var showManualConfig by remember { mutableStateOf(postViewModel.walletConnectPubkey.text.isNotBlank()) } - LaunchedEffect(postViewModel.walletConnectPubkey.text) { - if (postViewModel.walletConnectPubkey.text.isNotBlank()) { - showManualConfig = true - } - } - Column( modifier = Modifier @@ -322,309 +254,6 @@ fun UpdateZapAmountContent( ) } - // ── Section 3: Nostr Wallet Connect ─────────────────────────────────── - - HorizontalDivider( - modifier = Modifier.padding(vertical = 16.dp), - thickness = DividerThickness, - ) - - // Section header + connection status indicator - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringRes(R.string.wallet_connect_service), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.titleSmall, - ) - Text( - text = stringRes(R.string.wallet_connect_service_explainer), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.padding(top = 2.dp), - ) - } - } - - // Animated connection status badge - val isConnected = postViewModel.walletConnectPubkey.text.isNotBlank() - val statusColor by animateColorAsState( - targetValue = if (isConnected) Color(0xFF4CAF50) else MaterialTheme.colorScheme.placeholderText, - animationSpec = tween(durationMillis = 400), - label = "nwc_status_color", - ) - - AnimatedContent( - targetState = isConnected, - transitionSpec = { fadeIn(tween(300)) togetherWith fadeOut(tween(300)) }, - label = "nwc_status_badge", - ) { connected -> - Row( - modifier = Modifier.padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Icon( - symbol = if (connected) MaterialSymbols.CheckCircle else MaterialSymbols.RadioButtonUnchecked, - contentDescription = null, - tint = statusColor, - modifier = Modifier.size(18.dp), - ) - Text( - text = - if (connected) { - stringRes(R.string.wallet_connect_status_connected) - } else { - stringRes(R.string.wallet_connect_status_not_connected) - }, - color = statusColor, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - - // Connect action buttons - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - // Primary "Connect Wallet" button — opens the NWC app deep link - OutlinedButton( - modifier = Modifier.weight(1f), - shape = ButtonBorder, - onClick = { - try { - uri.openUri( - "nostrnwc://connect?appname=Amethyst&appicon=https%3A%2F%2Fraw.githubusercontent.com%2Fvitorpamplona%2Famethyst%2Frefs%2Fheads%2Fmain%2Ficon.png&callback=amethyst%2Bwalletconnect%3A%2F%2Fdlnwc", - ) - onClose() - } catch (_: IllegalArgumentException) { - accountViewModel.toastManager.toast( - R.string.couldnt_find_nwc_wallets, - R.string.couldnt_find_nwc_wallets_description, - ) - } - }, - ) { - Icon( - symbol = MaterialSymbols.Add, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(modifier = Modifier.width(4.dp)) - Text(text = stringRes(R.string.wallet_connect_connect_app)) - } - - Spacer(DoubleHorzSpacer) - - // Paste from clipboard - IconButton( - onClick = { - scope.launch { - val clipText = clipboardManager.getText() - try { - clipText?.let { postViewModel.copyFromClipboard(it) } - } catch (e: IllegalArgumentException) { - accountViewModel.toastManager.toast( - R.string.invalid_nip47_uri_title, - R.string.invalid_nip47_uri_description, - clipText ?: "", - ) - } - } - }, - ) { - Icon( - symbol = MaterialSymbols.ContentPaste, - contentDescription = stringRes(id = R.string.paste_from_clipboard), - modifier = Size24Modifier, - tint = MaterialTheme.colorScheme.primary, - ) - } - - // QR code scanner - IconButton(onClick = { qrScanning = true }) { - Icon( - painter = painterRes(R.drawable.ic_qrcode, 3), - contentDescription = stringRes(id = R.string.accessibility_scan_qr_code), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - } - - if (qrScanning) { - SimpleQrCodeScanner { - qrScanning = false - if (!it.isNullOrEmpty()) { - try { - postViewModel.updateNIP47(it) - } catch (e: IllegalArgumentException) { - if (e.message != null) { - accountViewModel.toastManager.toast( - stringRes(context, R.string.error_parsing_nip47_title), - stringRes(context, R.string.error_parsing_nip47, it, e.message!!), - ) - } else { - accountViewModel.toastManager.toast( - stringRes(context, R.string.error_parsing_nip47_title), - stringRes(context, R.string.error_parsing_nip47_no_error, it), - ) - } - } - } - } - } - - // Expandable manual configuration section - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { showManualConfig = !showManualConfig } - .padding(vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(R.string.wallet_connect_manual_config), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.weight(1f), - fontSize = Font14SP, - ) - Icon( - symbol = if (showManualConfig) MaterialSymbols.ExpandLess else MaterialSymbols.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.size(20.dp), - ) - } - - AnimatedVisibility( - visible = showManualConfig, - enter = expandVertically(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)) + fadeIn(), - exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)) + fadeOut(), - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) }, - value = postViewModel.walletConnectPubkey, - onValueChange = { postViewModel.walletConnectPubkey = it }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - ), - placeholder = { - Text( - text = "npub, hex", - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - ) - } - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) }, - modifier = Modifier.fillMaxWidth(), - value = postViewModel.walletConnectRelay, - onValueChange = { postViewModel.walletConnectRelay = it }, - placeholder = { - Text( - text = "wss://relay.server.com", - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - }, - singleLine = true, - ) - } - - var showPassword by remember { mutableStateOf(false) } - - val secretContext = LocalContext.current - - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - showPassword = true - } - } - - val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret) - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) }, - modifier = Modifier.fillMaxWidth(), - value = postViewModel.walletConnectSecret, - onValueChange = { postViewModel.walletConnectSecret = it }, - keyboardOptions = - KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go, - ), - placeholder = { - Text( - text = stringRes(R.string.wallet_connect_service_secret_placeholder), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - trailingIcon = { - IconButton( - onClick = { - if (!showPassword) { - authenticate( - title = authTitle, - context = secretContext, - keyguardLauncher = keyguardLauncher, - onApproved = { showPassword = true }, - onError = { title, message -> accountViewModel.toastManager.toast(title, message) }, - ) - } else { - showPassword = false - } - }, - ) { - Icon( - symbol = - if (showPassword) { - MaterialSymbols.VisibilityOff - } else { - MaterialSymbols.Visibility - }, - contentDescription = - if (showPassword) { - stringRes(R.string.show_password) - } else { - stringRes(R.string.hide_password) - }, - ) - } - }, - visualTransformation = - if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - ) - } - } - } - trailingContent() Spacer(modifier = Modifier.height(16.dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index c503ca3f19..ecc69c47b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -122,7 +122,6 @@ fun NIP47SetupScreen( paymentTargetsViewModel.refresh() nav.popBack() }, - nip47, accountViewModel, ) { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt index 055620acd8..3c31bc59a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt @@ -79,7 +79,7 @@ fun UpdateZapAmountScreen( UpdateZapAmountContent(postViewModel, onClose = { postViewModel.cancel() nav.popBack() - }, nip47, accountViewModel) + }, accountViewModel) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt index ac9d53d132..73004d6428 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt @@ -74,12 +74,14 @@ import kotlinx.coroutines.launch fun AddNwcWalletScreen( accountViewModel: AccountViewModel, nav: INav, + nip47Uri: String? = null, ) { val walletViewModel: WalletViewModel = viewModel() walletViewModel.init(accountViewModel) var walletName by remember { mutableStateOf("") } - var nwcUri by remember { mutableStateOf("") } + // Prefilled when arriving from the `nostr+walletconnect` connect deep link. + var nwcUri by remember { mutableStateOf(nip47Uri.orEmpty()) } var error by remember { mutableStateOf(null) } var qrScanning by remember { mutableStateOf(false) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddWalletScreen.kt index b5a58153b0..0c4d4e90de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddWalletScreen.kt @@ -97,7 +97,7 @@ fun AddWalletScreen( // chooser has done its job. Without this, completing the // add-wallet form pops back to the chooser, which is a // pointless dead end. - onClick = { nav.popUpTo(Route.WalletAddNwc, Route.WalletAdd::class) }, + onClick = { nav.popUpTo(Route.WalletAddNwc(), Route.WalletAdd::class) }, ) WalletTypeCard( icon = MaterialSymbols.AccountBalanceWallet,