diff --git a/android/build.gradle b/android/build.gradle index 7c7f3505a4..08536b7524 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -95,7 +95,6 @@ dependencies { implementation "androidx.compose.material:material:$compose_libs_version" implementation "androidx.compose.material:material-icons-extended:$compose_libs_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_libs_version" - implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version" //for flow-layout which is non-lazy grid implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" implementation "androidx.datastore:datastore-preferences:1.0.0" implementation "androidx.navigation:navigation-compose:2.7.7" @@ -122,7 +121,7 @@ dependencies { testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' - testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_libs_version" diff --git a/android/src/androidTest/java/io/parity/signer/domain/storage/ClearCryptedStorageTest.kt b/android/src/androidTest/java/io/parity/signer/domain/storage/ClearCryptedStorageTest.kt new file mode 100644 index 0000000000..5d8020f9be --- /dev/null +++ b/android/src/androidTest/java/io/parity/signer/domain/storage/ClearCryptedStorageTest.kt @@ -0,0 +1,38 @@ +package io.parity.signer.domain.storage + +import io.parity.signer.dependencygraph.ServiceLocator +import io.parity.signer.ui.helpers.PreviewData +import io.parity.signer.uniffi.QrData +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + + +class ClearCryptedStorageTest { + + private val storage = ClearCryptedStorage() + + @Before + fun setUp() { + val context = ServiceLocator.appContext + storage.init(context) + } + + @After + fun tearDown() { + } + + @Test + fun savingQrCodes() { + val seedName = "testname" + val qrCodes = listOf(QrData.Regular(PreviewData.exampleQRData), + QrData.Regular(PreviewData.exampleQRData.asReversed())) + + storage.saveBsQRCodes(seedName, qrCodes) + val recovered = storage.getBsQrCodes(seedName) + + Assert.assertEquals(qrCodes.size, recovered!!.size) + Assert.assertEquals(qrCodes, recovered) + } +} diff --git a/android/src/main/java/io/parity/signer/bottomsheets/PublicKeyBottomSheet.kt b/android/src/main/java/io/parity/signer/bottomsheets/PublicKeyBottomSheet.kt index d2f8dc8d5f..d225110899 100644 --- a/android/src/main/java/io/parity/signer/bottomsheets/PublicKeyBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/bottomsheets/PublicKeyBottomSheet.kt @@ -31,7 +31,7 @@ fun PublicKeyBottomSheetView( ) { BottomSheetHeader( title = name, - onCloseClicked = onClose + onClose = onClose ) SignerDivider(sidePadding = 24.dp) diff --git a/android/src/main/java/io/parity/signer/components/base/BottomSheetHeader.kt b/android/src/main/java/io/parity/signer/components/base/BottomSheetHeader.kt index 9a6cc478b3..e40f7a6a71 100644 --- a/android/src/main/java/io/parity/signer/components/base/BottomSheetHeader.kt +++ b/android/src/main/java/io/parity/signer/components/base/BottomSheetHeader.kt @@ -23,7 +23,7 @@ fun BottomSheetHeader( title: String, modifier: Modifier = Modifier, subtitile: String? = null, - onCloseClicked: Callback? + onClose: Callback? ) { Row( modifier = modifier @@ -46,9 +46,9 @@ fun BottomSheetHeader( ) } } - if (onCloseClicked != null) { + if (onClose != null) { CloseIcon( - onCloseClicked = onCloseClicked, + onCloseClicked = onClose, modifier = Modifier.padding(start = 16.dp) ) } @@ -86,11 +86,11 @@ fun BottomSheetSubtitle( private fun PreviewHeaderWithClose() { SignerNewTheme { Column() { - BottomSheetHeader(title = "Title", onCloseClicked = {}) + BottomSheetHeader(title = "Title", onClose = {}) Divider() BottomSheetHeader(title = "Very very very very long title Very very very very long title") {} Divider() - BottomSheetHeader(title = "Title", subtitile = "With subtitle", onCloseClicked = {}) + BottomSheetHeader(title = "Title", subtitile = "With subtitle", onClose = {}) Divider() BottomSheetSubtitle(R.string.subtitle_secret_recovery_phrase) } diff --git a/android/src/main/java/io/parity/signer/components/base/ScanIconPlain.kt b/android/src/main/java/io/parity/signer/components/base/ScanIconPlain.kt new file mode 100644 index 0000000000..a4298ff87d --- /dev/null +++ b/android/src/main/java/io/parity/signer/components/base/ScanIconPlain.kt @@ -0,0 +1,75 @@ +package io.parity.signer.components.base + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import io.parity.signer.R +import io.parity.signer.domain.Callback +import io.parity.signer.ui.theme.SignerNewTheme +import io.parity.signer.ui.theme.fill18 +import io.parity.signer.ui.theme.fill6 +import io.parity.signer.ui.theme.forcedFill30 +import io.parity.signer.ui.theme.pink500 + + + +@Composable +fun ScanIconPlain( + onClick: Callback, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(32.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + .background(MaterialTheme.colors.fill6), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(R.drawable.ic_qr_code_2), + contentDescription = stringResource(R.string.description_scan_icon), + colorFilter = ColorFilter.tint(MaterialTheme.colors.primary), + modifier = Modifier.size(20.dp) + ) + } +} + + + +@Preview( + name = "light", group = "themes", uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, backgroundColor = 0xFFFFFFFF, +) +@Preview( + name = "dark", group = "themes", uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, backgroundColor = 0xFF000000, +) +@Composable +private fun PreviewScanIcon() { + SignerNewTheme { + Column() { + ScanIconPlain(onClick = {}) + } + } +} + diff --git a/android/src/main/java/io/parity/signer/dependencygraph/ServiceLocator.kt b/android/src/main/java/io/parity/signer/dependencygraph/ServiceLocator.kt index fa5909e04c..4e362cb1ac 100644 --- a/android/src/main/java/io/parity/signer/dependencygraph/ServiceLocator.kt +++ b/android/src/main/java/io/parity/signer/dependencygraph/ServiceLocator.kt @@ -6,6 +6,8 @@ import io.parity.signer.domain.backend.UniffiInteractor import io.parity.signer.components.networkicon.UnknownNetworkColorsGenerator import io.parity.signer.domain.Authentication import io.parity.signer.domain.NetworkExposedStateKeeper +import io.parity.signer.domain.storage.BananaSplitRepository +import io.parity.signer.domain.storage.ClearCryptedStorage import io.parity.signer.domain.storage.DatabaseAssetsInteractor import io.parity.signer.domain.storage.PreferencesRepository import io.parity.signer.domain.storage.SeedRepository @@ -34,6 +36,7 @@ object ServiceLocator { val uniffiInteractor by lazy { UniffiInteractor(appContext) } val seedStorage: SeedStorage = SeedStorage() + val clearCryptedStorage: ClearCryptedStorage = ClearCryptedStorage() val preferencesRepository: PreferencesRepository by lazy { PreferencesRepository( appContext @@ -42,7 +45,8 @@ object ServiceLocator { val databaseAssetsInteractor by lazy { DatabaseAssetsInteractor( appContext, - seedStorage + seedStorage, + clearCryptedStorage, ) } val networkExposedStateKeeper by lazy { @@ -57,10 +61,19 @@ object ServiceLocator { class ActivityScope(val activity: FragmentActivity) { val seedRepository: SeedRepository = SeedRepository( - storage = seedStorage, + seedStorage = seedStorage, + clearCryptedStorage = clearCryptedStorage, authentication = authentication, activity = activity, - uniffiInteractor = uniffiInteractor + uniffiInteractor = uniffiInteractor, + ) + + val bsRepository: BananaSplitRepository = BananaSplitRepository( + seedStorage = seedStorage, + clearCryptedStorage = clearCryptedStorage, + authentication = authentication, + activity = activity, + uniffiInteractor = uniffiInteractor, ) } } diff --git a/android/src/main/java/io/parity/signer/domain/FeatureFlags.kt b/android/src/main/java/io/parity/signer/domain/FeatureFlags.kt index d7e2793f8a..bb53242744 100644 --- a/android/src/main/java/io/parity/signer/domain/FeatureFlags.kt +++ b/android/src/main/java/io/parity/signer/domain/FeatureFlags.kt @@ -13,7 +13,6 @@ object FeatureFlags { FeatureOption.EXPORT_SECRET_KEY -> false //unused FeatureOption.FAIL_DB_VERSION_CHECK -> false FeatureOption.SKIP_USB_CHECK -> true - } } diff --git a/android/src/main/java/io/parity/signer/domain/backend/UniffiInteractor.kt b/android/src/main/java/io/parity/signer/domain/backend/UniffiInteractor.kt index bf5582ee00..52f6bcbc91 100644 --- a/android/src/main/java/io/parity/signer/domain/backend/UniffiInteractor.kt +++ b/android/src/main/java/io/parity/signer/domain/backend/UniffiInteractor.kt @@ -343,6 +343,32 @@ class UniffiInteractor(val appContext: Context) { UniffiResult.Error(e) } } + + suspend fun bsGeneratePassphrase( + shards: Int + ): UniffiResult = + withContext(Dispatchers.IO) { + try { + val result = + io.parity.signer.uniffi.bsGeneratePassphrase(shards.toUInt()) + UniffiResult.Success(result) + } catch (e: ErrorDisplayed) { + UniffiResult.Error(e) + } + } + + suspend fun generateBananaSplit( + secret: String, title: String, passphrase: String, totalShards: UInt, requiredShards: UInt + ): UniffiResult> = + withContext(Dispatchers.IO) { + try { + val transactionResult = + io.parity.signer.uniffi.bsEncrypt(secret, title, passphrase, totalShards, requiredShards) + UniffiResult.Success(transactionResult) + } catch (e: ErrorDisplayed) { + UniffiResult.Error(e) + } + } } sealed class UniffiResult { diff --git a/android/src/main/java/io/parity/signer/domain/storage/BananaSplitRepository.kt b/android/src/main/java/io/parity/signer/domain/storage/BananaSplitRepository.kt new file mode 100644 index 0000000000..2b498b1ece --- /dev/null +++ b/android/src/main/java/io/parity/signer/domain/storage/BananaSplitRepository.kt @@ -0,0 +1,101 @@ +package io.parity.signer.domain.storage + +import androidx.fragment.app.FragmentActivity +import io.parity.signer.domain.AuthResult +import io.parity.signer.domain.Authentication +import io.parity.signer.domain.backend.OperationResult +import io.parity.signer.domain.backend.UniffiInteractor +import io.parity.signer.domain.backend.toOperationResult +import io.parity.signer.screens.scan.bananasplitcreate.BananaSplit +import io.parity.signer.uniffi.ErrorDisplayed +import io.parity.signer.uniffi.QrData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +class BananaSplitRepository( + private val seedStorage: SeedStorage, + private val clearCryptedStorage: ClearCryptedStorage, + private val authentication: Authentication, + private val activity: FragmentActivity, + private val uniffiInteractor: UniffiInteractor, +) { + + suspend fun creaseBs( + seedName: String, + maxShards: Int, + passPhrase: String + ): OperationResult { + + return when (val authResult = authentication.authenticate(activity)) { + AuthResult.AuthSuccess -> { + val phrase = seedStorage.getSeed(seedName, false) + val qrResults: OperationResult, ErrorDisplayed> = + uniffiInteractor.generateBananaSplit( + secret = phrase, + title = seedName, + passphrase = passPhrase, + totalShards = maxShards.toUInt(), + requiredShards = BananaSplit.getMinShards(maxShards).toUInt() + ).toOperationResult() + when (qrResults) { + is OperationResult.Err -> qrResults + is OperationResult.Ok -> { + //saving data + seedStorage.saveBsData(seedName, passPhrase) + clearCryptedStorage.saveBsQRCodes(seedName, qrResults.result) + OperationResult.Ok(Unit) + } + } + } + + AuthResult.AuthError, + AuthResult.AuthFailed, + AuthResult.AuthUnavailable -> { + OperationResult.Err(ErrorDisplayed.Str("auth error - $authResult")) + } + } + } + + fun getBsQrs(seedName: String): List? { + return clearCryptedStorage.getBsQrCodes(seedName) + } + + suspend fun removeBS(seedName: String): OperationResult { + return when (val authResult = authentication.authenticate(activity)) { + AuthResult.AuthSuccess -> { + //removing bs data data + withContext(Dispatchers.IO) { + seedStorage.removeBSData(seedName) + clearCryptedStorage.removeQrCode(seedName) + } + OperationResult.Ok(Unit) + } + + AuthResult.AuthError, + AuthResult.AuthFailed, + AuthResult.AuthUnavailable -> { + OperationResult.Err(ErrorDisplayed.Str("auth error - $authResult")) + } + } + } + + suspend fun getBsPassword(seedName: String): OperationResult { + return when (val authResult = authentication.authenticate(activity)) { + AuthResult.AuthSuccess -> { + val bsPassword = withContext(Dispatchers.IO) { + seedStorage.getBsPassword(seedName) + } + OperationResult.Ok(bsPassword) + } + + AuthResult.AuthError, + AuthResult.AuthFailed, + AuthResult.AuthUnavailable -> { + OperationResult.Err(ErrorDisplayed.Str("auth error - $authResult")) + } + } + } +} + + diff --git a/android/src/main/java/io/parity/signer/domain/storage/ClearCryptedStorage.kt b/android/src/main/java/io/parity/signer/domain/storage/ClearCryptedStorage.kt new file mode 100644 index 0000000000..d5f19775eb --- /dev/null +++ b/android/src/main/java/io/parity/signer/domain/storage/ClearCryptedStorage.kt @@ -0,0 +1,111 @@ +package io.parity.signer.domain.storage + +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import io.parity.signer.domain.backend.OperationResult +import io.parity.signer.screens.error.ErrorStateDestinationState +import io.parity.signer.uniffi.QrData +import kotlinx.coroutines.flow.update +import timber.log.Timber + + +/** + * Entrypted storage that doesn't require authentication to read the values + */ +class ClearCryptedStorage { + private lateinit var sharedPreferences: SharedPreferences + + fun init(appContext: Context): OperationResult { + val hasStrongbox = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + appContext + .packageManager + .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) + } else { + false + } + + Timber.d(TAG, "strongbox available: $hasStrongbox") + + // Init crypto for seeds: + // https://developer.android.com/training/articles/keystore + val masterKey = if (hasStrongbox) { + MasterKey.Builder(appContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .setRequestStrongBoxBacked(true) + .setUserAuthenticationRequired(false) + .build() + } else { + MasterKey.Builder(appContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .setUserAuthenticationRequired(false) + .build() + } + + try { + sharedPreferences = EncryptedSharedPreferences( + appContext, + KEYSTORE_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } catch (e: Exception) { + return OperationResult.Err(consumeStorageAuthError(e, appContext)) + } + return OperationResult.Ok(Unit) + } + + private fun String.toPreservedByteArray(): ByteArray { + return this.toByteArray(Charsets.ISO_8859_1) + } + + private fun ByteArray.toPreservedString(): String { + return String(this, Charsets.ISO_8859_1) + } + + @OptIn(ExperimentalUnsignedTypes::class) + fun saveBsQRCodes(seedName: String, qrs: List) { + val strings = qrs.map { + when (it) { + is QrData.Regular -> it.data + is QrData.Sensitive -> it.data + }.toUByteArray() + .toByteArray() + .toPreservedString() + }.toSet() + with(sharedPreferences.edit()) { + putStringSet(seedName, strings) + apply() + } + } + + /** + * always returns insensitive qr data independent of original one + */ + fun getBsQrCodes(seedName: String): List? { + return sharedPreferences.getStringSet(seedName, null) + ?.map { str -> + str.toPreservedByteArray() + .map { byte -> byte.toUByte() } + } + ?.map { QrData.Regular(data = it) } + } + + fun removeQrCode(seedName: String) { + with(sharedPreferences.edit()) { + remove(seedName) + apply() + } + } + + fun wipe() { + sharedPreferences.edit().clear().commit() // No, not apply(), do it now! + } +} + +private const val TAG = "ClearCryptedStorage" +private const val KEYSTORE_NAME = "ClearCryptedStorage" diff --git a/android/src/main/java/io/parity/signer/domain/storage/DatabaseAssetsInteractor.kt b/android/src/main/java/io/parity/signer/domain/storage/DatabaseAssetsInteractor.kt index f63dbcb515..c8f3b95bc1 100644 --- a/android/src/main/java/io/parity/signer/domain/storage/DatabaseAssetsInteractor.kt +++ b/android/src/main/java/io/parity/signer/domain/storage/DatabaseAssetsInteractor.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.security.keystore.UserNotAuthenticatedException import io.parity.signer.domain.getDbNameFromContext +import okhttp3.internal.wait import java.io.File import java.io.FileOutputStream @@ -13,7 +14,8 @@ import java.io.FileOutputStream */ class DatabaseAssetsInteractor( private val context: Context, - private val seedStorage: SeedStorage + private val seedStorage: SeedStorage, + private val cryptedStorage: ClearCryptedStorage, ) { private val dbName: String = context.getDbNameFromContext() @@ -25,6 +27,7 @@ class DatabaseAssetsInteractor( fun wipe() { deleteDir(File(dbName)) seedStorage.wipe() + cryptedStorage.wipe() } /** diff --git a/android/src/main/java/io/parity/signer/domain/storage/SeedRepository.kt b/android/src/main/java/io/parity/signer/domain/storage/SeedRepository.kt index 3830216b91..48d7eb9843 100644 --- a/android/src/main/java/io/parity/signer/domain/storage/SeedRepository.kt +++ b/android/src/main/java/io/parity/signer/domain/storage/SeedRepository.kt @@ -16,25 +16,26 @@ import io.parity.signer.uniffi.createKeySet class SeedRepository( - private val storage: SeedStorage, + private val seedStorage: SeedStorage, + private val clearCryptedStorage: ClearCryptedStorage, private val authentication: Authentication, private val activity: FragmentActivity, private val uniffiInteractor: UniffiInteractor, ) { fun containSeedName(seedName: String): Boolean { - return storage.lastKnownSeedNames.value.contains(seedName) + return seedStorage.lastKnownSeedNames.value.contains(seedName) } fun getLastKnownSeedNames(): Array { - return storage.lastKnownSeedNames.value + return seedStorage.lastKnownSeedNames.value } suspend fun getAllSeeds(): RepoResult> { return when (val authResult = authentication.authenticate(activity)) { AuthResult.AuthSuccess -> { - val result = storage.getSeedNames() - .associateWith { seedName -> storage.getSeed(seedName, false) } + val result = seedStorage.getSeedNames() + .associateWith { seedName -> seedStorage.getSeed(seedName, false) } RepoResult.Success(result) } @@ -97,7 +98,7 @@ class SeedRepository( when (val authResult = authentication.authenticate(activity)) { AuthResult.AuthSuccess -> { - val result = seedNames.map { it to storage.getSeed(it) } + val result = seedNames.map { it to seedStorage.getSeed(it) } return if (result.any { it.second.isEmpty() }) { RepoResult.Failure(IllegalStateException("phrase some are empty - broken storage?")) } else { @@ -158,7 +159,7 @@ class SeedRepository( seedPhrase: String, networks: List, ) { - storage.addSeed(seedName, seedPhrase) + seedStorage.addSeed(seedName, seedPhrase) try { createKeySet(seedName, seedPhrase, networks) } catch (e: ErrorDisplayed) { @@ -178,7 +179,8 @@ class SeedRepository( return when (val authResult = authentication.authenticate(activity)) { AuthResult.AuthSuccess -> { try { - storage.removeSeed(seedName) + seedStorage.removeSeed(seedName) + clearCryptedStorage.removeQrCode(seedName) when (val remove = uniffiInteractor.removeKeySet(seedName)) { is UniffiResult.Error -> OperationResult.Err(remove.error) is UniffiResult.Success -> OperationResult.Ok(Unit) @@ -200,7 +202,7 @@ class SeedRepository( private fun getSeedPhrasesDangerous(seedNames: List): RepoResult { val seedPhrases = seedNames - .map { storage.getSeed(it) } + .map { seedStorage.getSeed(it) } .filter { it.isNotEmpty() } .joinToString(separator = "\n") @@ -216,13 +218,13 @@ class SeedRepository( suspend fun isSeedPhraseCollision(seedPhrase: String): Boolean { return try { - val result = storage.checkIfSeedNameAlreadyExists(seedPhrase) + val result = seedStorage.checkIfSeedNameAlreadyExists(seedPhrase) result } catch (e: UserNotAuthenticatedException) { when (val authResult = authentication.authenticate(activity)) { AuthResult.AuthSuccess -> { - val result = storage.checkIfSeedNameAlreadyExists(seedPhrase) + val result = seedStorage.checkIfSeedNameAlreadyExists(seedPhrase) result } diff --git a/android/src/main/java/io/parity/signer/domain/storage/SeedStorage.kt b/android/src/main/java/io/parity/signer/domain/storage/SeedStorage.kt index b197707594..7bdc12f229 100644 --- a/android/src/main/java/io/parity/signer/domain/storage/SeedStorage.kt +++ b/android/src/main/java/io/parity/signer/domain/storage/SeedStorage.kt @@ -47,6 +47,7 @@ class SeedStorage { /** * @throws UserNotAuthenticatedException */ + @Throws(UserNotAuthenticatedException::class) fun init(appContext: Context): OperationResult { hasStrongbox = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { appContext @@ -107,6 +108,7 @@ class SeedStorage { /** * @throws UserNotAuthenticatedException */ + @Throws(UserNotAuthenticatedException::class) fun getSeedNames(): Array = sharedPreferences.all.keys.sorted().toTypedArray().also { _lastKnownSeedNames.value = it @@ -120,6 +122,7 @@ class SeedStorage { * * @throws UserNotAuthenticatedException */ + @Throws(UserNotAuthenticatedException::class) fun addSeed( seedName: String, seedPhrase: String, @@ -144,7 +147,8 @@ class SeedStorage { /** * @throws UserNotAuthenticatedException */ - fun checkIfSeedNameAlreadyExists(seedPhrase: String) : Boolean { + @Throws(UserNotAuthenticatedException::class) + fun checkIfSeedNameAlreadyExists(seedPhrase: String): Boolean { val result = sharedPreferences.all.values.contains(seedPhrase) Runtime.getRuntime().gc() return result @@ -153,6 +157,7 @@ class SeedStorage { /** * @throws UserNotAuthenticatedException */ + @Throws(UserNotAuthenticatedException::class) fun getSeed( seedName: String, showInLogs: Boolean = false @@ -168,14 +173,52 @@ class SeedStorage { } } + /** + * @throws UserNotAuthenticatedException + */ + @Throws(UserNotAuthenticatedException::class) + fun getBsPassword( + seedName: String, + ): String { + return sharedPreferences.getString("$seedName$BS_POSTFIX", "") ?: "" + } + + /** + * @throws UserNotAuthenticatedException + * @throws IllegalArgumentException when name collision happening + */ + @Throws(IllegalArgumentException::class, UserNotAuthenticatedException::class) + fun saveBsData( + seedName: String, + passPhrase: String, + ) { + if (sharedPreferences.contains("$seedName$BS_POSTFIX")) { + throw IllegalArgumentException("element with this name already exists in the storage") + } + sharedPreferences.edit() + .putString("$seedName$BS_POSTFIX", passPhrase) + .apply() + } + + @Throws(UserNotAuthenticatedException::class) + fun removeBSData(seedName: String) { + sharedPreferences.edit() + .remove("$seedName$BS_POSTFIX") + .apply() + } + /** * Don't forget to call tell rust seed names -so getSeedNames() * called and last known elements updated * * @throws [UserNotAuthenticatedException] */ + @Throws(UserNotAuthenticatedException::class) fun removeSeed(seedName: String) { - sharedPreferences.edit().remove(seedName).apply() + sharedPreferences.edit() + .remove(seedName) + .remove("$seedName$BS_POSTFIX") + .apply() _lastKnownSeedNames.update { lastState -> lastState.filter { it != seedName }.toTypedArray() } @@ -184,12 +227,16 @@ class SeedStorage { /** * @throws UserNotAuthenticatedException */ + @Throws(UserNotAuthenticatedException::class) fun wipe() { sharedPreferences.edit().clear().commit() // No, not apply(), do it now! } } -private fun consumeStorageAuthError(e: Exception, context: Context): ErrorStateDestinationState { +internal fun consumeStorageAuthError( + e: Exception, + context: Context +): ErrorStateDestinationState { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { when (e) { is AEADBadTagException, @@ -201,6 +248,7 @@ private fun consumeStorageAuthError(e: Exception, context: Context): ErrorStateD argVerbose = e.stackTraceToString() ) } + else -> throw e } } else { @@ -213,12 +261,13 @@ private fun consumeStorageAuthError(e: Exception, context: Context): ErrorStateD argVerbose = e.stackTraceToString() ) } + else -> throw e } } } - +private const val BS_POSTFIX = "\$\$bs_passphrase" diff --git a/android/src/main/java/io/parity/signer/domain/usecases/ResetUseCase.kt b/android/src/main/java/io/parity/signer/domain/usecases/ResetUseCase.kt index fdb708c955..ec27bea9a5 100644 --- a/android/src/main/java/io/parity/signer/domain/usecases/ResetUseCase.kt +++ b/android/src/main/java/io/parity/signer/domain/usecases/ResetUseCase.kt @@ -19,6 +19,7 @@ import io.parity.signer.uniffi.initNavigation class ResetUseCase { private val seedStorage = ServiceLocator.seedStorage + private val clearCryptedStorage = ServiceLocator.clearCryptedStorage private val databaseAssetsInteractor: DatabaseAssetsInteractor = ServiceLocator.databaseAssetsInteractor private val appContext = ServiceLocator.appContext @@ -104,6 +105,10 @@ class ResetUseCase { if (result is OperationResult.Err) { return result } + val result2 = clearCryptedStorage.init(appContext) + if (result2 is OperationResult.Err) { + return result2 + } } if (!appContext.isDbCreatedAndOnboardingPassed()) { initAssetsAndTotalRefresh() diff --git a/android/src/main/java/io/parity/signer/screens/createderivation/help/DerivationMethodsHelpBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/createderivation/help/DerivationMethodsHelpBottomSheet.kt index bdc9b2f32a..389d6ac14a 100644 --- a/android/src/main/java/io/parity/signer/screens/createderivation/help/DerivationMethodsHelpBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/createderivation/help/DerivationMethodsHelpBottomSheet.kt @@ -22,7 +22,7 @@ fun DerivationMethodsHelpBottomSheet( Column(Modifier.background(MaterialTheme.colors.backgroundTertiary)) { BottomSheetHeader( title = stringResource(R.string.derivation_help_methods_title), - onCloseClicked = onClose, + onClose = onClose, ) //scrollable part Column( diff --git a/android/src/main/java/io/parity/signer/screens/keydetails/exportprivatekey/PrivateKeyExportBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/keydetails/exportprivatekey/PrivateKeyExportBottomSheet.kt index b79543f1ff..52083d8ee8 100644 --- a/android/src/main/java/io/parity/signer/screens/keydetails/exportprivatekey/PrivateKeyExportBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/keydetails/exportprivatekey/PrivateKeyExportBottomSheet.kt @@ -47,7 +47,7 @@ fun PrivateKeyExportBottomSheet( ) { BottomSheetHeader( title = stringResource(R.string.export_private_key_title), - onCloseClicked = onClose, + onClose = onClose, ) //scrollable part if doesn't fit into screen Column( diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsDestination.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsDestination.kt index f405d1154b..b3cf19cf98 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsDestination.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsDestination.kt @@ -8,7 +8,7 @@ import androidx.navigation.navArgument import io.parity.signer.ui.mainnavigation.CoreUnlockedNavSubgraph fun NavGraphBuilder.keySetDetailsDestination( - navController: NavController, + coreNavController: NavController, ) { composable( route = CoreUnlockedNavSubgraph.KeySet.route, @@ -24,7 +24,7 @@ fun NavGraphBuilder.keySetDetailsDestination( KeySetDetailsScreenSubgraph( originalSeedName = seedName, - navController = navController, + coreNavController = coreNavController, ) } } diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsMenu.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsMenu.kt index f91f02eea3..6f5fa874f3 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsMenu.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsMenu.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.FileUpload +import androidx.compose.material.icons.outlined.QrCode import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf @@ -20,6 +21,8 @@ import io.parity.signer.R import io.parity.signer.components.base.BottomSheetConfirmDialog import io.parity.signer.components.base.SecondaryButtonWide import io.parity.signer.domain.Callback +import io.parity.signer.domain.FeatureFlags +import io.parity.signer.domain.FeatureOption import io.parity.signer.domain.NetworkState import io.parity.signer.screens.keydetails.MenuItemForBottomSheet import io.parity.signer.ui.theme.SignerNewTheme @@ -44,7 +47,8 @@ fun KeySetDeleteConfirmBottomSheet( fun KeyDetailsMenuGeneral( networkState: State, onSelectKeysClicked: Callback, - onBackupClicked: Callback, + onBackupBsClicked: Callback, + onBackupManualClicked: Callback, onDeleteClicked: Callback, exposeConfirmAction: Callback,//also called shield onCancel: Callback, @@ -62,12 +66,23 @@ fun KeyDetailsMenuGeneral( onclick = onSelectKeysClicked ) + MenuItemForBottomSheet( + vector = Icons.Outlined.QrCode, + label = stringResource(R.string.key_set_menu_option_backup_bs), + onclick = { + if (networkState.value == NetworkState.None) + onBackupBsClicked() + else + exposeConfirmAction() + } + ) + MenuItemForBottomSheet( iconId = R.drawable.ic_settings_backup_restore_28, - label = stringResource(R.string.menu_option_backup_key_set), + label = stringResource(R.string.key_set_menu_option_backup_manual), onclick = { if (networkState.value == NetworkState.None) - onBackupClicked() + onBackupManualClicked() else exposeConfirmAction() } @@ -89,7 +104,6 @@ fun KeyDetailsMenuGeneral( } - @Preview( name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true, backgroundColor = 0xFFFFFFFF, @@ -104,7 +118,7 @@ private fun PreviewKeyDetailsMenu() { SignerNewTheme { val state = remember { mutableStateOf(NetworkState.None) } KeyDetailsMenuGeneral( - state, {}, {}, {}, {}, {}, + state, {}, {}, {}, {}, {}, {}, ) } } diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenSubgraph.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenSubgraph.kt index 6f380f1789..363af6a53c 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenSubgraph.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.NavHost @@ -41,23 +42,21 @@ import kotlinx.coroutines.launch @Composable fun KeySetDetailsScreenSubgraph( originalSeedName: String?, - navController: NavController, + coreNavController: NavController, ) { val menuNavController = rememberNavController() val coroutineScope = rememberCoroutineScope() - var seedName by rememberSaveable() { - mutableStateOf(originalSeedName) - } - val keySetViewModel: KeySetDetailsViewModel = viewModel() + val seedName = keySetViewModel.shownSeedName.collectAsStateWithLifecycle() - LaunchedEffect(key1 = seedName) { - keySetViewModel.feedModelForSeed(seedName) + LaunchedEffect(key1 = originalSeedName) { + keySetViewModel.feedModelForSeed(originalSeedName) } LaunchedEffect(key1 = Unit) { //this is first screen - check db version here - keySetViewModel.validateDbSchemaCorrect().handleErrorAppState(navController) + keySetViewModel.validateDbSchemaCorrect() + .handleErrorAppState(coreNavController) } val filteredScreenModel = @@ -65,7 +64,7 @@ fun KeySetDetailsScreenSubgraph( val networkState = keySetViewModel.networkState.collectAsStateWithLifecycle() when (val state = - filteredScreenModel.value.handleErrorAppState(navController)) { + filteredScreenModel.value.handleErrorAppState(coreNavController)) { KeySetDetailsScreenState.NoKeySets -> { NoKeySetEmptyWelcomeScreen( @@ -75,12 +74,12 @@ fun KeySetDetailsScreenSubgraph( } }, onNewKeySet = { - navController.navigate( + coreNavController.navigate( CoreUnlockedNavSubgraph.newKeySet ) }, onRecoverKeySet = { - navController.navigate( + coreNavController.navigate( CoreUnlockedNavSubgraph.recoverKeySet ) }, @@ -94,7 +93,7 @@ fun KeySetDetailsScreenSubgraph( Box(Modifier.statusBarsPadding()) { KeySetDetailsScreenView( model = state.filteredModel, - navController = navController, + navController = coreNavController, networkState = networkState, fullModelWasEmpty = state.wasEmptyKeyset, onExposedClicked = { @@ -106,7 +105,7 @@ fun KeySetDetailsScreenSubgraph( menuNavController.navigate(KeySetDetailsMenuSubgraph.keys_menu) }, onAddNewDerivation = { - navController.navigate( + coreNavController.navigate( CoreUnlockedNavSubgraph.NewDerivedKey.destination( seedName = state.filteredModel.root.seedName ) @@ -122,7 +121,7 @@ fun KeySetDetailsScreenSubgraph( menuNavController.navigate(KeySetDetailsMenuSubgraph.show_root) }, onOpenKey = { keyAddr: String, keySpecs: String -> - navController.navigate( + coreNavController.navigate( CoreUnlockedNavSubgraph.KeyDetails.destination( keyAddr = keyAddr, keySpec = keySpecs @@ -152,11 +151,21 @@ fun KeySetDetailsScreenSubgraph( menuNavController.popBackStack() menuNavController.navigate(KeySetDetailsMenuSubgraph.export) }, - onBackupClicked = { + onBackupManualClicked = { menuNavController.navigate(KeySetDetailsMenuSubgraph.backup) { popUpTo(KeySetDetailsMenuSubgraph.empty) } }, + onBackupBsClicked = { + menuNavController.popBackStack() + seedName.value?.let { seedName -> + coreNavController.navigate( + CoreUnlockedNavSubgraph.CreateBananaSplit.destination( + seedName + ) + ) + } + }, onCancel = { menuNavController.popBackStack() }, @@ -175,11 +184,13 @@ fun KeySetDetailsScreenSubgraph( } composable(KeySetDetailsMenuSubgraph.select_keyset) { SeedSelectMenuFull( - coreNavController = navController, + coreNavController = coreNavController, selectedSeed = state.filteredModel.root.seedName, onSelectSeed = { newSeed -> - seedName = newSeed - closeAction() + keySetViewModel.viewModelScope.launch { + keySetViewModel.feedModelForSeed(newSeed) + closeAction() + } }, onClose = closeAction, ) @@ -192,7 +203,7 @@ fun KeySetDetailsScreenSubgraph( val root = state.filteredModel.root coroutineScope.launch { keySetViewModel.removeSeed(root) - .handleErrorAppState(navController) + .handleErrorAppState(coreNavController) closeAction() } }, diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenView.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenView.kt index 3e97ab3779..e0001b8270 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenView.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsScreenView.kt @@ -157,7 +157,7 @@ fun KeySetDetailsScreenView( ) ScanIconComponent( onClick = { - navController.navigate(CoreUnlockedNavSubgraph.camera) + navController.navigate(CoreUnlockedNavSubgraph.Camera.destination(null)) }, Modifier .align(Alignment.BottomCenter) diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsViewModel.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsViewModel.kt index 7767c7008c..d64207c146 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsViewModel.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/KeySetDetailsViewModel.kt @@ -19,6 +19,7 @@ import io.parity.signer.uniffi.ErrorDisplayed import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -43,6 +44,9 @@ class KeySetDetailsViewModel : ViewModel() { val networkState: StateFlow = networkExposedStateKeeper.airGapModeState + private val _shownSeedName: MutableStateFlow = MutableStateFlow(null) + val shownSeedName: StateFlow = _shownSeedName.asStateFlow() + private val fullScreenState = MutableStateFlow>( OperationResult.Ok(KeySetDetailsScreenState.LoadingState) @@ -83,16 +87,15 @@ class KeySetDetailsViewModel : ViewModel() { initialValue = fullScreenState.value, ) - private suspend fun getKeySetDetails(requestedSeedName: String?): OperationResult { if (requestedSeedName != null) { preferencesRepository.setLastSelectedSeed(requestedSeedName) } - val seedName = + _shownSeedName.value = requestedSeedName ?: preferencesRepository.getLastSelectedSeed() - val result = seedName?.let { seedName -> + val result = shownSeedName.value?.let { seedName -> uniffiInteractor.keySetBySeedName(seedName) .mapInner { KeySetDetailsScreenState.Data( diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/KeySetBackup.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/KeySetBackup.kt index 64595b74e1..5ca55a9de5 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/KeySetBackup.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/KeySetBackup.kt @@ -68,7 +68,7 @@ private fun KeySetBackupBottomSheet( BottomSheetHeader( title = model.seedName, subtitile = model.seedBase58.abbreviateString(BASE58_STYLE_ABBREVIATE), - onCloseClicked = onClose, + onClose = onClose, ) SignerDivider(sidePadding = 24.dp) Column( diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/SeedPhraseBox.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/SeedPhraseBox.kt index 629b8ef68d..a30761011a 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/SeedPhraseBox.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/backup/SeedPhraseBox.kt @@ -2,6 +2,8 @@ package io.parity.signer.screens.keysetdetails.backup import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize @@ -20,9 +22,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.google.accompanist.flowlayout.FlowMainAxisAlignment -import com.google.accompanist.flowlayout.FlowRow -import com.google.accompanist.flowlayout.SizeMode import io.parity.signer.R import io.parity.signer.domain.KeepScreenOn import io.parity.signer.domain.DisableScreenshots @@ -46,14 +45,13 @@ internal val PhraseWordStyle: TextStyle = TextStyle( fontSize = 13.sp ) +@OptIn(ExperimentalLayoutApi::class) @Composable fun SeedPhraseBox(seedPhrase: String) { val innerRound = dimensionResource(id = R.dimen.innerFramesCornerRadius) val innerShape = RoundedCornerShape(innerRound, innerRound, innerRound, innerRound) FlowRow( - mainAxisSize = SizeMode.Expand, - mainAxisAlignment = FlowMainAxisAlignment.SpaceBetween, modifier = Modifier .padding(horizontal = 16.dp) .padding(top = 8.dp, bottom = 16.dp) diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsExportResultBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsExportResultBottomSheet.kt index 394d5c8bb0..53d878b43f 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsExportResultBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsExportResultBottomSheet.kt @@ -46,7 +46,7 @@ fun KeySetDetailsExportResultBottomSheet( count = keysToExport, keysToExport, ), - onCloseClicked = onClose + onClose = onClose ) val plateShape = RoundedCornerShape(dimensionResource(id = R.dimen.qrShapeCornerRadius)) diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsMultiselectBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsMultiselectBottomSheet.kt index debed12421..3e2a9b5528 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsMultiselectBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/export/KeySetDetailsMultiselectBottomSheet.kt @@ -59,7 +59,7 @@ fun KeySetDetailsMultiselectBottomSheet( count = keysToExport, keysToExport, ), - onCloseClicked = onClose, + onClose = onClose, ) SignerDivider(sidePadding = 24.dp) Column( diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/filtermenu/NetworkFilterMenu.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/filtermenu/NetworkFilterMenu.kt index e5e73b22b6..5843fa6667 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/filtermenu/NetworkFilterMenu.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/filtermenu/NetworkFilterMenu.kt @@ -70,7 +70,7 @@ private fun NetworkFilterMenu( ) { BottomSheetHeader( title = stringResource(R.string.network_filters_header), - onCloseClicked = onCancel + onClose = onCancel ) SignerDivider(sidePadding = 24.dp) networks.forEach { network -> diff --git a/android/src/main/java/io/parity/signer/screens/keysetdetails/seedselectmenu/SeedSelectMenuView.kt b/android/src/main/java/io/parity/signer/screens/keysetdetails/seedselectmenu/SeedSelectMenuView.kt index e08e6f6517..44fbf9eb13 100644 --- a/android/src/main/java/io/parity/signer/screens/keysetdetails/seedselectmenu/SeedSelectMenuView.kt +++ b/android/src/main/java/io/parity/signer/screens/keysetdetails/seedselectmenu/SeedSelectMenuView.kt @@ -31,7 +31,7 @@ internal fun SeedSelectMenuView( Column { BottomSheetHeader( title = stringResource(R.string.key_sets_screem_title), - onCloseClicked = onClose + onClose = onClose ) SignerDivider() keySetsListModel.keys.forEach { item -> diff --git a/android/src/main/java/io/parity/signer/screens/keysets/create/NewKeysetBackupStepSubgraph.kt b/android/src/main/java/io/parity/signer/screens/keysets/create/NewKeysetBackupStepSubgraph.kt index b4f6271bad..c64e32aa41 100644 --- a/android/src/main/java/io/parity/signer/screens/keysets/create/NewKeysetBackupStepSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/keysets/create/NewKeysetBackupStepSubgraph.kt @@ -1,11 +1,7 @@ package io.parity.signer.screens.keysets.create -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/android/src/main/java/io/parity/signer/screens/keysets/create/backupstepscreens/NewKeySetBackupBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/keysets/create/backupstepscreens/NewKeySetBackupBottomSheet.kt index 0fc2f4a96d..6a3693a721 100644 --- a/android/src/main/java/io/parity/signer/screens/keysets/create/backupstepscreens/NewKeySetBackupBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/keysets/create/backupstepscreens/NewKeySetBackupBottomSheet.kt @@ -62,7 +62,6 @@ internal fun NewKeySetBackupBottomSheet( isCtaEnabled = confirmBackup && confirmNotLoose, ) } - } diff --git a/android/src/main/java/io/parity/signer/screens/keysets/restore/KeysetRecoverSubgraph.kt b/android/src/main/java/io/parity/signer/screens/keysets/restore/KeysetRecoverSubgraph.kt index 78822f3355..fd2e377bce 100644 --- a/android/src/main/java/io/parity/signer/screens/keysets/restore/KeysetRecoverSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/keysets/restore/KeysetRecoverSubgraph.kt @@ -1,11 +1,7 @@ package io.parity.signer.screens.keysets.restore -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -83,9 +79,10 @@ fun KeysetRecoverSubgraph( submitErrorState("navigation to finish called, but seed is not valid") } }, + onScanOpen = { CoreUnlockedNavSubgraph.Camera.destination(keysetName) }, modifier = Modifier .statusBarsPadding() - .imePadding() + .imePadding(), ) } composable( diff --git a/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/EnterSeedPhraseBox.kt b/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/EnterSeedPhraseBox.kt index 1d2245a528..7b1854388a 100644 --- a/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/EnterSeedPhraseBox.kt +++ b/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/EnterSeedPhraseBox.kt @@ -2,10 +2,19 @@ package io.parity.signer.screens.keysets.restore.restorephrase import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,6 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -28,10 +38,9 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.accompanist.flowlayout.FlowMainAxisAlignment -import com.google.accompanist.flowlayout.FlowRow -import com.google.accompanist.flowlayout.SizeMode import io.parity.signer.R +import io.parity.signer.components.base.ScanIconPlain +import io.parity.signer.domain.Callback import io.parity.signer.domain.DisableScreenshots import io.parity.signer.domain.KeepScreenOn import io.parity.signer.domain.conditional @@ -44,12 +53,14 @@ import io.parity.signer.ui.theme.textDisabled import io.parity.signer.ui.theme.textTertiary +@OptIn(ExperimentalLayoutApi::class) @Composable fun EnterSeedPhraseBox( enteredWords: List, rawUserInput: String, modifier: Modifier = Modifier, onEnteredChange: (progressWord: String) -> Unit, + onScanOpen: Callback, ) { val innerRound = dimensionResource(id = R.dimen.innerFramesCornerRadius) val innerShape = RoundedCornerShape(innerRound) @@ -65,47 +76,58 @@ fun EnterSeedPhraseBox( selection = TextRange(rawUserInput.length) ) - FlowRow( - mainAxisSize = SizeMode.Expand, - mainAxisSpacing = 4.dp, - mainAxisAlignment = FlowMainAxisAlignment.Start, - crossAxisSpacing = 4.dp, - modifier = modifier + Column( + modifier .background(MaterialTheme.colors.fill6, innerShape) - .defaultMinSize(minHeight = 156.dp) - .padding(8.dp), + .padding(8.dp) +// .clickable { focusRequester.requestFocus() } ) { - enteredWords.onEachIndexed { index, word -> - EnterSeedPhraseWord(index = index + 1, word = word) - } - val shouldShowPlaceholder = enteredWords.isEmpty() && rawUserInput.isEmpty() - BasicTextField( - textStyle = TextStyle(color = MaterialTheme.colors.primary), - value = seedWord.value, //as was before redesign, should been moved to rust but need to align with iOS - onValueChange = { - if (it.text != seedWord.value.text) { - onEnteredChange(it.text) - } - seedWord.value = it - }, - cursorBrush = SolidColor(MaterialTheme.colors.primary), - modifier = Modifier - .focusRequester(focusRequester) - .padding(vertical = 8.dp, horizontal = 12.dp) - .conditional(!shouldShowPlaceholder) { - width(IntrinsicSize.Min) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.heightIn(min = 100.dp) + ) { + enteredWords.onEachIndexed { index, word -> + EnterSeedPhraseWord(index = index + 1, word = word) + } + val shouldShowPlaceholder = + enteredWords.isEmpty() && rawUserInput.isEmpty() + BasicTextField( + textStyle = TextStyle(color = MaterialTheme.colors.primary), + value = seedWord.value, //as was before redesign, should been moved to rust but need to align with iOS + onValueChange = { + if (it.text != seedWord.value.text) { + onEnteredChange(it.text) + } + seedWord.value = it }, - decorationBox = @Composable { innerTextField -> - innerTextField() - if (shouldShowPlaceholder) { - Text( - text = stringResource(R.string.enter_seed_phease_box_placeholder), - color = MaterialTheme.colors.textTertiary, - style = SignerTypeface.BodyL, - ) + cursorBrush = SolidColor(MaterialTheme.colors.primary), + modifier = Modifier + .focusRequester(focusRequester) + .padding(vertical = 8.dp, horizontal = 12.dp) + .conditional(!shouldShowPlaceholder) { + width(IntrinsicSize.Min) + }, + decorationBox = @Composable { innerTextField -> + innerTextField() + if (shouldShowPlaceholder) { + Text( + text = stringResource(R.string.enter_seed_phease_box_placeholder), + color = MaterialTheme.colors.textTertiary, + style = SignerTypeface.BodyL, + ) + } } - } - ) + ) + } + Box( + modifier = Modifier + .fillMaxWidth(1f), + contentAlignment = Alignment.BottomEnd + ) { + ScanIconPlain(onClick = onScanOpen) + } } DisableScreenshots() @@ -139,6 +161,7 @@ private fun EnterSeedPhraseWord(index: Int, word: String) { } } + @Preview( name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true, backgroundColor = 0xFFFFFFFF, @@ -151,7 +174,7 @@ private fun EnterSeedPhraseWord(index: Int, word: String) { @Composable private fun PreviewSeedPhraseRestoreComponentEmptry() { SignerNewTheme { - EnterSeedPhraseBox(emptyList(), "", Modifier, {}) + EnterSeedPhraseBox(emptyList(), "", Modifier, {}, {}) } } @@ -171,7 +194,7 @@ private fun PreviewSeedPhraseRestoreComponentFinished() { "long", "text", "here", "how", "printed1234" ) SignerNewTheme { - EnterSeedPhraseBox(entered, "", Modifier, {}) + EnterSeedPhraseBox(entered, "", Modifier, {}, {}) } } @@ -191,7 +214,7 @@ private fun PreviewSeedPhraseRestoreComponentInProgress() { "long", "text", "here", "how" ) SignerNewTheme { - EnterSeedPhraseBox(entered, "printed", Modifier, {}) + EnterSeedPhraseBox(entered, "printed", Modifier, {}, {}) } } diff --git a/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/KeysetRecoverPhraseScreen.kt b/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/KeysetRecoverPhraseScreen.kt index 7d3a0b1eaf..4a417c2a33 100644 --- a/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/KeysetRecoverPhraseScreen.kt +++ b/android/src/main/java/io/parity/signer/screens/keysets/restore/restorephrase/KeysetRecoverPhraseScreen.kt @@ -29,6 +29,7 @@ fun KeysetRecoverPhraseScreen( onNewInput: (input: String) -> Unit, onAddSuggestedWord: (input: String) -> Unit, onDone: Callback, + onScanOpen: Callback, modifier: Modifier = Modifier, ) { Column( @@ -67,6 +68,7 @@ fun KeysetRecoverPhraseScreen( .padding(horizontal = 16.dp) .padding(top = 8.dp, bottom = 12.dp), onEnteredChange = onNewInput, + onScanOpen = onScanOpen, ) RestoreSeedPhraseSuggest( guessWord = model.suggestedWords, @@ -94,6 +96,7 @@ private fun PreviewKeysetRecoverPhraseScreenView() { onNewInput = { _ -> }, onAddSuggestedWord = { _ -> }, onDone = {}, + onScanOpen = {}, ) } } diff --git a/android/src/main/java/io/parity/signer/screens/scan/ScanNavSubgraph.kt b/android/src/main/java/io/parity/signer/screens/scan/ScanNavSubgraph.kt index 7b909ff8c5..6604167104 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/ScanNavSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/ScanNavSubgraph.kt @@ -17,7 +17,7 @@ import io.parity.signer.bottomsheets.password.EnterPassword import io.parity.signer.domain.Callback import io.parity.signer.domain.FakeNavigator import io.parity.signer.screens.scan.addnetwork.AddedNetworkSheetsSubgraph -import io.parity.signer.screens.scan.bananasplit.BananaSplitSubgraph +import io.parity.signer.screens.scan.bananasplitrestore.BananaSplitSubgraph import io.parity.signer.screens.scan.camera.ScanScreen import io.parity.signer.screens.scan.elements.WrongPasswordBottomSheet import io.parity.signer.screens.scan.errors.LocalErrorBottomSheet @@ -37,6 +37,7 @@ import kotlinx.coroutines.launch @Composable fun ScanNavSubgraph( onCloseCamera: Callback, + seedNameSuggestion: String?, openKeySet:(seedName: String) -> Unit, ) { val scanViewModel: ScanViewModel = viewModel() @@ -95,6 +96,7 @@ fun ScanNavSubgraph( LocalErrorSheetModel(context = context, details = error) scanViewModel.bananaSplitPassword.value = null }, + suggestedSeedName = seedNameSuggestion, onErrorWrongPassword = { scanViewModel.errorWrongPassword.value = true scanViewModel.bananaSplitPassword.value = null diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/BananaSplit.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/BananaSplit.kt new file mode 100644 index 0000000000..755a0ea404 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/BananaSplit.kt @@ -0,0 +1,9 @@ +package io.parity.signer.screens.scan.bananasplitcreate + + +object BananaSplit { + fun getMinShards(totalShards: Int): Int { + return totalShards/2 + 1 + } + const val defaultShards = 4 +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/BananaSplitCreateDestination.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/BananaSplitCreateDestination.kt new file mode 100644 index 0000000000..0a112a7ddf --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/BananaSplitCreateDestination.kt @@ -0,0 +1,53 @@ +package io.parity.signer.screens.scan.bananasplitcreate + +import androidx.compose.runtime.remember +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.navArgument +import io.parity.signer.screens.scan.bananasplitcreate.create.CreateBananaSplitScreen +import io.parity.signer.screens.scan.bananasplitcreate.show.BananaSplitShowFull +import io.parity.signer.ui.mainnavigation.CoreUnlockedNavSubgraph + +fun NavGraphBuilder.bananaSplitCreateDestination( + navController: NavController, +) { + navigation( + route = CoreUnlockedNavSubgraph.CreateBananaSplit.route, + startDestination = BananaSplitCreateDestination.ShowBS, + arguments = listOf(navArgument(CoreUnlockedNavSubgraph.CreateBananaSplit.seedNameArg) { + type = NavType.StringType + }), + ) { + composable( + route = BananaSplitCreateDestination.ShowBS, + ) { entry -> + val parentEntry = remember(entry) { + navController.getBackStackEntry(CoreUnlockedNavSubgraph.CreateBananaSplit.route) + } + val seedName = + parentEntry.arguments?.getString(CoreUnlockedNavSubgraph.CreateBananaSplit.seedNameArg)!! + + BananaSplitShowFull(navController, seedName) + } + composable( + route = BananaSplitCreateDestination.CreateBsCreateScreen, + ) { entry -> + val parentEntry = remember(entry) { + navController.getBackStackEntry(CoreUnlockedNavSubgraph.CreateBananaSplit.route) + } + val seedName = + parentEntry.arguments?.getString(CoreUnlockedNavSubgraph.CreateBananaSplit.seedNameArg)!! + + CreateBananaSplitScreen(navController, seedName) + } + } +} + + +internal object BananaSplitCreateDestination { + const val CreateBsCreateScreen = "create_banana_split" + const val ShowBS = "show_banana_split" +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBananaSplitScreen.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBananaSplitScreen.kt new file mode 100644 index 0000000000..c678c41c3c --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBananaSplitScreen.kt @@ -0,0 +1,54 @@ +package io.parity.signer.screens.scan.bananasplitcreate.create + +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import io.parity.signer.screens.error.handleErrorAppState +import io.parity.signer.screens.scan.bananasplitcreate.BananaSplit +import io.parity.signer.ui.mainnavigation.CoreUnlockedNavSubgraph +import kotlinx.coroutines.launch + + +@Composable +fun CreateBananaSplitScreen( + coreNavController: NavController, + seedName: String, +) { + val vm: CreateBsViewModel = viewModel() + + val passPhrase: MutableState = rememberSaveable() { + mutableStateOf( + vm.generatePassPhrase(BananaSplit.defaultShards) + .handleErrorAppState(coreNavController) ?: "" + ) + } + + CreateBananaSplitScreenInternal( + onClose = { coreNavController.popBackStack() }, + onCreate = { maxShards -> + vm.viewModelScope.launch { + val result = vm.createBS(seedName, maxShards, passPhrase.value) + result.handleErrorAppState(coreNavController)?.let { + coreNavController.popBackStack() + coreNavController.navigate( + CoreUnlockedNavSubgraph.CreateBananaSplit.destination( + seedName + ) + ) + } + } + }, + updatePassowrd = { + passPhrase.value = vm.generatePassPhrase(BananaSplit.defaultShards) + .handleErrorAppState(coreNavController) ?: "" + }, + password = passPhrase.value, + modifier = Modifier.statusBarsPadding(), + ) +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBananaSplitScreenInternal.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBananaSplitScreenInternal.kt new file mode 100644 index 0000000000..78e3061f59 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBananaSplitScreenInternal.kt @@ -0,0 +1,248 @@ +package io.parity.signer.screens.scan.bananasplitcreate.create + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.parity.signer.R +import io.parity.signer.components.base.NotificationFrameTextImportant +import io.parity.signer.components.base.ScreenHeaderWithButton +import io.parity.signer.domain.Callback +import io.parity.signer.screens.scan.bananasplitcreate.BananaSplit +import io.parity.signer.ui.theme.SignerNewTheme +import io.parity.signer.ui.theme.SignerTypeface +import io.parity.signer.ui.theme.appliedStroke +import io.parity.signer.ui.theme.textSecondary +import io.parity.signer.ui.theme.textTertiary + + +@Composable +internal fun CreateBananaSplitScreenInternal( + onClose: Callback, + onCreate: (shards: Int) -> Unit, + updatePassowrd: (shards: Int) -> Unit, + password: String, + modifier: Modifier = Modifier, +) { + + val shardsField: MutableState = rememberSaveable { + mutableStateOf( + BananaSplit.defaultShards.toString() + ) + } + + val shardsValue: Int? = shardsField.value.toIntOrNull() + val canProceed: Boolean = shardsValue?.let { it > 2 } ?: false + + Column(modifier = modifier) { + ScreenHeaderWithButton( + canProceed = canProceed, + btnText = stringResource(R.string.create_action_cta), + onClose = onClose, onDone = { + if (canProceed && shardsValue != null) { + onCreate(shardsValue) + } + } + ) + Column(Modifier.verticalScroll(rememberScrollState())) { + Text( + text = stringResource(R.string.create_bs_title), + color = MaterialTheme.colors.primary, + style = SignerTypeface.TitleL, + modifier = Modifier + .padding(horizontal = 24.dp) + ) + Text( + text = stringResource(R.string.create_bs_subtitle), + color = MaterialTheme.colors.textTertiary, + style = SignerTypeface.LabelS, + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(vertical = 8.dp) + ) + Text( + text = stringResource(R.string.create_bs_shards_header), + color = MaterialTheme.colors.primary, + style = SignerTypeface.BodyL, + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 6.dp) + ) + OutlinedTextField( + value = shardsField.value, + onValueChange = { newStr: String -> shardsField.value = newStr }, + visualTransformation = VisualTransformation.None, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + // fixme #1749 recreation of options leading to first letter dissapearing on some samsung devices so keeping it always Done + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { + if (canProceed && shardsValue != null) { + onCreate(shardsValue) + } + }), + isError = Integer.getInteger(shardsField.value) == null, + singleLine = true, + textStyle = SignerTypeface.LabelM, + colors = TextFieldDefaults.textFieldColors( + textColor = MaterialTheme.colors.primary, + errorCursorColor = MaterialTheme.colors.primary, + ), + modifier = Modifier + .fillMaxWidth(1f) + .padding(horizontal = 16.dp) + ) + + if (canProceed && shardsValue != null) { + val requiresShards = BananaSplit.getMinShards(shardsValue) + Text( + text = stringResource( + R.string.create_bs_shards_description_required, + requiresShards, + shardsValue, + ), + color = MaterialTheme.colors.textTertiary, + style = SignerTypeface.CaptionM, + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(vertical = 8.dp) + ) + } else { + //error description + Text( + text = stringResource(R.string.create_bs_shards_description_error), + color = MaterialTheme.colors.error, + style = SignerTypeface.CaptionM, + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(vertical = 8.dp) + ) + } + //passcode section + PassPhraseBox( + passPhrase = password, + onUpdate = { + if (canProceed && shardsValue != null) { + updatePassowrd(shardsValue) + } + }, + ) + Text( + text = stringResource(R.string.create_bs_buttom_description), + color = MaterialTheme.colors.textTertiary, + style = SignerTypeface.CaptionM, + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(vertical = 8.dp) + ) + NotificationFrameTextImportant( + message = stringResource(R.string.create_bs_notification_frame_text), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 32.dp), + withBorder = false + ) + } + } +} + + +@Composable +private fun PassPhraseBox( + passPhrase: String, + onUpdate: Callback, + modifier: Modifier = Modifier +) { + val innerShape = + RoundedCornerShape(dimensionResource(id = R.dimen.innerFramesCornerRadius)) + Row( + modifier = modifier + .padding(vertical = 8.dp, horizontal = 16.dp) + .border( + BorderStroke(1.dp, MaterialTheme.colors.appliedStroke), + innerShape + ) + + ) { + + Column( + Modifier + .weight(1f) + .padding(vertical = 16.dp) + .padding(start = 16.dp) + ) { + Text( + text = stringResource(R.string.create_bs_password_header), + color = MaterialTheme.colors.textTertiary, + style = SignerTypeface.BodyM, + modifier = Modifier.padding(bottom = 4.dp) + ) + Text( + text = passPhrase, + color = MaterialTheme.colors.primary, + style = SignerTypeface.BodyL, + modifier = Modifier + ) + } + + Icon( + imageVector = Icons.Outlined.Refresh, + contentDescription = stringResource(R.string.create_bs_refresh_password_icon_description), + tint = MaterialTheme.colors.textSecondary, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 16.dp) + .size(32.dp) + .clickable(onClick = onUpdate) + ) + } +} + +@Preview( + name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, backgroundColor = 0xFFFFFFFF, +) +@Preview( + name = "dark", group = "general", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, backgroundColor = 0xFF000000, +) +@Composable +private fun PreviewCreateBananaSplitScreen() { + SignerNewTheme { + CreateBananaSplitScreenInternal( + onClose = {}, + onCreate = {}, + updatePassowrd = {}, + password = "delirium-claim-clad-down", + ) + } +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBsViewModel.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBsViewModel.kt new file mode 100644 index 0000000000..09cbbfb142 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/create/CreateBsViewModel.kt @@ -0,0 +1,32 @@ +package io.parity.signer.screens.scan.bananasplitcreate.create + +import androidx.lifecycle.ViewModel +import io.parity.signer.dependencygraph.ServiceLocator +import io.parity.signer.domain.backend.OperationResult +import io.parity.signer.domain.backend.map +import io.parity.signer.domain.backend.toOperationResult +import io.parity.signer.screens.scan.bananasplitcreate.BananaSplit +import io.parity.signer.uniffi.ErrorDisplayed +import io.parity.signer.uniffi.QrData +import kotlinx.coroutines.runBlocking + + +class CreateBsViewModel : ViewModel() { + private val uniffiInteractor = ServiceLocator.uniffiInteractor + private val bsRepository = ServiceLocator.activityScope!!.bsRepository + + fun generatePassPhrase(totalShards: Int): OperationResult { + return runBlocking { + uniffiInteractor.bsGeneratePassphrase(totalShards) + }.toOperationResult() + } + + suspend fun createBS( + seedName: String, + maxShards: Int, + passPhrase: String + ): OperationResult { + return bsRepository.creaseBs(seedName, maxShards, passPhrase) + } + +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitExportMenu.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitExportMenu.kt new file mode 100644 index 0000000000..73382e6d32 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitExportMenu.kt @@ -0,0 +1,110 @@ +package io.parity.signer.screens.scan.bananasplitcreate.show + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Password +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.parity.signer.R +import io.parity.signer.components.base.BottomSheetConfirmDialog +import io.parity.signer.components.base.SecondaryButtonWide +import io.parity.signer.domain.Callback +import io.parity.signer.screens.keydetails.MenuItemForBottomSheet +import io.parity.signer.ui.theme.SignerNewTheme +import io.parity.signer.ui.theme.red400 + + +@Composable +fun BananaSplitExportRemoveConfirmBottomSheet( + onCancel: Callback, + onRemoveKeySet: Callback, +) { + BottomSheetConfirmDialog( + title = stringResource(R.string.banana_split_menu_remove_confirm_title), + message = stringResource(R.string.banana_split_menu_remove_confirm_description), + ctaLabel = stringResource(R.string.remove_key_set_confirm_cta), + isCtaDangerous = true, + onCancel = onCancel, + onCta = onRemoveKeySet, + ) +} + +@Composable +fun BananaSplitExportMenuBottomSheet( + onShowPassphrase: Callback, + onRemoveBackup: Callback, + onCancel: Callback, +) { + val sidePadding = 24.dp + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = sidePadding, end = sidePadding, top = 8.dp), + ) { + + MenuItemForBottomSheet( + Icons.Outlined.Password, + label = stringResource(R.string.banana_split_menu_option_show_password), + onclick = onShowPassphrase + ) + + MenuItemForBottomSheet( + iconId = R.drawable.ic_backspace_28, + label = stringResource(R.string.banana_split_menu_option_remove_backup), + tint = MaterialTheme.colors.red400, + onclick = onRemoveBackup + ) + Spacer(modifier = Modifier.padding(bottom = 8.dp)) + SecondaryButtonWide( + label = stringResource(R.string.generic_cancel), + onClicked = onCancel + ) + Spacer(modifier = Modifier.padding(bottom = 16.dp)) + } +} + + + +@Preview( + name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, backgroundColor = 0xFFFFFFFF, +) +@Preview( + name = "dark", group = "general", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, backgroundColor = 0xFF000000, +) +@Composable +private fun PreviewBananaSplitExportBottomSheet() { + SignerNewTheme { + BananaSplitExportMenuBottomSheet( + {}, {}, {}, + ) + } +} + +@Preview( + name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, backgroundColor = 0xFFFFFFFF, +) +@Preview( + name = "dark", group = "general", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, backgroundColor = 0xFF000000, +) +@Composable +private fun PreviewKBananaSplitExportRemoveConfirmBottomSheet() { + SignerNewTheme { + BananaSplitExportRemoveConfirmBottomSheet( + {}, {}, + ) + } +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitExportScreen.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitExportScreen.kt new file mode 100644 index 0000000000..d2bc9ffe54 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitExportScreen.kt @@ -0,0 +1,88 @@ +package io.parity.signer.screens.scan.bananasplitcreate.show + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.parity.signer.R +import io.parity.signer.components.base.NotificationFrameText +import io.parity.signer.components.base.ScreenHeaderClose +import io.parity.signer.components.qrcode.AnimatedQrKeysInfo +import io.parity.signer.components.qrcode.EmptyAnimatedQrKeysProvider +import io.parity.signer.components.qrcode.EmptyQrCodeProvider +import io.parity.signer.domain.Callback +import io.parity.signer.domain.getData +import io.parity.signer.screens.keysetdetails.export.KeySetDetailsExportService +import io.parity.signer.ui.helpers.PreviewData +import io.parity.signer.ui.theme.SignerNewTheme +import io.parity.signer.ui.theme.fill6 +import io.parity.signer.uniffi.QrData + + +@Composable +fun BananaSplitExportScreen( + qrCodes: List, + onMenu: Callback, + onClose: Callback, + modifier: Modifier = Modifier, +) { + Column(modifier.fillMaxHeight(1f)) { + ScreenHeaderClose(title = "", onClose = onClose, onMenu = onMenu) + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) + .padding(start = 16.dp, end = 16.dp, bottom = 48.dp, top = 48.dp) + ) { + if (LocalInspectionMode.current) { + AnimatedQrKeysInfo( + input = Unit, + provider = EmptyAnimatedQrKeysProvider(), + modifier = Modifier.padding(8.dp) + ) + } else { + AnimatedQrKeysInfo( + input = qrCodes.map { it.getData() }, + provider = EmptyQrCodeProvider(), + modifier = Modifier.padding(8.dp) + ) + } + NotificationFrameText(message = stringResource(R.string.create_bs_export_notification_text)) + } + } +} + + +@Preview( + name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, backgroundColor = 0xFFFFFFFF, +) +@Preview( + name = "dark", group = "general", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, backgroundColor = 0xFF000000, +) +@Composable +private fun PreviewBananaSplitExportScreen() { + SignerNewTheme { + BananaSplitExportScreen( + qrCodes = listOf( + QrData.Regular(PreviewData.exampleQRData), + ), + onClose = {}, + onMenu = {}, + ) + } +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitShowFull.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitShowFull.kt new file mode 100644 index 0000000000..64952dc0e2 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitShowFull.kt @@ -0,0 +1,142 @@ +package io.parity.signer.screens.scan.bananasplitcreate.show + +import android.widget.Toast +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import io.parity.signer.R +import io.parity.signer.domain.Callback +import io.parity.signer.domain.backend.OperationResult +import io.parity.signer.screens.error.handleErrorAppState +import io.parity.signer.screens.scan.bananasplitcreate.BananaSplitCreateDestination +import io.parity.signer.ui.BottomSheetWrapperRoot +import io.parity.signer.ui.mainnavigation.CoreUnlockedNavSubgraph +import kotlinx.coroutines.launch + + +@Composable +fun BananaSplitShowFull( + coreNavController: NavController, + seedName: String, +) { + val menuNavController = rememberNavController() + + val vm: ShowBananaSplitViewModel = viewModel() + val qrCodes = remember { + val qrCodes = vm.getBananaSplitQrs(seedName) + if (qrCodes == null) { + //no BS for this seed - open create screen + coreNavController.navigate(BananaSplitCreateDestination.CreateBsCreateScreen) { + popUpTo(CoreUnlockedNavSubgraph.CreateBananaSplit.route) + } + } + qrCodes ?: emptyList() + } + + BananaSplitExportScreen( + qrCodes = qrCodes, + onMenu = { menuNavController.navigate(BananaSplitShowMenu.ShowBsMenu) }, + onClose = { coreNavController.popBackStack() }, + modifier = Modifier.statusBarsPadding(), + ) + + NavHost( + navController = menuNavController, + startDestination = BananaSplitShowMenu.Empty, + ) { + val closeAction: Callback = { + menuNavController.popBackStack() + } + composable(BananaSplitShowMenu.Empty) { + //no menu - Spacer element so when other part shown there won't + // be an appearance animation from top left part despite there shouldn't be + Spacer(modifier = Modifier.fillMaxSize(1f)) + } + composable(BananaSplitShowMenu.ShowBsMenu) { + BottomSheetWrapperRoot(onClosedAction = closeAction) { + BananaSplitExportMenuBottomSheet( + onCancel = closeAction, + onShowPassphrase = { + menuNavController.navigate(BananaSplitShowMenu.ShowBsSeePassword) { + popUpTo(BananaSplitShowMenu.Empty) + } + }, + onRemoveBackup = { + menuNavController.navigate(BananaSplitShowMenu.ShowBSConfirmRemove) { + popUpTo(BananaSplitShowMenu.Empty) + } + }, + ) + } + } + composable(BananaSplitShowMenu.ShowBSConfirmRemove) { + val context = LocalContext.current + BottomSheetWrapperRoot(onClosedAction = closeAction) { + BananaSplitExportRemoveConfirmBottomSheet( + onCancel = closeAction, + onRemoveKeySet = { + vm.viewModelScope.launch { + val result = + vm.removeBS(seedName).handleErrorAppState(coreNavController) + + result?.let { + //Show toast + Toast.makeText( + context, + context.getString(R.string.banana_split_backup_removed_ok), + Toast.LENGTH_SHORT + ).show() + coreNavController.popBackStack() + } + } + }, + ) + } + } + composable(BananaSplitShowMenu.ShowBsSeePassword) { + val password: MutableState = remember { mutableStateOf(null) } + LaunchedEffect(key1 = seedName) { + val result = + vm.getPassword(seedName) + when (result) { + is OperationResult.Err -> { + menuNavController.popBackStack() + (result as OperationResult).handleErrorAppState(coreNavController) + } + + is OperationResult.Ok -> { + password.value = result.result + } + } + } + password.value?.let { password -> + BottomSheetWrapperRoot(onClosedAction = closeAction) { + BananaSplitShowPassphraseMenu( + onClose = closeAction, + password = password, + ) + } + } + } + } +} + +private object BananaSplitShowMenu { + const val Empty = "show_bs" + const val ShowBSConfirmRemove = "show_bs_confirm_remove" + const val ShowBsMenu = "show_bs_menu" + const val ShowBsSeePassword = "show_bs_password_show" +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitShowPassphraseMenu.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitShowPassphraseMenu.kt new file mode 100644 index 0000000000..a44144951e --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/BananaSplitShowPassphraseMenu.kt @@ -0,0 +1,59 @@ +package io.parity.signer.screens.scan.bananasplitcreate.show + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.parity.signer.R +import io.parity.signer.components.base.BottomSheetHeader +import io.parity.signer.domain.Callback +import io.parity.signer.ui.theme.SignerNewTheme +import io.parity.signer.ui.theme.SignerTypeface + + +@Composable +fun BananaSplitShowPassphraseMenu( + password: String, + onClose: Callback, +) { + Column() { + BottomSheetHeader( + title = stringResource(R.string.banana_split_menu_password_title), + onClose = onClose + ) + Text( + text = password, + color = MaterialTheme.colors.primary, + style = SignerTypeface.BodyL, + maxLines = 1, + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 16.dp) + .padding(bottom = 8.dp) + ) + } +} + + +@Preview( + name = "light", group = "general", uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, backgroundColor = 0xFFFFFFFF, +) +@Preview( + name = "dark", group = "general", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, backgroundColor = 0xFF000000, +) +@Composable +private fun PreviewBananaSplitShowPassphraseMenu() { + SignerNewTheme { + BananaSplitShowPassphraseMenu( + "delirium-claim-clad-down", {}, + ) + } +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/ShowBananaSplitViewModel.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/ShowBananaSplitViewModel.kt new file mode 100644 index 0000000000..829b09e3b0 --- /dev/null +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitcreate/show/ShowBananaSplitViewModel.kt @@ -0,0 +1,29 @@ +package io.parity.signer.screens.scan.bananasplitcreate.show + +import androidx.lifecycle.ViewModel +import io.parity.signer.dependencygraph.ServiceLocator +import io.parity.signer.domain.backend.OperationResult +import io.parity.signer.domain.backend.UniffiInteractor +import io.parity.signer.uniffi.ErrorDisplayed +import io.parity.signer.uniffi.QrData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +class ShowBananaSplitViewModel : ViewModel() { + private val bsRepository = ServiceLocator.activityScope!!.bsRepository + + + fun getBananaSplitQrs(seedName: String): List? { + return bsRepository.getBsQrs(seedName) + } + + suspend fun removeBS(seedName: String): OperationResult { + return bsRepository.removeBS(seedName) + + } + + suspend fun getPassword(seedName: String): OperationResult { + return bsRepository.getBsPassword(seedName) + } +} diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitPasswordScreen.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitPasswordScreen.kt similarity index 99% rename from android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitPasswordScreen.kt rename to android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitPasswordScreen.kt index ab2dd99b3e..18904d1be4 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitPasswordScreen.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitPasswordScreen.kt @@ -1,4 +1,4 @@ -package io.parity.signer.screens.scan.bananasplit +package io.parity.signer.screens.scan.bananasplitrestore import android.annotation.SuppressLint import android.content.res.Configuration diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitSubgraph.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitSubgraph.kt similarity index 90% rename from android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitSubgraph.kt rename to android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitSubgraph.kt index 581719d1d7..d50ed30b0c 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitSubgraph.kt @@ -1,4 +1,4 @@ -package io.parity.signer.screens.scan.bananasplit +package io.parity.signer.screens.scan.bananasplitrestore import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -15,7 +15,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import io.parity.signer.domain.Callback -import io.parity.signer.screens.scan.bananasplit.networks.RecoverKeysetSelectNetworkBananaFlowScreen +import io.parity.signer.screens.scan.bananasplitrestore.networks.RecoverKeysetSelectNetworkBananaFlowScreen import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -24,6 +24,7 @@ import kotlinx.coroutines.launch fun BananaSplitSubgraph( qrData: List, onClose: Callback, + suggestedSeedName: String?, onSuccess: (newSeed: String) -> Unit, onErrorWrongPassword: Callback, onCustomError: (errorText: String) -> Unit, @@ -32,7 +33,7 @@ fun BananaSplitSubgraph( val bananaViewModel: BananaSplitViewModel = viewModel() DisposableEffect(qrData) { - bananaViewModel.initState(qrData) + bananaViewModel.initState(qrData, suggestedSeedName) onDispose { bananaViewModel.cleanState() } diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitViewModel.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitViewModel.kt similarity index 96% rename from android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitViewModel.kt rename to android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitViewModel.kt index 61dfc8ea40..283d442ed6 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/BananaSplitViewModel.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/BananaSplitViewModel.kt @@ -1,4 +1,4 @@ -package io.parity.signer.screens.scan.bananasplit +package io.parity.signer.screens.scan.bananasplitrestore import android.content.Context import androidx.lifecycle.ViewModel @@ -49,8 +49,9 @@ class BananaSplitViewModel() : ViewModel() { private lateinit var qrCodeData: List private var invalidPasswordAttempts = 0 - fun initState(qrCodeData: List) { + fun initState(qrCodeData: List, suggestedSeedName: String?) { cleanState() + this._seedName.value = suggestedSeedName ?: "" this.qrCodeData = qrCodeData this.invalidPasswordAttempts = 0 _isWrongPasswordTerminal.value = false diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/networks/BananaKeysetSelectNetworkBananaFlowScreen.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/networks/BananaKeysetSelectNetworkBananaFlowScreen.kt similarity index 94% rename from android/src/main/java/io/parity/signer/screens/scan/bananasplit/networks/BananaKeysetSelectNetworkBananaFlowScreen.kt rename to android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/networks/BananaKeysetSelectNetworkBananaFlowScreen.kt index ca1bd67667..dcb1574054 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/networks/BananaKeysetSelectNetworkBananaFlowScreen.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/networks/BananaKeysetSelectNetworkBananaFlowScreen.kt @@ -1,4 +1,4 @@ -package io.parity.signer.screens.scan.bananasplit.networks +package io.parity.signer.screens.scan.bananasplitrestore.networks import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState diff --git a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/networks/BananaNetworksViewModel.kt b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/networks/BananaNetworksViewModel.kt similarity index 89% rename from android/src/main/java/io/parity/signer/screens/scan/bananasplit/networks/BananaNetworksViewModel.kt rename to android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/networks/BananaNetworksViewModel.kt index 02863883fb..573c9a1bcf 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/bananasplit/networks/BananaNetworksViewModel.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/bananasplitrestore/networks/BananaNetworksViewModel.kt @@ -1,4 +1,4 @@ -package io.parity.signer.screens.scan.bananasplit.networks +package io.parity.signer.screens.scan.bananasplitrestore.networks import androidx.lifecycle.ViewModel import io.parity.signer.dependencygraph.ServiceLocator diff --git a/android/src/main/java/io/parity/signer/screens/scan/camera/CameraViewModel.kt b/android/src/main/java/io/parity/signer/screens/scan/camera/CameraViewModel.kt index 220994b5de..f5125bab29 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/camera/CameraViewModel.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/camera/CameraViewModel.kt @@ -8,7 +8,10 @@ import com.google.mlkit.vision.barcode.BarcodeScanner import com.google.mlkit.vision.common.InputImage import io.parity.signer.domain.encodeHex import io.parity.signer.domain.submitErrorState -import io.parity.signer.uniffi.* +import io.parity.signer.uniffi.BananaSplitRecoveryResult +import io.parity.signer.uniffi.DecodeSequenceResult +import io.parity.signer.uniffi.qrparserGetPacketsTotal +import io.parity.signer.uniffi.qrparserTryDecodeQrSequence import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -70,7 +73,9 @@ class CameraViewModel() : ViewModel() { barcodes.forEach { val payloadString = it?.rawBytes?.encodeHex() if (!currentMultiQrTransaction.contains(payloadString) && !payloadString.isNullOrEmpty()) { - if (total.value == null) { + val knownTotal = total.value + + if (knownTotal == null) { try { val proposeTotal = qrparserGetPacketsTotal(payloadString, true).toInt() @@ -82,23 +87,25 @@ class CameraViewModel() : ViewModel() { _total.value = proposeTotal } } catch (e: java.lang.Exception) { - Timber.e("scanVM", "QR sequence length estimation $e") + Timber.e("QR sequence length estimation $e") } } else { currentMultiQrTransaction += payloadString - if ((currentMultiQrTransaction.size + 1) >= (total.value ?: 0)) { + + if (currentMultiQrTransaction.size >= knownTotal) { decode(currentMultiQrTransaction.toList()) } else { _captured.value = currentMultiQrTransaction.size } - Timber.d("scanVM", "captured " + captured.value.toString()) + + Timber.d("captured " + captured.value.toString()) } } } Trace.endSection() } .addOnFailureListener { - Timber.e("scanVM", "Scan failed " + it.message.toString()) + Timber.e(it, "Scan failed") } .addOnCompleteListener { Trace.endSection() @@ -146,7 +153,7 @@ class CameraViewModel() : ViewModel() { } } catch (e: Exception) { - Timber.e("scanVM", "Single frame decode failed $e") + Timber.e(e, "Single frame decode failed") } } diff --git a/android/src/main/java/io/parity/signer/screens/scan/transaction/dynamicderivations/AddDerivedKeysScreen.kt b/android/src/main/java/io/parity/signer/screens/scan/transaction/dynamicderivations/AddDerivedKeysScreen.kt index ff294e922c..34d6ec4210 100644 --- a/android/src/main/java/io/parity/signer/screens/scan/transaction/dynamicderivations/AddDerivedKeysScreen.kt +++ b/android/src/main/java/io/parity/signer/screens/scan/transaction/dynamicderivations/AddDerivedKeysScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.parity.signer.R -import io.parity.signer.components.base.NotificationFrameTextAlert import io.parity.signer.components.base.NotificationFrameTextImportant import io.parity.signer.components.base.PrimaryButtonWide import io.parity.signer.components.base.ScreenHeaderClose diff --git a/android/src/main/java/io/parity/signer/screens/settings/backup/SeedBackupBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/settings/backup/SeedBackupBottomSheet.kt index 7f48de59c3..943d6861f3 100644 --- a/android/src/main/java/io/parity/signer/screens/settings/backup/SeedBackupBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/settings/backup/SeedBackupBottomSheet.kt @@ -64,7 +64,7 @@ private fun SeedBackupBottomSheet( //header BottomSheetHeader( title = seedName, - onCloseClicked = onClose, + onClose = onClose, ) SignerDivider(sidePadding = 24.dp) Column( diff --git a/android/src/main/java/io/parity/signer/screens/settings/networks/details/NetworkDetailsSubgraph.kt b/android/src/main/java/io/parity/signer/screens/settings/networks/details/NetworkDetailsSubgraph.kt index 86b012b9ff..bee9652f10 100644 --- a/android/src/main/java/io/parity/signer/screens/settings/networks/details/NetworkDetailsSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/settings/networks/details/NetworkDetailsSubgraph.kt @@ -63,7 +63,7 @@ fun NetworkDetailsSubgraph( ) }, onAddNetwork = { - navController.navigate(CoreUnlockedNavSubgraph.camera) + navController.navigate(CoreUnlockedNavSubgraph.Camera.destination(null)) menuController.popBackStack() }, ) diff --git a/android/src/main/java/io/parity/signer/screens/settings/networks/helper/NetworkHelpersSubgraph.kt b/android/src/main/java/io/parity/signer/screens/settings/networks/helper/NetworkHelpersSubgraph.kt index c27ac7b4f1..1d96586060 100644 --- a/android/src/main/java/io/parity/signer/screens/settings/networks/helper/NetworkHelpersSubgraph.kt +++ b/android/src/main/java/io/parity/signer/screens/settings/networks/helper/NetworkHelpersSubgraph.kt @@ -15,7 +15,7 @@ fun NavGraphBuilder.networkHelpersCoreSubgraph( ) { networkHelpersSubgraph( routePath = CoreUnlockedNavSubgraph.networkHelpers, - onScanClicked = { navController.navigate(CoreUnlockedNavSubgraph.camera) }, + onScanClicked = { navController.navigate(CoreUnlockedNavSubgraph.Camera.destination(null)) }, navController = navController, ) } diff --git a/android/src/main/java/io/parity/signer/screens/settings/networks/list/NetworksListScreenDestination.kt b/android/src/main/java/io/parity/signer/screens/settings/networks/list/NetworksListScreenDestination.kt index 75ae10da40..d80653b7a4 100644 --- a/android/src/main/java/io/parity/signer/screens/settings/networks/list/NetworksListScreenDestination.kt +++ b/android/src/main/java/io/parity/signer/screens/settings/networks/list/NetworksListScreenDestination.kt @@ -37,7 +37,7 @@ fun NavGraphBuilder.networkListDestination( ) }, onNetworkHelp = { navController.navigate(CoreUnlockedNavSubgraph.networkHelpers) }, - onAddNetwork = { navController.navigate(CoreUnlockedNavSubgraph.camera) }, + onAddNetwork = { navController.navigate(CoreUnlockedNavSubgraph.Camera.destination(null)) }, ) } } diff --git a/android/src/main/java/io/parity/signer/screens/settings/networks/signspecs/view/SignSpecsResultBottomSheet.kt b/android/src/main/java/io/parity/signer/screens/settings/networks/signspecs/view/SignSpecsResultBottomSheet.kt index a569ff88a7..47fae43b79 100644 --- a/android/src/main/java/io/parity/signer/screens/settings/networks/signspecs/view/SignSpecsResultBottomSheet.kt +++ b/android/src/main/java/io/parity/signer/screens/settings/networks/signspecs/view/SignSpecsResultBottomSheet.kt @@ -51,7 +51,7 @@ internal fun SignSpecsResultBottomSheet( ) { BottomSheetHeader( title = stringResource(R.string.sign_specs_result_title), - onCloseClicked = onBack + onClose = onBack ) Box( modifier = Modifier diff --git a/android/src/main/java/io/parity/signer/ui/mainnavigation/CoreUnlockedNavSubgraph.kt b/android/src/main/java/io/parity/signer/ui/mainnavigation/CoreUnlockedNavSubgraph.kt index 22b729df4e..8e16cdeddf 100644 --- a/android/src/main/java/io/parity/signer/ui/mainnavigation/CoreUnlockedNavSubgraph.kt +++ b/android/src/main/java/io/parity/signer/ui/mainnavigation/CoreUnlockedNavSubgraph.kt @@ -17,6 +17,7 @@ import io.parity.signer.screens.keysetdetails.keySetDetailsDestination import io.parity.signer.screens.keysets.create.NewKeysetSubgraph import io.parity.signer.screens.keysets.restore.KeysetRecoverSubgraph import io.parity.signer.screens.scan.ScanNavSubgraph +import io.parity.signer.screens.scan.bananasplitcreate.bananaSplitCreateDestination import io.parity.signer.screens.settings.networks.helper.networkHelpersCoreSubgraph import io.parity.signer.screens.settings.settingsFullSubgraph @@ -56,6 +57,7 @@ fun CoreUnlockedNavSubgraph(navController: NavHostController) { coreNavController = navController ) } + bananaSplitCreateDestination(navController) composable( route = CoreUnlockedNavSubgraph.KeyDetails.route, arguments = listOf( @@ -90,12 +92,18 @@ fun CoreUnlockedNavSubgraph(navController: NavHostController) { DerivationCreateSubgraph( onBack = { navController.popBackStack() }, - onOpenCamera = { navController.navigate(CoreUnlockedNavSubgraph.camera) }, + onOpenCamera = { + navController.navigate( + CoreUnlockedNavSubgraph.Camera.destination( + null + ) + ) + }, seedName = seedName, ) } composable( - CoreUnlockedNavSubgraph.camera, + CoreUnlockedNavSubgraph.Camera.route, enterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Up, @@ -108,11 +116,21 @@ fun CoreUnlockedNavSubgraph(navController: NavHostController) { animationSpec = tween() ) }, + arguments = listOf( + navArgument(CoreUnlockedNavSubgraph.Camera.bsKeysetArg) { + type = NavType.StringType + nullable = true + } + ) ) { + val bsKeysetValue = + it.arguments?.getString(CoreUnlockedNavSubgraph.Camera.bsKeysetArg) + ScanNavSubgraph( onCloseCamera = { navController.popBackStack() }, + seedNameSuggestion = bsKeysetValue, openKeySet = { seedName -> navController.navigate( CoreUnlockedNavSubgraph.KeySet.destination( @@ -137,7 +155,24 @@ fun CoreUnlockedNavSubgraph(navController: NavHostController) { object CoreUnlockedNavSubgraph { const val newKeySet = "core_new_keyset" const val recoverKeySet = "keyset_recover_flow" - const val camera = "unlocked_camera" + + object Camera { + internal const val bsKeysetArg = "seed_name_arg" + private const val baseRoute = "unlocked_camera" + const val route = "$baseRoute?$bsKeysetArg={$bsKeysetArg}" //optional + fun destination(bsKeysetValue: String?): String { + val result = + if (bsKeysetValue == null) baseRoute else "$baseRoute?$bsKeysetArg=${bsKeysetValue}" + return result + } + } + + object CreateBananaSplit { + internal const val seedNameArg = "seed_name_arg" + private const val baseRoute = "banana_split_create" + const val route = "$baseRoute/{$seedNameArg}" + fun destination(seedName: String) = "$baseRoute/$seedName" + } object KeySet { internal const val seedName = "seed_name_arg" @@ -179,6 +214,7 @@ object CoreUnlockedNavSubgraph { ) = "$baseRoute/$argHeader/$argDescription/$argVerbose" } + const val errorWrongDbVersionUpdate = "core_wrong_version_mismatch" const val airgapBreached = "core_airgap_blocker" diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 19b6d5e61c..f170fcbd2f 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -45,7 +45,8 @@ Locked key Network filter button Export Keys - Backup Key Set + Banana Split Backup + Manual Backup Database initiated Device was connected to network General verifier set @@ -453,6 +454,23 @@ Disable ADB on the device Couldn\'t open secure storage There was an error during opening secure storage. Possible reasons - device unlock method was changed. Error below + Create + Banana Split Backup + Backup your key set by turning the secret phrase into sharded QR codes with passphrase protection + Number of QR Code Shards + %1$d shards out of %2$d to reconstruct + "The number of shares must be no less than 2 " + Write down your passphrase. You\'ll need it to recover from Banana Split. + Banana Split backup will recover the key set without derived keys. To back up derived keys, use the manual backup option. Each key will have to be added individually by entering the derivation path name. + Passphrase for the Recovery + Refresh Password + Scan this animated QR code into the bs.parity.io app to print QR code shards. + Remove Banana Split + You can still use this backup if you have QR code shards printed and have passphrase written down on paper. + Show Passphrase + Remove Backup + Passphrase + Banana Split Backup removed