diff --git a/app/build.gradle b/app/build.gradle
index 15286a17..8662a991 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -249,4 +249,6 @@ dependencies {
implementation libs.coil.svg
// enables network for coil
implementation libs.coil.okhttp
+
+ implementation libs.storage
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5e5e263a..e247297b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,8 @@
+
+
>,
navController: NavHostController,
+ storageHelper: SimpleStorageHelper,
) {
val accountState by accountStateViewModel.accountContent.collectAsState()
val intents by flow.collectAsState(initial = emptyList())
@@ -72,7 +75,8 @@ fun AccountScreen(
) { state ->
when (state) {
is AccountState.LoggedOff -> {
- MainLoginPage(accountStateViewModel)
+ val newNavController = rememberNavController()
+ MainLoginPage(accountStateViewModel, storageHelper, newNavController)
}
is AccountState.LoggedIn -> {
LaunchedEffect(intent, intents) {
@@ -133,6 +137,7 @@ fun AccountScreen(
localRoute,
database,
navController,
+ storageHelper,
onRemoveIntentData = {
val oldIntents = intents.toMutableList()
oldIntents.remove(it)
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 12f9d542..94940cee 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt
@@ -250,12 +250,13 @@ class AccountStateViewModel(npub: String?) : ViewModel() {
proxyPort: Int,
signPolicy: Int,
seedWords: Set,
+ name: String,
) {
val key = seedWords.joinToString(separator = " ") { it }
val keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(key))
val account = Account(
NostrSignerInternal(keyPair),
- name = "",
+ name = name,
useProxy = useProxy,
proxyPort = proxyPort,
language = null,
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 11902c36..4a3166ff 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt
@@ -1,6 +1,5 @@
package com.greenart7c3.nostrsigner.ui
-import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -14,15 +13,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -30,8 +26,8 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
+import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -39,10 +35,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -55,490 +52,824 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillNode
import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.ParagraphStyle
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.buildAnnotatedString
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.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.toLowerCase
+import androidx.compose.ui.text.toUpperCase
+import androidx.compose.ui.text.withLink
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.greenart7c3.nostrsigner.BuildConfig
+import androidx.navigation.NavController
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import com.anggrayudi.storage.SimpleStorageHelper
+import com.anggrayudi.storage.file.CreateMode
+import com.anggrayudi.storage.file.makeFile
+import com.anggrayudi.storage.file.openOutputStream
import com.greenart7c3.nostrsigner.R
-import com.greenart7c3.nostrsigner.service.PackageUtils
-import com.greenart7c3.nostrsigner.ui.actions.ConnectOrbotDialog
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.AmberElevatedButton
-import com.greenart7c3.nostrsigner.ui.components.SeedWordsPage
+import com.greenart7c3.nostrsigner.ui.components.IconRow
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
+import com.greenart7c3.nostrsigner.ui.theme.RichTextDefaults
+import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
+import com.halilibo.richtext.commonmark.MarkdownParseOptions
+import com.halilibo.richtext.markdown.BasicMarkdown
+import com.halilibo.richtext.ui.material3.RichText
+import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.CryptoUtils.random
+import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.crypto.nip06.Bip39Mnemonics
+import com.vitorpamplona.quartz.encoders.toHexKey
+import com.vitorpamplona.quartz.encoders.toNpub
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-@Composable
-fun AmberLogo(size: Int) {
- Image(
- painterResource(id = R.mipmap.ic_launcher_foreground),
- contentDescription = "Amber",
- modifier = Modifier
- .size(size.dp)
- .clip(CircleShape)
- .background(colorResource(id = R.color.amber)),
- )
-}
-
@Composable
fun MainPage(
scope: CoroutineScope,
- state: PagerState,
+ navController: NavController,
) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding()
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.Center,
- ) {
- Box(modifier = Modifier.height(0.dp))
+ val configuration = LocalConfiguration.current
+ val screenWidthDp = configuration.screenWidthDp.dp
+ val percentage = (screenWidthDp * 0.93f)
+ val verticalPadding = (screenWidthDp - percentage)
+ val context = LocalContext.current
+ Scaffold {
Column(
modifier = Modifier
- .padding(20.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
+ .fillMaxSize()
+ .padding(it)
+ .padding(horizontal = verticalPadding)
+ .padding(top = verticalPadding * 1.5f),
) {
- AmberLogo(200)
-
- Spacer(modifier = Modifier.height(40.dp))
-
Column(
- modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- AmberElevatedButton(
- onClick = {
- scope.launch {
- state.animateScrollToPage(1)
- }
- },
- text = stringResource(R.string.add_a_key),
+ Text(
+ text = stringResource(R.string.app_name_release).toUpperCase(Locale.current),
+ fontSize = 36.sp,
+ )
+ Text(
+ text = stringResource(R.string.a_nostr_secure_signer),
+ fontSize = 16.sp,
+ color = Color(0xFFC98500),
)
- AmberButton(
- onClick = {
- scope.launch {
- state.animateScrollToPage(2)
+ Image(
+ ImageVector.vectorResource(R.drawable.frame),
+ contentDescription = "Logo",
+ )
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AmberElevatedButton(
+ onClick = {
+ scope.launch {
+ navController.navigate("loginPage")
+ }
+ },
+ text = stringResource(R.string.add_a_key),
+ )
+
+ AmberButton(
+ onClick = {
+ scope.launch {
+ navController.navigate("create")
+ }
+ },
+ text = stringResource(R.string.generate_a_new_key),
+ )
+ }
+
+ Spacer(Modifier.weight(1f))
+
+ Text(
+ modifier = Modifier.padding(bottom = 20.dp),
+ color = Color(0xFF8C8C8C),
+ text = buildAnnotatedString {
+ withStyle(
+ style = ParagraphStyle(
+ textAlign = TextAlign.Center,
+ ),
+ ) {
+ append("Amber is a free and open source project\n")
+ withLink(
+ LinkAnnotation.Url(
+ context.getString(R.string.amber_github_uri),
+ styles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ ),
+ ),
+ ),
+ ) {
+ append(context.getString(R.string.check_the_code))
+ }
}
},
- text = stringResource(R.string.generate_a_new_key),
)
}
}
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainLoginPage(
accountViewModel: AccountStateViewModel,
+ storageHelper: SimpleStorageHelper,
+ navController: NavHostController,
) {
val scope = rememberCoroutineScope()
+
+ NavHost(
+ navController,
+ startDestination = "login",
+ ) {
+ composable(
+ "login",
+ content = {
+ MainPage(
+ scope = scope,
+ navController = navController,
+ )
+ },
+ )
+
+ composable(
+ "create",
+ content = {
+ SignUpPage(
+ accountViewModel = accountViewModel,
+ scope = scope,
+ navController = navController,
+ storageHelper = storageHelper,
+ )
+ },
+ )
+
+ composable(
+ "loginPage",
+ content = {
+ LoginPage(
+ accountViewModel = accountViewModel,
+ navController = navController,
+ )
+ },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SignUpPage(
+ accountViewModel: AccountStateViewModel,
+ scope: CoroutineScope,
+ navController: NavController,
+ storageHelper: SimpleStorageHelper,
+) {
+ var loading by remember { mutableStateOf(false) }
+ val configuration = LocalConfiguration.current
+ val screenWidthDp = configuration.screenWidthDp.dp
+ val percentage = (screenWidthDp * 0.93f)
+ val verticalPadding = (screenWidthDp - percentage)
+ var nickname by remember { mutableStateOf(TextFieldValue()) }
+ var password by remember { mutableStateOf(TextFieldValue()) }
+ var password2 by remember { mutableStateOf(TextFieldValue()) }
+ var keyPair by remember { mutableStateOf(KeyPair()) }
val state = rememberPagerState {
3
}
+ val context = LocalContext.current
+ var checked by remember { mutableStateOf(false) }
+ var seedWords by remember { mutableStateOf(setOf()) }
+ var enabled by remember { mutableStateOf(false) }
+ storageHelper.onFolderSelected = { _, folder ->
+ val ncryptsec = CryptoUtils.nip49.encrypt(keyPair.privKey!!.toHexKey(), password.text)
+ val ncryptsecFile = folder.makeFile(
+ context,
+ mimeType = "application/text",
+ name = "${nickname.text}.ncryptsec",
+ mode = CreateMode.REPLACE,
+ )
+ ncryptsecFile?.let {
+ it.openOutputStream(context, append = false).use { stream ->
+ stream?.write(ncryptsec.toByteArray())
+ }
+ }
+ enabled = true
+ }
+
+ LaunchedEffect(Unit) {
+ launch(Dispatchers.IO) {
+ loading = true
+ while (seedWords.size < 12) {
+ val entropy = random(16)
+ seedWords = Bip39Mnemonics.toMnemonics(entropy).toSet()
+ keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(seedWords.joinToString(" ")))
+ }
+ loading = false
+ }
+ }
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
- Text(text = stringResource(R.string.app_name))
+ when (state.currentPage) {
+ 0 -> {
+ Text(text = stringResource(R.string.generate_a_new_key))
+ }
+ 1 -> {
+ Text(text = stringResource(R.string.backup_the_nostr_account))
+ }
+ else -> {
+ Text(text = stringResource(R.string.permissions_and_connection))
+ }
+ }
},
)
},
- ) { innerPadding ->
- HorizontalPager(
- modifier = Modifier
- .padding(innerPadding)
- .padding(40.dp)
- .fillMaxSize(),
- state = state,
- userScrollEnabled = false,
- ) {
- when (it) {
- 0 -> {
- MainPage(
- scope = scope,
- state = state,
- )
- }
- 1 -> {
- LoginPage(
- accountViewModel = accountViewModel,
- )
- }
- 2 -> {
- SignUpPage(
- accountViewModel = accountViewModel,
- scope = scope,
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun SignUpPage(
- accountViewModel: AccountStateViewModel,
- scope: CoroutineScope,
-) {
- val useProxy = remember { mutableStateOf(false) }
- val proxyPort = remember { mutableStateOf("9050") }
- val connectOrbotDialogOpen = remember { mutableStateOf(false) }
- var seedWords by remember { mutableStateOf(setOf()) }
- val pageState = rememberPagerState {
- 3
- }
- val context = LocalContext.current
-
- LaunchedEffect(Unit) {
- launch(Dispatchers.IO) {
- val entropy = random(16)
- seedWords = Bip39Mnemonics.toMnemonics(entropy).toSet()
- }
- }
-
- HorizontalPager(
- state = pageState,
- modifier = Modifier.fillMaxSize(),
- userScrollEnabled = false,
- ) { currentPage ->
- when (currentPage) {
- 0 -> {
- SeedWordsPage(
- seedWords = seedWords,
- onNextPage = {
- scope.launch {
- @Suppress("KotlinConstantConditions")
- if (BuildConfig.FLAVOR != "offline" && PackageUtils.isOrbotInstalled(context)) {
- pageState.animateScrollToPage(1)
- } else {
- pageState.animateScrollToPage(2)
+ bottomBar = {
+ BottomAppBar {
+ IconRow(
+ center = true,
+ title = stringResource(R.string.go_back),
+ icon = ImageVector.vectorResource(R.drawable.back),
+ onClick = {
+ if (state.currentPage > 0) {
+ scope.launch {
+ state.animateScrollToPage(state.currentPage - 1)
+ }
+ } else {
+ scope.launch {
+ navController.navigateUp()
}
}
},
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
- 1 -> {
- OrbotPage(
- connectOrbotDialogOpen = connectOrbotDialogOpen,
- useProxy = useProxy,
- proxyPort = proxyPort,
- pageState = pageState,
- scope = scope,
- context = context,
- )
- }
- else -> {
- SignPolicyScreen(
- accountViewModel = accountViewModel,
- key = "",
- password = "",
- useProxy = useProxy.value,
- proxyPort = proxyPort.value.toInt(),
- seedWords = seedWords,
- )
- }
- }
- }
-}
-
-@Composable
-fun OrbotPage(
- connectOrbotDialogOpen: MutableState,
- useProxy: MutableState,
- proxyPort: MutableState,
- pageState: PagerState,
- scope: CoroutineScope,
- context: Context,
-) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding()
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
+ },
) {
- AmberLogo(size = 150)
-
- Spacer(modifier = Modifier.height(40.dp))
-
- @Suppress("KotlinConstantConditions")
- if (BuildConfig.FLAVOR != "offline" && PackageUtils.isOrbotInstalled(context)) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
+ if (loading) {
+ CenterCircularProgressIndicator(Modifier.fillMaxSize())
+ } else {
+ HorizontalPager(
modifier = Modifier
- .clickable {
- if (!useProxy.value) {
- connectOrbotDialogOpen.value = true
- } else {
- useProxy.value = false
- }
- },
- ) {
- Checkbox(
- checked = useProxy.value,
- onCheckedChange = { value ->
- if (value) {
- connectOrbotDialogOpen.value = true
- } else {
- useProxy.value = false
- }
- },
- )
-
- Text(stringResource(R.string.connect_through_your_orbot_setup))
- }
-
- if (connectOrbotDialogOpen.value) {
- ConnectOrbotDialog(
- onClose = { connectOrbotDialogOpen.value = false },
- onPost = {
- connectOrbotDialogOpen.value = false
- useProxy.value = true
- },
- onError = {
- scope.launch {
- Toast.makeText(
- context,
- it,
- Toast.LENGTH_LONG,
- )
- .show()
- }
- },
- proxyPort,
- )
- }
-
- Spacer(modifier = Modifier.height(20.dp))
-
- AmberButton(
- onClick = {
- scope.launch {
- pageState.animateScrollToPage(2)
- }
- },
- text = stringResource(R.string.next),
- )
- }
- }
-}
-
-@Composable
-fun SignPolicyScreen(
- accountViewModel: AccountStateViewModel,
- key: String,
- password: String,
- useProxy: Boolean,
- proxyPort: Int,
- seedWords: Set,
-) {
- val radioOptions = listOf(
- TitleExplainer(
- title = stringResource(R.string.sign_policy_basic),
- explainer = stringResource(R.string.sign_policy_basic_explainer),
- ),
- TitleExplainer(
- title = stringResource(R.string.sign_policy_manual),
- explainer = stringResource(R.string.sign_policy_manual_explainer),
- ),
- )
- var selectedOption by remember { mutableIntStateOf(0) }
- Column(
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding()
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.Center,
- ) {
- Box(modifier = Modifier.height(0.dp))
-
- Column(
- modifier = Modifier
- .padding(20.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- AmberLogo(size = 150)
- Spacer(modifier = Modifier.height(40.dp))
-
- Text(
- text = stringResource(R.string.sign_policy_title),
- style = MaterialTheme.typography.titleLarge,
- fontSize = 28.sp,
- )
-
- Spacer(modifier = Modifier.height(4.dp))
-
- Text(
- text = stringResource(R.string.sign_policy_explainer),
- textAlign = TextAlign.Center,
- )
-
- Spacer(modifier = Modifier.height(40.dp))
-
- radioOptions.forEachIndexed { index, option ->
- Row(
- Modifier
- .fillMaxWidth()
- .selectable(
- selected = selectedOption == index,
- onClick = {
- selectedOption = index
- },
- )
- .border(
- width = 1.dp,
- color = if (selectedOption == index) {
- MaterialTheme.colorScheme.primary
- } else {
- Color.Transparent
- },
- shape = RoundedCornerShape(8.dp),
- )
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- RadioButton(
- selected = selectedOption == index,
- onClick = {
- selectedOption = index
- },
- )
- Column(
- verticalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- Text(
- text = option.title,
- modifier = Modifier.padding(start = 16.dp),
- style = MaterialTheme.typography.titleLarge,
- )
- option.explainer?.let {
+ .fillMaxSize(),
+ state = state,
+ userScrollEnabled = false,
+ ) { page ->
+ when (page) {
+ 0 -> {
+ var showPassword by remember { mutableStateOf(false) }
+ Column(
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(it)
+ .padding(horizontal = verticalPadding)
+ .padding(top = verticalPadding * 1.5f),
+ ) {
Text(
- text = it,
- modifier = Modifier.padding(start = 16.dp),
+ text = stringResource(R.string.your_nostr_account_is_ready),
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp),
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color(0xFFFFDE9E)),
+ ) {
+ Text(
+ text = keyPair.pubKey.toNpub(),
+ Modifier.padding(10.dp),
+ )
+ }
+ Spacer(Modifier.height(8.dp))
+ Text(stringResource(R.string.you_will_find_it_in_your_account_section_so_you_don_t_need_to_copy_it_right_now))
+
+ Text(
+ text = stringResource(R.string.please_add_a_nickname_later_you_will_able_to_personalise_your_full_profile_bio_pic_etc_using_your_preferred_nostr_client),
+ modifier = Modifier.padding(top = 20.dp, bottom = 8.dp),
+ )
+ OutlinedTextField(
+ value = nickname,
+ onValueChange = { value ->
+ nickname = value
+ },
+ placeholder = {
+ Text(
+ stringResource(R.string.nickname),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ imeAction = ImeAction.Next,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Text(
+ text = stringResource(R.string.now_pick_a_password_we_will_use_it_to_encrypt_your_private_key_named_ncryptsec_so_you_can_store_it_safely),
+ modifier = Modifier.padding(top = 20.dp, bottom = 8.dp),
+ )
+ OutlinedTextField(
+ value = password,
+ onValueChange = { value ->
+ password = value
+ },
+ visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingIcon = {
+ Row {
+ IconButton(onClick = { showPassword = !showPassword }) {
+ Icon(
+ imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
+ contentDescription = if (showPassword) {
+ stringResource(R.string.show_password)
+ } else {
+ stringResource(R.string.hide_password)
+ },
+ )
+ }
+ }
+ },
+ placeholder = {
+ Text(
+ stringResource(R.string.password),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ keyboardType = KeyboardType.Password,
+ autoCorrectEnabled = false,
+ imeAction = ImeAction.Next,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ AmberButton(
+ modifier = Modifier.padding(vertical = 40.dp),
+ onClick = {
+ if (nickname.text.isBlank()) {
+ Toast.makeText(
+ context,
+ "Nickname is required",
+ Toast.LENGTH_SHORT,
+ ).show()
+ return@AmberButton
+ }
+ if (password.text.isBlank()) {
+ Toast.makeText(
+ context,
+ "Password is required",
+ Toast.LENGTH_SHORT,
+ ).show()
+ return@AmberButton
+ }
+ scope.launch {
+ state.animateScrollToPage(1)
+ }
+ },
+ text = stringResource(R.string.continue_button),
+ )
+ }
+ }
+
+ 1 -> {
+ var showPassword by remember { mutableStateOf(false) }
+ Column(
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(it)
+ .padding(horizontal = verticalPadding)
+ .padding(top = verticalPadding * 1.5f),
+ ) {
+ Text(stringResource(R.string.please_download_your_account_backup_kit_containing_the_encrypted_private_key_and_store_it_in_a_couple_of_safe_places))
+ Spacer(Modifier.height(8.dp))
+ Text(stringResource(R.string.please_enter_the_password_chosen_in_the_previous_step_this_prove_you_typed_and_remembered_it_correctly))
+ Spacer(Modifier.height(8.dp))
+ OutlinedTextField(
+ value = password2,
+ onValueChange = { value ->
+ password2 = value
+ },
+ trailingIcon = {
+ Row {
+ IconButton(onClick = { showPassword = !showPassword }) {
+ Icon(
+ imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
+ contentDescription = if (showPassword) {
+ stringResource(R.string.show_password)
+ } else {
+ stringResource(R.string.hide_password)
+ },
+ )
+ }
+ }
+ },
+ placeholder = {
+ Text(
+ stringResource(R.string.password),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Password,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+ Spacer(Modifier.height(8.dp))
+ AmberButton(
+ onClick = {
+ if (password2.text != password.text) {
+ Toast.makeText(
+ context,
+ "Passwords do not match",
+ Toast.LENGTH_SHORT,
+ ).show()
+ return@AmberButton
+ }
+ storageHelper.openFolderPicker()
+ },
+ text = stringResource(R.string.download_backup_kit),
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(vertical = 20.dp)
+ .clickable {
+ checked = !checked
+ },
+ ) {
+ Switch(
+ modifier = Modifier.scale(0.85f),
+ checked = checked,
+ onCheckedChange = {
+ checked = it
+ },
+ )
+ Text(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 8.dp),
+ text = stringResource(R.string.i_want_also_to_download_the_seed_version_of_my_private_key),
+ )
+ }
+ if (checked) {
+ Text(
+ stringResource(R.string.this_is_an_optional_additional_way_to_backup_your_private_key_write_down_on_paper_the_12_words_keeping_the_correct_order),
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ val firstColumnWords = seedWords.filterIndexed { index, _ -> index % 2 == 0 }
+ val secondColumnWords = seedWords.filterIndexed { index, _ -> index % 2 != 0 }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ firstColumnWords.forEachIndexed { index, word ->
+ OutlinedTextField(
+ value = word,
+ onValueChange = {},
+ modifier = Modifier.padding(8.dp),
+ readOnly = true,
+ prefix = { Text("${index * 2 + 1} - ") },
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ secondColumnWords.forEachIndexed { index, word ->
+ OutlinedTextField(
+ value = word,
+ onValueChange = {},
+ modifier = Modifier.padding(8.dp),
+ readOnly = true,
+ prefix = { Text("${index * 2 + 2} - ") },
+ )
+ }
+ }
+ }
+ }
+ AmberButton(
+ enabled = enabled,
+ onClick = {
+ scope.launch {
+ state.animateScrollToPage(2)
+ }
+ },
+ text = stringResource(R.string.continue_button),
+ )
+ }
+ }
+
+ 2 -> {
+ val radioOptions = listOf(
+ TitleExplainer(
+ title = stringResource(R.string.sign_policy_basic),
+ explainer = stringResource(R.string.sign_policy_basic_explainer),
+ ),
+ TitleExplainer(
+ title = stringResource(R.string.sign_policy_manual),
+ explainer = stringResource(R.string.sign_policy_manual_explainer),
+ ),
+ )
+ var selectedOption by remember { mutableIntStateOf(0) }
+ var useProxy by remember { mutableStateOf(false) }
+ var proxyPort by remember { mutableStateOf(TextFieldValue("9050")) }
+ Column(
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(it)
+ .padding(horizontal = verticalPadding)
+ .padding(top = verticalPadding * 1.5f),
+ ) {
+ Text(
+ text = stringResource(R.string.sign_policy_explainer),
+ )
+ Column(
+ Modifier.padding(vertical = 10.dp),
+ ) {
+ radioOptions.forEachIndexed { index, option ->
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = selectedOption == index,
+ onClick = {
+ selectedOption = index
+ },
+ )
+ .border(
+ width = 1.dp,
+ color = if (selectedOption == index) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ Color.Transparent
+ },
+ shape = RoundedCornerShape(8.dp),
+ )
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ RadioButton(
+ selected = selectedOption == index,
+ onClick = {
+ selectedOption = index
+ },
+ )
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = option.title,
+ modifier = Modifier.padding(start = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ option.explainer?.let {
+ Text(
+ text = it,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ }
+ }
+ }
+ }
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(vertical = 20.dp)
+ .clickable {
+ useProxy = !useProxy
+ },
+ ) {
+ Switch(
+ modifier = Modifier.scale(0.85f),
+ checked = useProxy,
+ onCheckedChange = {
+ useProxy = it
+ },
+ )
+ Text(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 8.dp),
+ text = stringResource(R.string.connect_through_your_orbot_setup),
+ )
+ }
+
+ if (useProxy) {
+ val myMarkDownStyle =
+ RichTextDefaults.copy(
+ stringStyle = RichTextDefaults.stringStyle?.copy(
+ linkStyle =
+ SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary,
+ ),
+ ),
+ )
+ val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown2)
+
+ val astNode1 =
+ remember {
+ CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
+ }
+
+ RichText(
+ modifier = Modifier.padding(vertical = 8.dp),
+ style = myMarkDownStyle,
+ renderer = null,
+ ) {
+ BasicMarkdown(astNode1)
+ }
+
+ OutlinedTextField(
+ value = proxyPort,
+ onValueChange = { value ->
+ proxyPort = value
+ },
+ label = {
+ Text(
+ text = stringResource(R.string.orbot_socks_port),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ placeholder = {
+ Text(
+ stringResource(R.string.orbot_socks_port),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Number,
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ )
+ }
+ AmberButton(
+ modifier = Modifier
+ .padding(vertical = 20.dp),
+ onClick = {
+ if (proxyPort.text.toIntOrNull() == null) {
+ Toast.makeText(
+ context,
+ "Invalid port number",
+ Toast.LENGTH_SHORT,
+ ).show()
+ return@AmberButton
+ }
+
+ accountViewModel.newKey(
+ useProxy = useProxy,
+ signPolicy = selectedOption,
+ proxyPort = proxyPort.text.toInt(),
+ seedWords = seedWords,
+ name = nickname.text,
+ )
+ },
+ text = stringResource(R.string.finish),
)
}
}
}
}
- Spacer(modifier = Modifier.height(40.dp))
-
- AmberButton(
- onClick = {
- if (key.isBlank()) {
- accountViewModel.newKey(
- useProxy = useProxy,
- proxyPort = proxyPort,
- signPolicy = selectedOption,
- seedWords = seedWords,
- )
- } else {
- accountViewModel.startUI(
- key,
- password,
- null,
- useProxy = useProxy,
- proxyPort = proxyPort,
- signPolicy = selectedOption,
- )
- }
- },
- text = stringResource(R.string.finish),
- )
}
}
}
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
-fun LoginPage(accountViewModel: AccountStateViewModel) {
- val pageState = rememberPagerState {
- 2
- }
-
- val key = remember { mutableStateOf(TextFieldValue("")) }
+fun LoginPage(
+ accountViewModel: AccountStateViewModel,
+ navController: NavController,
+) {
+ val configuration = LocalConfiguration.current
+ val screenWidthDp = configuration.screenWidthDp.dp
+ val percentage = (screenWidthDp * 0.93f)
+ val verticalPadding = (screenWidthDp - percentage)
+ val key = remember { mutableStateOf(TextFieldValue()) }
+ var dialogOpen by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
- var dialogOpen by remember {
- mutableStateOf(false)
- }
- val context = LocalContext.current
- val password = remember { mutableStateOf(TextFieldValue("")) }
val needsPassword =
remember {
derivedStateOf {
key.value.text.startsWith("ncryptsec1")
}
}
- val useProxy = remember { mutableStateOf(false) }
- val proxyPort = remember { mutableStateOf("9050") }
- var connectOrbotDialogOpen by remember { mutableStateOf(false) }
+ val password = remember { mutableStateOf(TextFieldValue()) }
+ val context = LocalContext.current
+ val pageState = rememberPagerState {
+ 2
+ }
val scope = rememberCoroutineScope()
- HorizontalPager(
- state = pageState,
- modifier = Modifier.fillMaxSize(),
- ) { currentPage ->
- when (currentPage) {
- 0 -> {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding()
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.Center,
- ) {
- // The first child is glued to the top.
- // Hence we have nothing at the top, an empty box is used.
- Box(modifier = Modifier.height(0.dp))
-
- // The second child, this column, is centered vertically.
+ Scaffold(
+ topBar = {
+ CenterAlignedTopAppBar(
+ title = {
+ if (pageState.currentPage == 0) {
+ Text(text = stringResource(R.string.add_a_key))
+ } else {
+ Text(text = stringResource(R.string.permissions_and_connection))
+ }
+ },
+ )
+ },
+ bottomBar = {
+ BottomAppBar {
+ IconRow(
+ center = true,
+ title = stringResource(R.string.go_back),
+ icon = ImageVector.vectorResource(R.drawable.back),
+ onClick = {
+ if (pageState.currentPage > 0) {
+ scope.launch {
+ pageState.animateScrollToPage(pageState.currentPage - 1)
+ }
+ } else {
+ scope.launch {
+ navController.navigateUp()
+ }
+ }
+ },
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ },
+ ) {
+ HorizontalPager(
+ pageState,
+ userScrollEnabled = false,
+ ) { page ->
+ when (page) {
+ 0 -> {
Column(
modifier = Modifier
- .padding(20.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(it)
+ .padding(horizontal = verticalPadding)
+ .padding(top = verticalPadding * 1.5f),
) {
- Image(
- painterResource(id = R.mipmap.ic_launcher_foreground),
- contentDescription = "NostrSigner",
- modifier = Modifier
- .size(150.dp)
- .clip(CircleShape)
- .background(colorResource(id = R.color.amber)),
- )
-
- Spacer(modifier = Modifier.height(40.dp))
-
var showPassword by remember {
mutableStateOf(false)
}
@@ -562,13 +893,9 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
LocalAutofillTree.current += autofillNodePassword
Text(
- stringResource(R.string.add_your_key),
- fontWeight = FontWeight.Bold,
- fontSize = 24.sp,
+ stringResource(R.string.setup_amber_with_your_nostr_private_key_you_can_enter_different_versions_nsec_ncryptsec_or_hex_you_can_also_scan_it_from_a_qr_code),
)
- Spacer(modifier = Modifier.height(40.dp))
-
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
@@ -583,7 +910,8 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
cancelAutofillForNode(autofillNodeKey)
}
}
- },
+ }
+ .padding(vertical = 20.dp),
shape = RoundedCornerShape(18.dp),
value = key.value,
onValueChange = { key.value = it },
@@ -660,8 +988,6 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
)
}
- Spacer(modifier = Modifier.height(20.dp))
-
if (needsPassword.value) {
OutlinedTextField(
modifier = Modifier
@@ -677,7 +1003,8 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
cancelAutofillForNode(autofillNodePassword)
}
}
- },
+ }
+ .padding(bottom = 20.dp),
shape = RoundedCornerShape(18.dp),
value = password.value,
onValueChange = {
@@ -737,51 +1064,7 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
)
}
- Spacer(modifier = Modifier.height(10.dp))
-
- @Suppress("KotlinConstantConditions")
- if (BuildConfig.FLAVOR != "offline" && PackageUtils.isOrbotInstalled(context)) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Checkbox(
- checked = useProxy.value,
- onCheckedChange = {
- if (it) {
- connectOrbotDialogOpen = true
- } else {
- useProxy.value = false
- }
- },
- )
-
- Text(stringResource(R.string.connect_through_your_orbot_setup))
- }
-
- if (connectOrbotDialogOpen) {
- ConnectOrbotDialog(
- onClose = { connectOrbotDialogOpen = false },
- onPost = {
- connectOrbotDialogOpen = false
- useProxy.value = true
- },
- onError = {
- scope.launch {
- Toast.makeText(
- context,
- it,
- Toast.LENGTH_LONG,
- )
- .show()
- }
- },
- proxyPort,
- )
- }
- }
-
- Spacer(modifier = Modifier.height(10.dp))
-
AmberButton(
- text = stringResource(R.string.next),
onClick = {
if (key.value.text.isBlank()) {
errorMessage = context.getString(R.string.key_is_required)
@@ -801,20 +1084,189 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
}
}
},
+ text = stringResource(R.string.next),
+ )
+ }
+ }
+ 1 -> {
+ val radioOptions = listOf(
+ TitleExplainer(
+ title = stringResource(R.string.sign_policy_basic),
+ explainer = stringResource(R.string.sign_policy_basic_explainer),
+ ),
+ TitleExplainer(
+ title = stringResource(R.string.sign_policy_manual),
+ explainer = stringResource(R.string.sign_policy_manual_explainer),
+ ),
+ )
+ var selectedOption by remember { mutableIntStateOf(0) }
+ var useProxy by remember { mutableStateOf(false) }
+ var proxyPort by remember { mutableStateOf(TextFieldValue("9050")) }
+ Column(
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(it)
+ .padding(horizontal = verticalPadding)
+ .padding(top = verticalPadding * 1.5f),
+ ) {
+ Text(
+ text = stringResource(R.string.sign_policy_explainer),
+ )
+ Column(
+ Modifier.padding(vertical = 10.dp),
+ ) {
+ radioOptions.forEachIndexed { index, option ->
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = selectedOption == index,
+ onClick = {
+ selectedOption = index
+ },
+ )
+ .border(
+ width = 1.dp,
+ color = if (selectedOption == index) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ Color.Transparent
+ },
+ shape = RoundedCornerShape(8.dp),
+ )
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ RadioButton(
+ selected = selectedOption == index,
+ onClick = {
+ selectedOption = index
+ },
+ )
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = option.title,
+ modifier = Modifier.padding(start = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ option.explainer?.let {
+ Text(
+ text = it,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ }
+ }
+ }
+ }
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(vertical = 20.dp)
+ .clickable {
+ useProxy = !useProxy
+ },
+ ) {
+ Switch(
+ modifier = Modifier.scale(0.85f),
+ checked = useProxy,
+ onCheckedChange = {
+ useProxy = it
+ },
+ )
+ Text(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 8.dp),
+ text = stringResource(R.string.connect_through_your_orbot_setup),
+ )
+ }
+
+ if (useProxy) {
+ val myMarkDownStyle =
+ RichTextDefaults.copy(
+ stringStyle = RichTextDefaults.stringStyle?.copy(
+ linkStyle =
+ SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary,
+ ),
+ ),
+ )
+ val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown2)
+
+ val astNode1 =
+ remember {
+ CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
+ }
+
+ RichText(
+ modifier = Modifier.padding(vertical = 8.dp),
+ style = myMarkDownStyle,
+ renderer = null,
+ ) {
+ BasicMarkdown(astNode1)
+ }
+
+ OutlinedTextField(
+ value = proxyPort,
+ onValueChange = { value ->
+ proxyPort = value
+ },
+ label = {
+ Text(
+ text = stringResource(R.string.orbot_socks_port),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ placeholder = {
+ Text(
+ stringResource(R.string.orbot_socks_port),
+ color = TextFieldDefaults.colors().unfocusedPlaceholderColor,
+ )
+ },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Number,
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ )
+ }
+ AmberButton(
+ modifier = Modifier
+ .padding(vertical = 20.dp),
+ onClick = {
+ if (proxyPort.text.toIntOrNull() == null) {
+ Toast.makeText(
+ context,
+ "Invalid port number",
+ Toast.LENGTH_SHORT,
+ ).show()
+ return@AmberButton
+ }
+
+ accountViewModel.startUI(
+ key = key.value.text,
+ password = password.value.text,
+ route = null,
+ useProxy = useProxy,
+ signPolicy = selectedOption,
+ proxyPort = proxyPort.text.toInt(),
+ )
+ },
+ text = stringResource(R.string.finish),
)
}
}
}
- else -> {
- SignPolicyScreen(
- accountViewModel = accountViewModel,
- key = key.value.text,
- password = password.value.text,
- useProxy = useProxy.value,
- proxyPort = proxyPort.value.toInt(),
- seedWords = emptySet(),
- )
- }
}
}
}
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt
index 1bfe8a55..b6806c1f 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt
@@ -80,6 +80,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument
import coil3.compose.AsyncImage
+import com.anggrayudi.storage.SimpleStorageHelper
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
@@ -439,6 +440,7 @@ fun MainScreen(
route: MutableState,
database: AppDatabase,
navController: NavHostController,
+ storageHelper: SimpleStorageHelper,
onRemoveIntentData: (IntentData) -> Unit,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
@@ -553,6 +555,8 @@ fun MainScreen(
sheetState = sheetState,
account = account,
accountStateViewModel = accountStateViewModel,
+ storageHelper = storageHelper,
+ navController = navController,
onClose = {
scope.launch {
shouldShowBottomSheet = false
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountsBottomSheet.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountsBottomSheet.kt
index b7fcd75b..887f4076 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountsBottomSheet.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountsBottomSheet.kt
@@ -8,41 +8,50 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
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.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
+import androidx.navigation.NavController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.anggrayudi.storage.SimpleStorageHelper
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
@@ -51,9 +60,11 @@ import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.MainLoginPage
import com.greenart7c3.nostrsigner.ui.components.ActiveMarker
import com.greenart7c3.nostrsigner.ui.components.CloseButton
+import com.greenart7c3.nostrsigner.ui.components.IconRow
import com.greenart7c3.nostrsigner.ui.components.PostButton
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.vitorpamplona.quartz.encoders.toNpub
+import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -61,9 +72,14 @@ fun AccountsBottomSheet(
sheetState: SheetState,
account: Account,
accountStateViewModel: AccountStateViewModel,
+ storageHelper: SimpleStorageHelper,
+ navController: NavController,
onClose: () -> Unit,
) {
val clipboardManager = LocalClipboardManager.current
+ val scope = rememberCoroutineScope()
+ val newNavController = rememberNavController()
+ val navBackStackEntry by newNavController.currentBackStackEntryAsState()
ModalBottomSheet(
sheetState = sheetState,
@@ -173,26 +189,44 @@ fun AccountsBottomSheet(
}
if (popupExpanded) {
+ val configuration = LocalConfiguration.current
+ val screenWidthDp = configuration.screenWidthDp.dp
+ val screenHeightDp = configuration.screenHeightDp.dp
+
Dialog(
onDismissRequest = { popupExpanded = false },
- properties = DialogProperties(usePlatformDefaultWidth = false),
) {
- Surface(
- color = MaterialTheme.colorScheme.background,
- modifier = Modifier.fillMaxSize(),
+ Scaffold(
+ bottomBar = {
+ if (navBackStackEntry?.destination?.route == "login") {
+ BottomAppBar {
+ IconRow(
+ center = true,
+ title = stringResource(R.string.go_back),
+ icon = ImageVector.vectorResource(R.drawable.back),
+ onClick = {
+ scope.launch {
+ popupExpanded = false
+ }
+ },
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ },
+ modifier = Modifier
+ .requiredWidth(screenWidthDp)
+ .height(screenHeightDp),
) {
- Box {
- MainLoginPage(accountStateViewModel)
- TopAppBar(
- title = { Text(text = stringResource(R.string.add_new_account)) },
- navigationIcon = {
- IconButton(onClick = { popupExpanded = false }) {
- Icon(
- imageVector = Icons.AutoMirrored.Default.ArrowBack,
- contentDescription = "Back",
- )
- }
- },
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ ) {
+ MainLoginPage(
+ accountStateViewModel,
+ storageHelper = storageHelper,
+ navController = newNavController,
)
}
}
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberButton.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberButton.kt
index e84d3cc3..7ff67485 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberButton.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberButton.kt
@@ -87,6 +87,7 @@ fun AmberElevatedButton(
modifier = Modifier.scale(1.50f),
textAlign = textAlign,
maxLines = maxLines,
+ fontWeight = FontWeight.Normal,
)
}
}
diff --git a/app/src/main/res/drawable/frame.xml b/app/src/main/res/drawable/frame.xml
new file mode 100644
index 00000000..b7640ea6
--- /dev/null
+++ b/app/src/main/res/drawable/frame.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5fa9f408..d0519dfa 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,7 +26,7 @@
Invalid key
Key is required
Login
- Create a key
+ Create a new Nostr account
Backup Keys
Read your public key
No permissions granted
@@ -90,6 +90,14 @@
\n5. Configure the Socks port in this screen
\n6. Press the Activate button to use Orbot as a proxy
+
+ 1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
+ \n2. Start Orbot
+ \n3. In Orbot, check the Socks port. The default uses 9050
+ \n4. If necessary change the port in Orbot
+ \n5. Configure the Socks port in this screen
+ \n6. Press the Activate button to use Orbot as a proxy
+
Orbot Socks Port
Invalid port number
Use Orbot
@@ -335,7 +343,7 @@
Nip %1$s not found
Nip not found for the event kind %1$s
Use secret to connect to the application
- Add a key
+ Use your private key
Next
Add your key
Approve basic actions
@@ -434,4 +442,22 @@
Manage the relays used for communicating with external applications
Testing relay...
Always approve these permissions
+ A NOSTR SECURE SIGNER
+ Go back
+ Your Nostr account is ready!
+ You will find it in your account section, so you don’t need to copy it right now.
+ Please add a nickname, later you will able to personalise your full profile (bio, pic, etc) using your preferred Nostr client:
+ Your nickname
+ Now pick a password. We will use it to encrypt your private key (named ncryptsec) so you can store it safely:
+ Pick a password
+ Continue
+ Please download your account backup kit containing the encrypted private key and store it in a couple of safe places.
+ Please enter the password chosen in the previous step, this prove you typed and remembered it correctly:
+ Download your backup kit
+ I want also to download the seed version of my private key
+ This is an optional additional way to backup your private key, write down on paper the 12 words, keeping the correct order.
+ Backup the Nostr account
+ Permissions and connection
+ Setup Amber with your Nostr private key. You can enter different versions: Nsec, Ncryptsec or Hex.\nYou can also scan it from a QR Code.
+ Check the code
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 912b1d11..d98b871c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -27,6 +27,7 @@ agp = "8.7.2"
ktlint = "12.1.1"
ksp = "2.0.20-1.0.24"
coil = "3.0.0-rc02"
+storage = "2.0.0"
[libraries]
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
@@ -68,6 +69,7 @@ coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref
coil-gif = { group = "io.coil-kt.coil3", name = "coil-gif", version.ref = "coil" }
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
+storage = { module = "com.anggrayudi:storage", version.ref = "storage" }
[plugins]
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }