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" }