From 61e903a71d173859421513157292aee0d48dc60c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:12:09 +0000 Subject: [PATCH 1/2] Add built-in Tor option to login/signup flow Replace the Orbot-only toggle in the first-account signup/login screens with a full Tor selection UI showing both Built-in Tor and Orbot options. Users can now choose Built-in Tor (no external app required) or configure Orbot with a custom SOCKS port during account creation. - AccountStateViewModel: change useProxy: Boolean to torMode: TorMode in both newKey() and startUI() so the exact TorMode is persisted - LoginScreen: replace boolean switch + Orbot-only UI with a toggle that reveals Built-in Tor section (with activate button) and Orbot section (with port field and Use Orbot button) in both SignUpPage and LoginPage - Port validation only runs when TorMode.ORBOT is selected https://claude.ai/code/session_01CBvmaELoVYbgJi4Wi8fgrG --- .../nostrsigner/ui/AccountStateViewModel.kt | 8 +- .../greenart7c3/nostrsigner/ui/LoginScreen.kt | 128 +++++++++++++++--- 2 files changed, 112 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index 2feb5ad7..243b452b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -96,7 +96,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { suspend fun startUI( keyPair: KeyPair, route: String?, - useProxy: Boolean, + torMode: TorMode, proxyPort: Int, signPolicy: Int, ) { @@ -112,7 +112,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { if (LocalPreferences.allSavedAccounts(Amber.instance).isEmpty()) { Amber.instance.settings = Amber.instance.settings.copy( - torMode = if (useProxy) TorMode.ORBOT else TorMode.DISABLED, + torMode = torMode, proxyPort = proxyPort, ) LocalPreferences.saveSettingsToEncryptedStorage( @@ -124,7 +124,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { } suspend fun newKey( - useProxy: Boolean, + torMode: TorMode, proxyPort: Int, signPolicy: Int, seedWords: Set, @@ -146,7 +146,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { ) if (LocalPreferences.allSavedAccounts(Amber.instance).isEmpty()) { Amber.instance.settings = Amber.instance.settings.copy( - torMode = if (useProxy) TorMode.ORBOT else TorMode.DISABLED, + torMode = torMode, proxyPort = proxyPort, ) LocalPreferences.saveSettingsToEncryptedStorage( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt index 425932c4..136c3ee5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -104,6 +105,7 @@ import androidx.navigation.compose.composable import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.TorMode import com.greenart7c3.nostrsigner.service.AccountExportService import com.greenart7c3.nostrsigner.ui.components.AmberButton import com.greenart7c3.nostrsigner.ui.components.AmberElevatedButton @@ -589,7 +591,7 @@ fun SignUpPage( ), ) var selectedOption by remember { mutableIntStateOf(0) } - var useProxy by remember { mutableStateOf(false) } + var torMode by remember { mutableStateOf(TorMode.DISABLED) } var proxyPort by remember { mutableStateOf(TextFieldValue("9050")) } Column( Modifier @@ -660,25 +662,58 @@ fun SignUpPage( modifier = Modifier .padding(vertical = 20.dp) .clickable { - useProxy = !useProxy + torMode = if (torMode == TorMode.DISABLED) TorMode.BUILTIN else TorMode.DISABLED }, ) { Switch( modifier = Modifier.scale(0.85f), - checked = useProxy, + checked = torMode != TorMode.DISABLED, onCheckedChange = { value -> - useProxy = value + torMode = if (value) TorMode.BUILTIN else TorMode.DISABLED }, ) Text( modifier = Modifier .weight(1f) .padding(start = 8.dp), - text = stringResource(R.string.connect_through_your_orbot_setup), + text = stringResource(R.string.connect_via_tor_short), ) } - if (useProxy) { + if (torMode != TorMode.DISABLED) { + Text( + text = stringResource(R.string.builtin_tor_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.builtin_tor_description), + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.height(8.dp)) + AmberButton( + modifier = Modifier.fillMaxWidth(), + onClick = { torMode = TorMode.BUILTIN }, + text = if (torMode == TorMode.BUILTIN) { + stringResource(R.string.builtin_tor_active) + } else { + stringResource(R.string.use_builtin_tor) + }, + enabled = torMode != TorMode.BUILTIN, + ) + + Spacer(modifier = Modifier.height(24.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.orbot_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(8.dp)) + val myMarkDownStyle = RichTextDefaults.copy( stringStyle = RichTextDefaults.stringStyle?.copy( @@ -730,7 +765,17 @@ fun SignUpPage( ), modifier = Modifier .fillMaxWidth() - .padding(bottom = 20.dp), + .padding(bottom = 8.dp), + ) + AmberButton( + modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp), + onClick = { torMode = TorMode.ORBOT }, + text = if (torMode == TorMode.ORBOT) { + stringResource(R.string.use_orbot) + " ✓" + } else { + stringResource(R.string.use_orbot) + }, + enabled = torMode != TorMode.ORBOT, ) } } @@ -738,7 +783,7 @@ fun SignUpPage( modifier = Modifier .padding(vertical = 20.dp), onClick = { - if (proxyPort.text.toIntOrNull() == null) { + if (torMode == TorMode.ORBOT && proxyPort.text.toIntOrNull() == null) { Toast.makeText( context, "Invalid port number", @@ -750,9 +795,9 @@ fun SignUpPage( Amber.instance.applicationIOScope.launch { loading = true accountViewModel.newKey( - useProxy = useProxy, + torMode = torMode, signPolicy = selectedOption, - proxyPort = proxyPort.text.toInt(), + proxyPort = if (torMode == TorMode.ORBOT) proxyPort.text.toInt() else 9050, seedWords = seedWords, name = nickname.text, ) @@ -1116,7 +1161,7 @@ fun LoginPage( ), ) var selectedOption by remember { mutableIntStateOf(0) } - var useProxy by remember { mutableStateOf(false) } + var torMode by remember { mutableStateOf(TorMode.DISABLED) } var proxyPort by remember { mutableStateOf(TextFieldValue("9050")) } val scrollState = rememberScrollState() @@ -1188,25 +1233,58 @@ fun LoginPage( modifier = Modifier .padding(vertical = 20.dp) .clickable { - useProxy = !useProxy + torMode = if (torMode == TorMode.DISABLED) TorMode.BUILTIN else TorMode.DISABLED }, ) { Switch( modifier = Modifier.scale(0.85f), - checked = useProxy, + checked = torMode != TorMode.DISABLED, onCheckedChange = { value -> - useProxy = value + torMode = if (value) TorMode.BUILTIN else TorMode.DISABLED }, ) Text( modifier = Modifier .weight(1f) .padding(start = 8.dp), - text = stringResource(R.string.connect_through_your_orbot_setup), + text = stringResource(R.string.connect_via_tor_short), ) } - if (useProxy) { + if (torMode != TorMode.DISABLED) { + Text( + text = stringResource(R.string.builtin_tor_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.builtin_tor_description), + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.height(8.dp)) + AmberButton( + modifier = Modifier.fillMaxWidth(), + onClick = { torMode = TorMode.BUILTIN }, + text = if (torMode == TorMode.BUILTIN) { + stringResource(R.string.builtin_tor_active) + } else { + stringResource(R.string.use_builtin_tor) + }, + enabled = torMode != TorMode.BUILTIN, + ) + + Spacer(modifier = Modifier.height(24.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.orbot_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(8.dp)) + val myMarkDownStyle = RichTextDefaults.copy( stringStyle = RichTextDefaults.stringStyle?.copy( @@ -1258,7 +1336,17 @@ fun LoginPage( ), modifier = Modifier .fillMaxWidth() - .padding(bottom = 20.dp), + .padding(bottom = 8.dp), + ) + AmberButton( + modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp), + onClick = { torMode = TorMode.ORBOT }, + text = if (torMode == TorMode.ORBOT) { + stringResource(R.string.use_orbot) + " ✓" + } else { + stringResource(R.string.use_orbot) + }, + enabled = torMode != TorMode.ORBOT, ) } } @@ -1266,7 +1354,7 @@ fun LoginPage( modifier = Modifier .padding(vertical = 20.dp), onClick = { - if (proxyPort.text.toIntOrNull() == null) { + if (torMode == TorMode.ORBOT && proxyPort.text.toIntOrNull() == null) { Toast.makeText( context, "Invalid port number", @@ -1280,9 +1368,9 @@ fun LoginPage( accountViewModel.startUI( keyPair = keyPair, route = null, - useProxy = useProxy, + torMode = torMode, signPolicy = selectedOption, - proxyPort = proxyPort.text.toInt(), + proxyPort = if (torMode == TorMode.ORBOT) proxyPort.text.toInt() else 9050, ) isLoading = false Amber.instance.applicationIOScope.launch(Dispatchers.Main) { From 8620ddfe014edb144f391a0dc1047dc0267917b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:31:08 +0000 Subject: [PATCH 2/2] Start built-in Tor when BUILTIN mode is selected at login/signup When the first account is created with TorMode.BUILTIN selected, call TorManager.start() immediately after saving settings so Tor begins connecting before the app navigates to the main screen. This mirrors the same call made in the Settings screen handler. https://claude.ai/code/session_01CBvmaELoVYbgJi4Wi8fgrG --- .../greenart7c3/nostrsigner/ui/AccountStateViewModel.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index 243b452b..34894f03 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -7,6 +7,7 @@ import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.TorMode +import com.greenart7c3.nostrsigner.service.TorManager import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -118,6 +119,9 @@ class AccountStateViewModel(npub: String?) : ViewModel() { LocalPreferences.saveSettingsToEncryptedStorage( Amber.instance.settings, ) + if (torMode == TorMode.BUILTIN) { + TorManager.start(Amber.instance, Amber.instance.applicationIOScope) + } } LocalPreferences.updatePrefsForLogin(Amber.instance, account, keyPair.pubKey.toHexKey(), keyPair.privKey!!.toHexKey(), null) startUI(account, route) @@ -152,6 +156,9 @@ class AccountStateViewModel(npub: String?) : ViewModel() { LocalPreferences.saveSettingsToEncryptedStorage( Amber.instance.settings, ) + if (torMode == TorMode.BUILTIN) { + TorManager.start(Amber.instance, Amber.instance.applicationIOScope) + } Amber.instance.applicationIOScope.launch { Amber.instance.reconnect() }