From 8d7d7b5f84cfd9a17c18d0193796302e03982d73 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sat, 22 Aug 2026 18:45:28 +0900 Subject: [PATCH] WIP --- README.md | 150 +++-- app/src/main/AndroidManifest.xml | 4 +- .../java/swiss/qpq/gajumobile/MainActivity.kt | 368 ++++++++--- .../swiss/qpq/gajumobile/data/AppState.kt | 34 +- .../qpq/gajumobile/data/WalletRepository.kt | 17 + .../qpq/gajumobile/data/models/Account.kt | 29 +- .../gajumobile/security/AccountAirlock.java | 53 +- .../gajumobile/security/EncryptionService.kt | 2 - .../qpq/gajumobile/security/KeyManager.kt | 19 +- .../ui/components/GajuComponents.kt | 335 ++++++---- .../ui/screens/AccountNamingScreen.kt | 140 ++++ .../ui/screens/CreateWalletScreen.kt | 85 +++ .../gajumobile/ui/screens/DashboardScreen.kt | 276 ++++++-- .../ui/screens/DeleteConfirmationScreen.kt | 148 +++++ .../ui/screens/EnvironmentInfoScreen.kt | 99 +++ .../gajumobile/ui/screens/ExplorerScreen.kt | 61 ++ .../qpq/gajumobile/ui/screens/LockScreen.kt | 4 +- .../ui/screens/ManageAccountScreen.kt | 209 ++++++ .../ui/screens/ManageWalletScreen.kt | 146 +++++ .../ui/screens/ManagementScreens.kt | 602 ------------------ .../ui/screens/MnemonicRecoveryScreen.kt | 186 ++++++ .../ui/screens/MnemonicVerifyScreen.kt | 209 ++++++ .../gajumobile/ui/screens/OnboardingFlow.kt | 299 --------- .../gajumobile/ui/screens/QRReceiveScreen.kt | 98 +++ .../gajumobile/ui/screens/QRScannerScreen.kt | 86 +++ .../gajumobile/ui/screens/SendFormScreen.kt | 135 ++++ .../gajumobile/ui/screens/SendReviewScreen.kt | 140 ++++ .../gajumobile/ui/screens/SettingsScreen.kt | 110 ++++ .../ui/screens/SetupChoiceScreen.kt | 83 +++ .../ui/screens/TransactionScreens.kt | 451 ------------- .../ui/screens/TransactionSuccessScreen.kt | 40 ++ .../ui/screens/ViewMnemonicScreen.kt | 66 ++ .../ui/screens/WalletSetupScreens.kt | 507 --------------- app/src/main/res/drawable/ic_add.xml | 9 + app/src/main/res/drawable/ic_check_circle.xml | 9 + app/src/main/res/drawable/ic_content_copy.xml | 10 + app/src/main/res/drawable/ic_delete.xml | 9 + app/src/main/res/drawable/ic_edit.xml | 9 + app/src/main/res/drawable/ic_lock.xml | 9 + app/src/main/res/drawable/ic_more_vert.xml | 9 + app/src/main/res/drawable/ic_qr_code.xml | 9 + app/src/main/res/drawable/ic_visibility.xml | 9 + 42 files changed, 3035 insertions(+), 2238 deletions(-) create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletScreen.kt delete mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManagementScreens.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt delete mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/OnboardingFlow.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt delete mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionScreens.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt create mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/ViewMnemonicScreen.kt delete mode 100644 app/src/main/java/swiss/qpq/gajumobile/ui/screens/WalletSetupScreens.kt create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_check_circle.xml create mode 100644 app/src/main/res/drawable/ic_content_copy.xml create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/drawable/ic_lock.xml create mode 100644 app/src/main/res/drawable/ic_more_vert.xml create mode 100644 app/src/main/res/drawable/ic_qr_code.xml create mode 100644 app/src/main/res/drawable/ic_visibility.xml diff --git a/README.md b/README.md index 3bce6af..5c2fcb3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # GajuMobile -The Android implementation of the GajuMobile wallet. +The Android implementation of the Gajumaru client application +(essentially GajuDesk-lite). -This wallet is primarily a signature device. Its purpose is not to be an -all-in-one app for every possible Gajumaru feature. It is also not intended to +This app is primarily a signature device. Its purpose is not to be an +all-in-one app for every possible Gajumaru feature. It is also not intended to integrate directly with every major app that is integrated with the Gajumaru. @@ -15,81 +16,114 @@ GRIDS. App-specific functionality (direct integration with a DEX or other type of exchange, interpretation of NFTs, transaction history tracking, etc.) are -outside of scope for GajuMobile. +out of scope for GajuMobile. -Other applications can be linked to by GajuMobile, and other applications can -form Gajumaru transactions that GajuMobile can then be prompted to sign through -GRIDS, whether over the LAN or WAN. +Other applications can be linked to by GajuMobile via clickable URL components, +and other applications can form Gajumaru transactions that GajuMobile can then +be prompted to sign through GRIDS, whether over the LAN or WAN. ## Project rules -GajuMobile (whether on Android, iOS, or Pine) is not permitted to include any -foreign dependencies, where "foreign" is defined as any libraries coming from -outside the Gajumaru project itself. As we cannot escape Android, at a -minimum, we are forced to trust that Android itself is trustworthy, and as -Jetpack Compose and Kotlin are now part of the core definition, those must be -trusted as well. - -I don't like it, and my opinions are objectively correct, but this is the -baseline scope of trust we must accept. At least it is well defined. - -This project is not permitted to use Kotlin for most primitive operations due -to basic language, build and runtime constraints imposed by the (ridiculous) -language definition. For this reason primitives that provide the functionality -found in Hakuzaru and supporting Erlang libraries have been ported in -self-contained plain-Java libraries provided by the gm-java project. In general -gm-java is written in such a way that fragments of old object memory is not -left scattered across the heap awaiting GC, and what garbage there is that -might be sensitive can be zeroed before leaving scope (yes, that was pretty -annoying to achieve in Java, and it is actually impossible to achieve in -Kotlin). - -Android provides a number of facilities to achieve some memory isolation, -however, and these facilities must be used. As most of the facilities that are -of greatest interest to us in the wallet context did not exist prior to Android -14, that is the oldest version we may currently support. Hopefully we can -retire that version sooner than later, and absolutely *must* retire support for -Android 14 once it leaves the official support window. - -It would be entirely possible to augment this project to work on older Android -phones, and this may even be a good idea because it would make GajuMobile a -more inclusive platform. The method to support that would be to build a second, -parallel project with support for legacy devices instead, though, rather than -force this project to maintain support for more fallback security mechanisms -and platform detection than it already is forced to. +To maintain the security posture of this project, every contributor must +strictly adhere to these rules. "Industry standards" and "best practices" are +generally regarded here as vectors for collective incompetence and unvetted +dependency bloat. We write our own code. ### Reasoning -Kotlin is a rats' nest of antisensical decisions and techniques designed by -smart people for consumption by less smart people. This results in a so-called -"expressive" mess of language featuritis that can't decide what paradigm it -lives in, so it lives in them all while achieving none of them. - It is impossible to tell from the code alone what is actually going to happen in the runtime, or even understand what is going to happen in the potentially limitless number of pre-compilation steps based on what files are even included in a given module, much less pin down a truly deterministic build profile if -external dependencies are included. This is the primary reason for the "no +external dependencies are included. This is the primary reason for the "no foreign deps" rule. +### 1. No Foreign Dependencies + +GajuMobile (whether on Android, iOS, or Pine) is not permitted to include any +foreign dependencies. A "foreign" dependency is defined as any library +originating from outside the Gajumaru project itself. + +Since we cannot escape the platforms we target, we accept the baseline trust +of the OS, its core UI toolkits, and primary languages (Jetpack Compose/Kotlin, +SwiftUI/Swift). Any attempt to import external convenience libraries will +be rejected. I don't like it, but my opinions are objectively correct, and +this is the boundary of trust we accept because it is at least well-defined. + + +### 2. The Airlock Pattern & Memory Hygiene + +Kotlin is a rats' nest of antisensical decisions and techniques. Kotlin is +hipster junk food created by smart people for consumption by less smart people. +It is a midwit's wet dream of "expressive" garbage, a language suffering an +extreme case of featuritis that can't decide what paradigm it lives in, so it +lives in them all while achieving none of them. + +It is impossible to achieve memory hygiene in Kotlin. Therefore, all +sensitive cryptographic operations -- key generation, signing, and mnemonic +derivation -- must occur within `AccountAirlock.java`. + +* **Secrets Isolation:** Sensitive material may never exist as Kotlin + `String` or `ByteArray` objects for longer than a single stack frame, and + only when the user actually must interact with secrets directly, namely + when viewing or entering their mnemonic. +* **Explicit Decontamination:** All secret buffers must be explicitly + zeroed using `CryptoUtils.wipe()` in a `finally` block before leaving + scope. (See the gm-java project README.) +* **gm-java Primitives:** Primitives providing functionality found in + Hakuzaru/Erlang must be implemented in the self-contained `gm-java` + project. This project is written to ensure that fragments of old object + memory are not left scattered across the heap awaiting GC. + + +### 3. Forbidden Primitives + +Do not use `java.math.BigInteger` for any key-related material (seeds, +private keys, mnemonics). Use the Radix-2^25.5 field arithmetic implemented +in `gm-java`. BigInteger is a leak-prone abstraction that has no place in +security sensitive contexts. + + +### 4. Hardware-Enforced Security + +As the security facilities required for a proper signature device did not +exist prior to Android 14, that is the oldest version we support. We must +exploit the strongest security facilities available: +* **Keystore Policy:** The Master Key must use + `setUserAuthenticationRequired(true)`, delegating authorization to + the system's secure lock. +* **Entropy:** Use `setRandomizedEncryptionRequired(true)` to ensure + the hardware module generates high-entropy IVs. +* **StrongBox:** Use `setIsStrongBoxBacked(true)` with a graceful TEE + fallback. + + +### 5. Explicit Imports + +All `import` directives must be fully specified. `import foo.bar.baz.*` style +wildcard imports are strictly forbidden. Wildcards hide intent, create +ambiguity in the build, and are potentially dangerous in Kotlin. + + ## Terms -Copyright (c) 2026 QPQ AG . All rights reserved. Project: -Gajumaru Core Java Libraries +Copyright (c) 2026 QPQ AG . All rights reserved. +Project: Gajumaru Core Java Libraries -This program is dual-licensed: 1) Under the GNU Affero General Public License -as published by the Free Software Foundation, either version 3 of the License, -or (at your option) any later version (AGPL-3.0-or-later). (See LICENSE file.) +This program is dual-licensed: +1) Under the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version (AGPL-3.0-or-later). (See LICENSE file.) -2) Under a commercial/proprietary license available directly from QPQ AG. If -you wish to use this software outside the strict constraints of the AGPLv3 -(e.g. within a closed-source or proprietary product), you must purchase a -commercial license from QPQ AG. +2) Under a commercial/proprietary license available directly from QPQ AG. + If you wish to use this software outside the strict constraints of the + AGPLv3 (e.g. within a closed-source or proprietary product), you must + purchase a commercial license from QPQ AG. Authors: - - Craig Everett +- Craig Everett -SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial +SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac44bde..84f5e6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,9 @@ - lockErrorMessage = err - } + onError = { err -> + lockErrorMessage = err + } + ) } } else { val routerError: (String) -> Unit = { message -> @@ -89,19 +95,17 @@ enum class Screen { SETUP_CHOICE, MNEMONIC_RECOVERY, ACCOUNT_NAMING, - SETUP_SUCCESS, DASHBOARD, QR_SCANNER, QR_RECEIVE, - SEND_FORM, - SEND_REVIEW, - SEND_SUCCESS, SETTINGS, - MANAGE_OVERVIEW, MANAGE_WALLET, MANAGE_ACCOUNT, DELETE_CONFIRM, - EXPLORER + EXPLORER, + VIEW_MNEMONIC, + VERIFY_MNEMONIC, + ENVIRONMENT_INFO } @Composable @@ -111,124 +115,314 @@ fun GajuRouter( onError: (String) -> Unit ) { val context = LocalContext.current + val appState by appStateManager.state.collectAsState() - // Determine start screen val initialScreen = remember { - if (walletRepo.listWallets().isEmpty()) { - Screen.CREATE_WALLET - } else { - Screen.DASHBOARD - } + if (walletRepo.listWallets().isEmpty()) Screen.CREATE_WALLET else Screen.DASHBOARD } - var currentScreen by rememberSaveable { mutableStateOf(initialScreen) } + val screenStack = remember { mutableStateListOf(initialScreen) } + val currentScreen = screenStack.last() + + fun navigateTo(screen: Screen) { + screenStack.add(screen) + } + + fun navigateBack() { + if (screenStack.size > 1) { + screenStack.removeAt(screenStack.size - 1) + } + } + + BackHandler(enabled = screenStack.size > 1) { + navigateBack() + } var pendingWalletName by rememberSaveable { mutableStateOf("") } var pendingAccount by remember { mutableStateOf(null) } var setupRecovered by rememberSaveable { mutableStateOf(false) } + var selectedWalletId by rememberSaveable { mutableStateOf("") } + var selectedAccountId by rememberSaveable { mutableStateOf("") } + + var deleteType by rememberSaveable { mutableStateOf("") } + var deleteName by rememberSaveable { mutableStateOf("") } + + var viewMnemonicAccount by remember { mutableStateOf(null) } + var viewMnemonicPhrase by remember { mutableStateOf?>(null) } + + var verifyMnemonicAccount by remember { mutableStateOf(null) } + var verifyMnemonicPhrase by remember { mutableStateOf?>(null) } + when (currentScreen) { Screen.CREATE_WALLET -> { - CreateWalletScreen { name -> - pendingWalletName = name - currentScreen = Screen.SETUP_CHOICE - } + CreateWalletScreen( + onWalletCreated = { name -> + pendingWalletName = name + selectedWalletId = "" + navigateTo(Screen.SETUP_CHOICE) + }, + onBack = if (screenStack.size > 1) { { navigateBack() } } else null + ) } Screen.SETUP_CHOICE -> { SetupChoiceScreen( onCreate = { - android.util.Log.d("GajuRouter", "CREATE ACCOUNT clicked") try { val masterKey = KeyManager.getMasterKey(context) - android.util.Log.d("GajuRouter", "Master key retrieved") val phrase = AccountAirlock.generateMnemonic() - android.util.Log.d("GajuRouter", "Mnemonic generated") - val account = AccountAirlock.createAccount("Account 1", phrase, masterKey) - android.util.Log.d("GajuRouter", "Account created: ${account.id}") + val account = AccountAirlock.createAccount("New Account", phrase, masterKey) AccountAirlock.wipePhrase(phrase) pendingAccount = account setupRecovered = false - currentScreen = Screen.ACCOUNT_NAMING + navigateTo(Screen.ACCOUNT_NAMING) } catch (e: Exception) { - android.util.Log.e("GajuRouter", "Failed to generate account", e) onError("Failed to generate account: ${e.message}") } }, onRecover = { setupRecovered = true - currentScreen = Screen.MNEMONIC_RECOVERY - } + navigateTo(Screen.MNEMONIC_RECOVERY) + }, + onBack = { navigateBack() } ) } Screen.MNEMONIC_RECOVERY -> { MnemonicRecoveryScreen( onMnemonicConfirmed = { phrase -> - android.util.Log.d("GajuRouter", "RECOVER ACCOUNT clicked") try { val masterKey = KeyManager.getMasterKey(context) - android.util.Log.d("GajuRouter", "Master key retrieved") - val account = AccountAirlock.createAccount("Account 1", phrase, masterKey) - android.util.Log.d("GajuRouter", "Account recovered: ${account.id}") + val account = AccountAirlock.createAccount("Recovered Account", phrase, masterKey) AccountAirlock.wipePhrase(phrase) pendingAccount = account - currentScreen = Screen.ACCOUNT_NAMING + navigateTo(Screen.ACCOUNT_NAMING) } catch (e: Exception) { - android.util.Log.e("GajuRouter", "Failed to recover account", e) onError("Failed to recover account: ${e.message}") } }, - onBack = { currentScreen = Screen.SETUP_CHOICE } + onBack = { navigateBack() } ) } Screen.ACCOUNT_NAMING -> { - val account = pendingAccount - if (account == null) { - android.util.Log.e("GajuRouter", "pendingAccount is NULL in ACCOUNT_NAMING") - return - } - val akId = ApiEncoder.encode(ApiEncoder.Type.ACCOUNT_PUBKEY, account.publicKey) - android.util.Log.d("GajuRouter", "Showing ACCOUNT_NAMING for $akId") + val account = pendingAccount ?: return + val akId = account.gajuId - AccountNamingScreen(publicKey = akId) { name -> - val finalAccount = account.copy(label = name) - val wallet = Wallet( - id = UUID.randomUUID().toString(), - name = pendingWalletName.ifBlank { "My Wallet" }, - accounts = listOf(finalAccount) - ) - walletRepo.saveWallet(wallet) - - if (!setupRecovered) { - appStateManager.markUnverified(finalAccount.id) - } - - currentScreen = Screen.DASHBOARD + AccountNamingScreen( + publicKey = akId, + onAccountNamed = { name -> + val finalAccount = account.copy(label = name) + + if (selectedWalletId.isNotEmpty()) { + val wallet = walletRepo.loadWallet(selectedWalletId) + if (wallet != null) { + walletRepo.saveWallet(wallet.copy(accounts = wallet.accounts + finalAccount)) + } + } else { + val wallet = Wallet( + id = UUID.randomUUID().toString(), + name = pendingWalletName.ifBlank { "My Wallet" }, + accounts = listOf(finalAccount) + ) + walletRepo.saveWallet(wallet) + } + + if (!setupRecovered) { + appStateManager.markUnverified(finalAccount.id) + } + + selectedWalletId = "" + appStateManager.notifyWalletsChanged() + screenStack.clear() + screenStack.add(Screen.DASHBOARD) + }, + onBack = { navigateBack() } + ) + } + Screen.VIEW_MNEMONIC -> { + val acc = viewMnemonicAccount ?: return + val phrase = viewMnemonicPhrase ?: return + ViewMnemonicScreen(label = acc.label, phrase = phrase) { + AccountAirlock.wipePhrase(phrase) + viewMnemonicAccount = null + viewMnemonicPhrase = null + navigateBack() } } - Screen.DASHBOARD -> { - DashboardScreen( - onSend = { currentScreen = Screen.QR_SCANNER }, - onReceive = { currentScreen = Screen.QR_RECEIVE }, - onNavigate = { dest -> - currentScreen = when (dest) { - "settings" -> Screen.SETTINGS - "manage" -> Screen.MANAGE_OVERVIEW - "transactions" -> Screen.EXPLORER - else -> Screen.DASHBOARD + Screen.VERIFY_MNEMONIC -> { + val acc = verifyMnemonicAccount ?: return + val phrase = verifyMnemonicPhrase ?: return + MnemonicVerifyScreen( + label = acc.label, + expectedPhrase = phrase, + onVerified = { + appStateManager.clearVerified(acc.id) + AccountAirlock.wipePhrase(phrase) + verifyMnemonicAccount = null + verifyMnemonicPhrase = null + navigateBack() + }, + onBack = { + AccountAirlock.wipePhrase(phrase) + verifyMnemonicAccount = null + verifyMnemonicPhrase = null + navigateBack() + } + ) + } + Screen.DELETE_CONFIRM -> { + DeleteConfirmationScreen( + type = deleteType, + name = deleteName, + onConfirm = { + if (deleteType == "WALLET") { + walletRepo.deleteWallet(selectedWalletId) + } else { + walletRepo.deleteAccount(selectedWalletId, selectedAccountId) } + appStateManager.notifyWalletsChanged() + screenStack.removeAt(screenStack.size - 1) + screenStack.removeAt(screenStack.size - 1) + }, + onCancel = { navigateBack() } + ) + } + Screen.DASHBOARD -> { + val wallets = remember(appState.walletsVersion) { + walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } + } + DashboardScreen( + wallets = wallets, + balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), + balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), + onSend = { navigateTo(Screen.QR_SCANNER) }, + onReceive = { navigateTo(Screen.QR_RECEIVE) }, + onAddWallet = { + selectedWalletId = "" + navigateTo(Screen.CREATE_WALLET) + }, + onAddAccount = { walletId -> + selectedWalletId = walletId + navigateTo(Screen.SETUP_CHOICE) + }, + onAccountSettings = { walletId, accountId -> + selectedWalletId = walletId + selectedAccountId = accountId + navigateTo(Screen.MANAGE_ACCOUNT) + }, + onNavigate = { dest -> + val screen = when (dest) { + "settings" -> Screen.SETTINGS + "transactions" -> Screen.EXPLORER + "environment_info" -> Screen.ENVIRONMENT_INFO + else -> null + } + if (screen != null) navigateTo(screen) }, ) } - // Fallback mapping for existing screens - Screen.QR_SCANNER -> QRScannerScreen(onGenerate = { currentScreen = Screen.QR_RECEIVE }, onUpload = {}) { currentScreen = Screen.DASHBOARD } - Screen.QR_RECEIVE -> QRReceiveScreen(onDone = { currentScreen = Screen.DASHBOARD }) { currentScreen = Screen.DASHBOARD } - Screen.SETTINGS -> SettingsScreen(onNavigate = { currentScreen = Screen.DASHBOARD }, onBack = { currentScreen = Screen.DASHBOARD }) - Screen.MANAGE_OVERVIEW -> ManageOverviewScreen(onWalletDetail = {}, onAccountDetail = {}, onNavigate = { currentScreen = Screen.DASHBOARD }) - - else -> { - Text("Screen $currentScreen not yet fully wired") + Screen.MANAGE_WALLET -> { + val wallet = remember(selectedWalletId, appState.walletsVersion) { + walletRepo.loadWallet(selectedWalletId) + } ?: return + ManageWalletScreen( + walletName = wallet.name, + onRename = { newName -> + walletRepo.saveWallet(wallet.copy(name = newName)) + appStateManager.notifyWalletsChanged() + }, + onDelete = { + deleteType = "WALLET" + deleteName = wallet.name + navigateTo(Screen.DELETE_CONFIRM) + } + ) } + Screen.MANAGE_ACCOUNT -> { + val wallet = remember(selectedWalletId, appState.walletsVersion) { + walletRepo.loadWallet(selectedWalletId) + } ?: return + val account = wallet.accounts.find { it.id == selectedAccountId } ?: return + val isUnverified = appState.unverifiedAccountIds.contains(account.id) + + ManageAccountScreen( + account = account, + walletName = wallet.name, + balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), + balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), + isUnverified = isUnverified, + onViewMnemonic = { + try { + val masterKey = KeyManager.getMasterKey(context) + val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey) + viewMnemonicAccount = account + viewMnemonicPhrase = phrase + navigateTo(Screen.VIEW_MNEMONIC) + } catch (e: Exception) { + onError("Authentication failed: ${e.message}") + } + }, + onVerifyMnemonic = { + try { + val masterKey = KeyManager.getMasterKey(context) + val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey) + verifyMnemonicAccount = account + verifyMnemonicPhrase = phrase + navigateTo(Screen.VERIFY_MNEMONIC) + } catch (e: Exception) { + onError("Authentication failed: ${e.message}") + } + }, + onRename = { newName -> + val updatedAccounts = wallet.accounts.map { + if (it.id == account.id) it.copy(label = newName) else it + } + walletRepo.saveWallet(wallet.copy(accounts = updatedAccounts)) + appStateManager.notifyWalletsChanged() + }, + onDelete = { + deleteType = "ACCOUNT" + deleteName = account.label + navigateTo(Screen.DELETE_CONFIRM) + } + ) + } + Screen.SETTINGS -> { + SettingsScreen( + balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), + onFormatChange = { type -> + appStateManager.setBalanceFormat(type.name) + }, + onNavigate = { dest -> + val screen = when (dest) { + "dashboard" -> Screen.DASHBOARD + "transactions" -> Screen.EXPLORER + "environment_info" -> Screen.ENVIRONMENT_INFO + else -> null + } + if (screen != null) { + screenStack.clear() + screenStack.add(screen) + } + } + ) + } + Screen.ENVIRONMENT_INFO -> { + EnvironmentInfoScreen(onBack = { navigateBack() }) + } + Screen.QR_SCANNER -> QRScannerScreen(onGenerate = { navigateTo(Screen.QR_RECEIVE) }, onUpload = {}) + Screen.QR_RECEIVE -> QRReceiveScreen(onDone = { navigateBack() }) + Screen.EXPLORER -> ExplorerScreen(onNavigate = { dest -> + val screen = when (dest) { + "dashboard" -> Screen.DASHBOARD + "settings" -> Screen.SETTINGS + else -> null + } + if (screen != null) { + screenStack.clear() + screenStack.add(screen) + } + }) } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/AppState.kt b/app/src/main/java/swiss/qpq/gajumobile/data/AppState.kt index 2251d4c..1701ae2 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/data/AppState.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/data/AppState.kt @@ -1,21 +1,31 @@ package swiss.qpq.gajumobile.data import android.content.Context +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.encodeToString import java.io.File @Serializable data class AppState( val unverifiedAccountIds: Set = emptySet(), - val lastPesteredAt: Long = 0L + val lastPesteredAt: Long = 0L, + val balanceFormat: String = "US", + val balanceUnit: String = "GAJU", + val walletsVersion: Int = 0 // Incremented on wallet changes to trigger UI updates ) class AppStateManager(context: Context) { private val json = Json { prettyPrint = true } private val stateFile = File(context.filesDir, "app_state.json") + + private val _state = MutableStateFlow(loadStateFromFile()) + val state: StateFlow = _state.asStateFlow() - fun loadState(): AppState { + private fun loadStateFromFile(): AppState { return if (stateFile.exists()) { try { json.decodeFromString(stateFile.readText()) @@ -29,15 +39,31 @@ class AppStateManager(context: Context) { fun saveState(state: AppState) { stateFile.writeText(json.encodeToString(state)) + _state.value = state } fun markUnverified(accountId: String) { - val current = loadState() + val current = _state.value saveState(current.copy(unverifiedAccountIds = current.unverifiedAccountIds + accountId)) } fun clearVerified(accountId: String) { - val current = loadState() + val current = _state.value saveState(current.copy(unverifiedAccountIds = current.unverifiedAccountIds - accountId)) } + + fun setBalanceFormat(format: String) { + val current = _state.value + saveState(current.copy(balanceFormat = format)) + } + + fun setBalanceUnit(unit: String) { + val current = _state.value + saveState(current.copy(balanceUnit = unit)) + } + + fun notifyWalletsChanged() { + val current = _state.value + saveState(current.copy(walletsVersion = current.walletsVersion + 1)) + } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt b/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt index ee74f9e..815b32a 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt @@ -22,6 +22,23 @@ class WalletRepository(private val context: Context) { file.writeText(envelopeJson) } + fun deleteWallet(walletId: String) { + val file = getWalletFile(walletId) + if (file.exists()) { + file.delete() + } + } + + fun deleteAccount(walletId: String, accountId: String) { + val wallet = loadWallet(walletId) ?: return + val updatedAccounts = wallet.accounts.filter { it.id != accountId } + if (updatedAccounts.isEmpty()) { + deleteWallet(walletId) + } else { + saveWallet(wallet.copy(accounts = updatedAccounts)) + } + } + fun loadWallet(walletId: String): Wallet? { val file = getWalletFile(walletId) if (!file.exists()) return null diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt b/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt index ece3cab..7209599 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt @@ -2,6 +2,9 @@ package swiss.qpq.gajumobile.data.models import kotlinx.serialization.Serializable import swiss.qpq.gajumobile.security.EncryptedEnvelope +import swiss.qpq.gajumaru.core.encoding.ApiEncoder +import swiss.qpq.gajumaru.core.formatting.GajuFormat +import java.math.BigInteger @Serializable data class Account( @@ -11,6 +14,29 @@ data class Account( val privateKeyEnvelope: EncryptedEnvelope, val cachedBalance: CachedBalance? = null ) { + /** + * The canonical Gajumaru protocol ID (ak_...). + */ + val gajuId: String + get() = ApiEncoder.encode(ApiEncoder.Type.ACCOUNT_PUBKEY, publicKey) + + /** + * A shortened version of the ID for tight UI layouts. + */ + fun getShortId(): String { + val full = gajuId + return if (full.length <= 16) full else "${full.take(8)}...${full.takeLast(8)}" + } + + /** + * Renders the cached balance using the provided formatting rules. + */ + fun formattedBalance(type: GajuFormat.Type, unit: GajuFormat.Unit): String { + val bi = try { BigInteger(cachedBalance?.totalPucks ?: "0") } catch (_: Exception) { BigInteger.ZERO } + val spec = GajuFormat.FormatSpec(type, unit, ',', 3) + return GajuFormat.amount(spec, bi) + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -38,8 +64,7 @@ data class Account( @Serializable data class CachedBalance( - val amount: String, - val pucks: String, // Fractional part + val totalPucks: String, val chainId: String, val timestamp: Long ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java index 32b03db..6fda690 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java +++ b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java @@ -132,12 +132,6 @@ public final class AccountAirlock { /** * Decrypts an account's private key and signs a message. - * - * @param envelope The encrypted private key envelope. - * @param masterKey The hardware-backed Master Key handle from Android Keystore. - * @param message The message to sign. - * @return The Ed25519 signature. - * @throws Exception if decryption or signing fails. */ public static byte[] sign( EncryptedEnvelope envelope, @@ -149,7 +143,6 @@ public final class AccountAirlock { byte[] plaintextPrivateKey = null; try { - // 1. Decrypt the Data Encryption Key (DEK) using the Master Key plaintextDek = Vault.decrypt( masterKey, envelope.getDekIv(), @@ -157,26 +150,50 @@ public final class AccountAirlock { ); SecretKey dek = new SecretKeySpec(plaintextDek, "AES"); - // 2. Decrypt the Private Key using the DEK plaintextPrivateKey = Vault.decrypt( dek, envelope.getDataIv(), envelope.getEncryptedData() ); - // 3. Perform Ed25519 signature - // The Ed25519 implementation in gm-java uses internal scratch pads - // that are also wiped upon completion. return Ed25519.sign(plaintextPrivateKey, message); } finally { - // 4. Memory Hygiene: Wipe sensitive data from the heap - if (plaintextDek != null) { - CryptoUtils.wipe(plaintextDek); - } - if (plaintextPrivateKey != null) { - CryptoUtils.wipe(plaintextPrivateKey); - } + if (plaintextDek != null) CryptoUtils.wipe(plaintextDek); + if (plaintextPrivateKey != null) CryptoUtils.wipe(plaintextPrivateKey); + } + } + + /** + * Decrypts an account's private key (seed) and derives the mnemonic. + */ + public static byte[][] deriveMnemonic( + EncryptedEnvelope envelope, + SecretKey masterKey + ) throws Exception { + + byte[] plaintextDek = null; + byte[] plaintextPrivateKey = null; + + try { + plaintextDek = Vault.decrypt( + masterKey, + envelope.getDekIv(), + envelope.getEncryptedDek() + ); + SecretKey dek = new SecretKeySpec(plaintextDek, "AES"); + + plaintextPrivateKey = Vault.decrypt( + dek, + envelope.getDataIv(), + envelope.getEncryptedData() + ); + + return Mnemonic.encode(plaintextPrivateKey); + + } finally { + if (plaintextDek != null) CryptoUtils.wipe(plaintextDek); + if (plaintextPrivateKey != null) CryptoUtils.wipe(plaintextPrivateKey); } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt b/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt index e411350..96c5c89 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt @@ -2,8 +2,6 @@ package swiss.qpq.gajumobile.security import android.content.Context import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.KeyGenerator diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/KeyManager.kt b/app/src/main/java/swiss/qpq/gajumobile/security/KeyManager.kt index 0ffe9e4..1b4eafa 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/KeyManager.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/security/KeyManager.kt @@ -16,10 +16,6 @@ object KeyManager { fun getMasterKey(context: Context): SecretKey { val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } - if (keyStore.containsAlias(MASTER_KEY_ALIAS)) { - keyStore.deleteEntry(MASTER_KEY_ALIAS) - } - return if (keyStore.containsAlias(MASTER_KEY_ALIAS)) { keyStore.getKey(MASTER_KEY_ALIAS, null) as SecretKey } else { @@ -27,17 +23,6 @@ object KeyManager { } } - /** - * Forcefully deletes the master key. - * Use this if you hit "Caller-provided IV" errors during development. - */ - fun resetMasterKey() { - val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } - if (keyStore.containsAlias(MASTER_KEY_ALIAS)) { - keyStore.deleteEntry(MASTER_KEY_ALIAS) - } - } - private fun generateMasterKey(context: Context): SecretKey { val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, @@ -51,7 +36,9 @@ object KeyManager { .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) - .setRandomizedEncryptionRequired(true) // Stronger: hardware module generates high-entropy IV + .setRandomizedEncryptionRequired(true) + .setUserAuthenticationRequired(true) + .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL) // Target StrongBox if available (Android 9+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt index 0479ee6..3d39876 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt @@ -23,7 +23,11 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults @@ -68,13 +72,92 @@ fun GajuButton( ), ) { Text( - text = text.uppercase(), + text = text, fontSize = 18.sp, fontWeight = FontWeight.Bold, ) } } +@Composable +fun GajuTopBar( + onNavigate: (String) -> Unit +) { + var menuExpanded by remember { mutableStateOf(value = false) } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Left Placeholder (to balance the right menu icon) + Spacer(modifier = Modifier.width(48.dp)) + + // Branded Logo (Center) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = R.drawable.gajumobile_icon_colored), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "GajuMobile", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + ) + } + + // Overflow Menu (Right) + Box { + IconButton(onClick = { menuExpanded = true }) { + Icon( + painter = painterResource(id = R.drawable.ic_more_vert), + contentDescription = "Menu", + tint = MaterialTheme.colorScheme.onBackground + ) + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + DropdownMenuItem( + text = { Text("Transactions") }, + onClick = { + menuExpanded = false + onNavigate("transactions") + }, + leadingIcon = { Icon(painterResource(R.drawable.gajumobile_icon), null, modifier = Modifier.size(18.dp), tint = Color.Unspecified) } + ) + DropdownMenuItem( + text = { Text("Settings") }, + onClick = { + menuExpanded = false + onNavigate("settings") + }, + leadingIcon = { Icon(painterResource(R.drawable.settings), null, modifier = Modifier.size(18.dp)) } + ) + DropdownMenuItem( + text = { Text("Environment Info") }, + onClick = { + menuExpanded = false + onNavigate("environment_info") + }, + leadingIcon = { Icon(painterResource(R.drawable.ic_lock), null, modifier = Modifier.size(18.dp)) } + ) + } + } + } +} + @Composable fun GajuHeader() { Row( @@ -100,80 +183,6 @@ fun GajuHeader() { } } -@Composable -fun GajuBottomNavigation( - currentDestination: String, - onNavigate: (String) -> Unit, -) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 8.dp, - ) { - Row( - modifier = Modifier - .windowInsetsPadding(WindowInsets.navigationBars) - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.gajumobile_icon_colored), - contentDescription = "Home", - modifier = Modifier - .size(40.dp) - .clickable { onNavigate("dashboard") }, - tint = Color.Unspecified, - ) - - NavigationItem( - label = "SETTINGS", - icon = R.drawable.settings, - selected = currentDestination == "settings", - ) { onNavigate("settings") } - - NavigationItem( - label = "MANAGE", - icon = R.drawable.ic_account_box, - selected = currentDestination == "manage", - ) { onNavigate("manage") } - - NavigationItem( - label = "TRANSACTIONS", - icon = R.drawable.ic_favorite, - selected = currentDestination == "transactions", - ) { onNavigate("transactions") } - } - } -} - -@Composable -private fun NavigationItem( - label: String, - icon: Int, - selected: Boolean, - onClick: () -> Unit, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.clickable { onClick() }, - ) { - Icon( - painter = painterResource(id = icon), - contentDescription = label, - modifier = Modifier.size(24.dp), - tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = label, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} - @Composable fun ActionCircle( text: String, @@ -190,7 +199,7 @@ fun ActionCircle( contentAlignment = Alignment.Center, ) { Text( - text = text.uppercase(), + text = text, color = contentColor, fontWeight = FontWeight.Bold, ) @@ -221,7 +230,7 @@ fun MnemonicReveal( } else { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon( - painter = painterResource(id = R.drawable.ic_favorite), + painter = painterResource(id = R.drawable.ic_lock), contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ) @@ -246,7 +255,7 @@ fun GajuTextField( ) { Column(modifier = modifier) { Text( - text = label.uppercase(), + text = label, fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(bottom = 8.dp), @@ -270,50 +279,12 @@ fun GajuTextField( } } -@Composable -fun WarningBox( - title: String, - content: String, - modifier: Modifier = Modifier, - icon: Int = R.drawable.ic_favorite, // Default warning icon placeholder -) { - Surface( - modifier = modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(48.dp), - ) - Text( - text = title.uppercase(), - fontWeight = FontWeight.ExtraBold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = content, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, - modifier = Modifier.padding(vertical = 8.dp), - ) - } - } -} - @Composable fun SuccessLayout( title: String, content: @Composable ColumnScope.() -> Unit, onProceed: () -> Unit, - proceedText: String = "PROCEED TO DASHBOARD", + proceedText: String = "Proceed to Dashboard", ) { var checked by remember { mutableStateOf(value = false) } @@ -366,10 +337,8 @@ fun SuccessLayout( .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - GajuHeader() - Text( - text = title.uppercase(), + text = title, color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Bold, @@ -380,3 +349,129 @@ fun SuccessLayout( } } } + +@Composable +fun ManageAction(label: String, icon: Int, isDestructive: Boolean = false, onClick: () -> Unit = {}) { + val color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onClick() }.padding(vertical = 8.dp)) { + Icon( + painterResource(icon), + null, + tint = color, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = label, + color = color, + fontSize = 16.sp, + ) + } +} + +@Composable +fun SecurityDataSection( + title: String, + data: List> +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(bottom = 8.dp) + ) + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + data.forEachIndexed { index, pair -> + Column { + Text( + pair.first, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + Text( + pair.second, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (index < data.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ) + } + } + } + } + } +} + +@Composable +fun CommittedWordChip( + index: Int, + word: String, + onClick: () -> Unit +) { + Surface( + onClick = onClick, + color = MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(8.dp), + tonalElevation = 2.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "$index.", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.6f) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = word, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } +} + +@Composable +fun ScannerAction(label: String, icon: Int, onClick: () -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Surface( + modifier = Modifier.size(48.dp).clickable { onClick() }, + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Text( + text = label, + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(top = 8.dp), + ) + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt new file mode 100644 index 0000000..641bf31 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt @@ -0,0 +1,140 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField + +@Composable +fun AccountNamingScreen( + publicKey: String, + onAccountNamed: (String) -> Unit, + onBack: (() -> Unit)? = null +) { + val clipboardManager = LocalClipboardManager.current + var name by remember { mutableStateOf("") } + var idExpanded by remember { mutableStateOf(false) } + + val displayedId = remember(publicKey, idExpanded) { + if (idExpanded || publicKey.length <= 16) { + publicKey + } else { + val prefix = publicKey.take(8) + val suffix = publicKey.takeLast(8) + "$prefix...$suffix" + } + } + + Column( + modifier = Modifier.fillMaxSize().padding(24.dp).safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + GajuHeader() + Text( + "Name your account", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + "Account ID:", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface + ) + + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Surface( + modifier = Modifier + .clickable { idExpanded = !idExpanded }, + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.small + ) { + Text( + displayedId, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = { clipboardManager.setText(AnnotatedString(publicKey)) }, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_content_copy), + contentDescription = "Copy ID", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + GajuTextField( + value = name, + onValueChange = { name = it }, + label = "Account Name", + placeholder = "Optional (defaults to ID)" + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row(modifier = Modifier.fillMaxWidth()) { + if (onBack != null) { + GajuButton( + text = "Back", + onClick = onBack, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) + Spacer(modifier = Modifier.width(16.dp)) + } + GajuButton( + text = "Finish", + onClick = { onAccountNamed(name.ifBlank { publicKey }) }, + modifier = Modifier.weight(1f) + ) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt new file mode 100644 index 0000000..41a810d --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt @@ -0,0 +1,85 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField + +@Composable +fun CreateWalletScreen( + onWalletCreated: (String) -> Unit, + onBack: (() -> Unit)? = null +) { + var walletName by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(24.dp) + .safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + GajuHeader() + Spacer(modifier = Modifier.height(32.dp)) + Text( + "Create Wallet", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + "A wallet is a collection of accounts, like a folder for your identities.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(32.dp)) + GajuTextField( + value = walletName, + onValueChange = { walletName = it }, + label = "Wallet Name", + placeholder = "e.g. Personal, Savings" + ) + Spacer(modifier = Modifier.weight(1f)) + + Row(modifier = Modifier.fillMaxWidth()) { + if (onBack != null) { + GajuButton( + text = "Back", + onClick = onBack, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) + Spacer(modifier = Modifier.width(16.dp)) + } + GajuButton( + text = "Continue", + enabled = walletName.isNotBlank(), + onClick = { onWalletCreated(walletName) }, + modifier = Modifier.weight(1f) + ) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt index 527537b..0ddcfed 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt @@ -12,13 +12,19 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SecondaryTabRow +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Surface import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -28,26 +34,48 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.data.models.Wallet +import swiss.qpq.gajumaru.core.formatting.GajuFormat import swiss.qpq.gajumobile.ui.components.ActionCircle -import swiss.qpq.gajumobile.ui.components.GajuBottomNavigation -import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTopBar @Composable fun DashboardScreen( + wallets: List, + balanceFormat: GajuFormat.Type = GajuFormat.Type.US, + balanceUnit: GajuFormat.Unit = GajuFormat.Unit.GAJU, onSend: () -> Unit, onReceive: () -> Unit, + onAddWallet: () -> Unit, + onAddAccount: (String) -> Unit, + onAccountSettings: (String, String) -> Unit, onNavigate: (String) -> Unit, ) { - var selectedTab by remember { mutableIntStateOf(0) } - val tabs = listOf("DAILY", "SHOPPING", "INCIDENTALS") + if (wallets.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("No wallets found. Please create one.") + } + return + } + + val clipboardManager = LocalClipboardManager.current + val walletPagerState = rememberPagerState(pageCount = { wallets.size }) + val currentWallet = wallets[walletPagerState.currentPage] + + var selectedAccountIndex by remember(walletPagerState.currentPage) { mutableIntStateOf(0) } + val currentAccount = currentWallet.accounts.getOrNull(selectedAccountIndex) Scaffold( - bottomBar = { GajuBottomNavigation("dashboard", onNavigate) }, + topBar = { GajuTopBar(onNavigate = onNavigate) } ) { innerPadding -> Column( modifier = Modifier @@ -56,44 +84,161 @@ fun DashboardScreen( .padding(innerPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { - GajuHeader() - - Text( - text = "EXPENSES", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(top = 16.dp), - ) - - SecondaryTabRow( - selectedTabIndex = selectedTab, - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground, - divider = {}, - indicator = { - TabRowDefaults.SecondaryIndicator( - Modifier.tabIndicatorOffset(selectedTab), - color = MaterialTheme.colorScheme.primary, - ) - }, - modifier = Modifier.padding(vertical = 16.dp), + // Wallet Area + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - tabs.forEachIndexed { index, title -> - Tab( - selected = selectedTab == index, - onClick = { selectedTab = index }, - text = { - Text( - title, - color = if (selectedTab == index) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - ) - } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "WALLET", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary ) + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = onAddWallet, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add), + contentDescription = "Add Wallet", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + + HorizontalPager( + state = walletPagerState, + modifier = Modifier.fillMaxWidth().height(48.dp), + verticalAlignment = Alignment.CenterVertically + ) { page -> + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = wallets[page].name, + color = MaterialTheme.colorScheme.onBackground, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + } + + // Pager Indicators + Row( + Modifier.height(8.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + repeat(wallets.size) { iteration -> + val color = if (walletPagerState.currentPage == iteration) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant + Box( + modifier = Modifier + .padding(2.dp) + .background(color, androidx.compose.foundation.shape.CircleShape) + .size(6.dp) + ) + } } } + // Account Tabs - Inset below wallet + if (currentWallet.accounts.isNotEmpty()) { + Surface( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + shape = RoundedCornerShape(12.dp) + ) { + SecondaryScrollableTabRow( + selectedTabIndex = selectedAccountIndex, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + divider = {}, + edgePadding = 12.dp, + indicator = { + TabRowDefaults.SecondaryIndicator( + modifier = Modifier.tabIndicatorOffset(selectedAccountIndex), + color = MaterialTheme.colorScheme.primary, + ) + } + ) { + currentWallet.accounts.forEachIndexed { index, account -> + val isSelected = selectedAccountIndex == index + Tab( + selected = isSelected, + onClick = { selectedAccountIndex = index }, + text = { + Text( + text = account.label, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 100.dp) + ) + } + ) + } + + // Trailing "+" Tab to Add Account + Tab( + selected = false, + onClick = { onAddAccount(currentWallet.id) }, + text = { + Icon( + painter = painterResource(id = R.drawable.ic_add), + contentDescription = "Add Account", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + ) + } + } + } + + // Account ID Display with Copy Widget + currentAccount?.let { account -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = account.getShortId(), + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = { clipboardManager.setText(AnnotatedString(account.gajuId)) }, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_content_copy), + contentDescription = "Copy ID", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + } + + // Balance Box Box( modifier = Modifier .fillMaxWidth() @@ -108,42 +253,37 @@ fun DashboardScreen( verticalAlignment = Alignment.CenterVertically, ) { Text("BALANCE", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - Icon( - painter = painterResource(id = R.drawable.ic_favorite), // Replace with eye icon - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp), - ) + IconButton( + onClick = { + currentAccount?.let { onAccountSettings(currentWallet.id, it.id) } + }, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.settings), + contentDescription = "Account Settings", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } } Spacer(modifier = Modifier.height(16.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text("木", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 24.sp) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "128,990,422", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - ) - Text( - text = ".129,853", - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - fontSize = 18.sp, - ) - } - - Row(modifier = Modifier.padding(start = 32.dp)) { - Text("Gajus", color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 10.sp) - Spacer(modifier = Modifier.width(16.dp)) - Text("Pucks", color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 10.sp) - } + val balanceStr = currentAccount?.formattedBalance(balanceFormat, balanceUnit) ?: "0" + + Text( + text = balanceStr, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + ) } } Spacer(modifier = Modifier.weight(1f)) + // Send / Receive Buttons Row( modifier = Modifier .fillMaxWidth() @@ -151,13 +291,13 @@ fun DashboardScreen( horizontalArrangement = Arrangement.SpaceEvenly, ) { ActionCircle( - text = "SEND", + text = "Send", containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary, onClick = onSend, ) ActionCircle( - text = "RECEIVE", + text = "Receive", containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, onClick = onReceive, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt new file mode 100644 index 0000000..def179d --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DeleteConfirmationScreen.kt @@ -0,0 +1,148 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader + +@Composable +fun DeleteConfirmationScreen( + type: String, // "WALLET" or "ACCOUNT" + name: String, + onConfirm: () -> Unit, + onCancel: () -> Unit, +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + ) { + GajuButton( + text = "CANCEL", + onClick = onCancel, + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ) + Spacer(modifier = Modifier.height(16.dp)) + GajuButton( + text = "CONFIRM\nDELETE", + onClick = onConfirm, + modifier = Modifier.fillMaxWidth(), + containerColor = Color(0xFFF44336), + ) + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Text( + text = "DELETE $type", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 24.dp), + ) + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Text( + text = name, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text("Accounts within this wallet", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + // TODO: Use real account/balance if possible + Text("Daily account", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 16.dp)) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = "WARNING: Deleting a $type will delete all accounts within it.", + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + textAlign = TextAlign.Center, + ) + + Text( + text = """ + To recover a deleted account, you will need your mnemonic phrase. Lost mnemonic phrases cannot be retrieved. + + All funds within a lost account are also lost. + + Back up your mnemonic phrases for every account before making any changes here to be safe. + + This action cannot be undone. + """.trimIndent(), + color = MaterialTheme.colorScheme.onBackground, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 16.dp), + ) + + var checked by remember { mutableStateOf(value = false) } + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { checked = !checked }) { + Checkbox(checked = checked, onCheckedChange = { checked = it }) + Text( + "I understand and assume all risks from this action. I would like to proceed.", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt new file mode 100644 index 0000000..9a7475c --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt @@ -0,0 +1,99 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.SecurityDataSection + +@Composable +fun EnvironmentInfoScreen( + onBack: () -> Unit +) { + val context = androidx.compose.ui.platform.LocalContext.current + val hasStrongBox = remember { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + context.packageManager.hasSystemFeature(android.content.pm.PackageManager.FEATURE_STRONGBOX_KEYSTORE) + } else false + } + + val appVersion = remember { + try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + "${pInfo.versionName} (${androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(pInfo)})" + } catch (_: Exception) { + "Unknown" + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + .safeDrawingPadding() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + GajuHeader() + Text( + "Environment Info", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(24.dp)) + + SecurityDataSection( + title = "Security Hardware", + data = listOf( + "Keystore" to if (hasStrongBox) "StrongBox (Secure Element)" else "Hardware TEE", + "Auth Enforcement" to "OS Lock Required", + "Encryption" to "Hardware-Generated IVs" + ) + ) + + SecurityDataSection( + title = "Device Identity", + data = listOf( + "Manufacturer" to android.os.Build.MANUFACTURER, + "Model" to android.os.Build.MODEL, + "Board" to android.os.Build.BOARD, + "Hardware" to android.os.Build.HARDWARE + ) + ) + + SecurityDataSection( + title = "System Build", + data = listOf( + "Android Version" to "${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})", + "Security Patch" to android.os.Build.VERSION.SECURITY_PATCH, + "Fingerprint" to android.os.Build.FINGERPRINT + ) + ) + + SecurityDataSection( + title = "GajuMobile", + data = listOf( + "App Version" to appVersion, + "Network" to "Mainnet (Production)" + ) + ) + + Spacer(modifier = Modifier.weight(1f)) + + GajuButton(text = "Done", onClick = onBack) + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt new file mode 100644 index 0000000..0fe51f0 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt @@ -0,0 +1,61 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.ui.components.GajuTopBar + +@Composable +fun ExplorerScreen( + onNavigate: (String) -> Unit +) { + Scaffold( + topBar = { GajuTopBar(onNavigate = onNavigate) }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Transactions", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Text("Loading Groot", color = MaterialTheme.colorScheme.primary, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier.fillMaxWidth().weight(1f).padding(horizontal = 24.dp, vertical = 8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(4.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Text("Gaju Explorer Content", color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt index 26e0a27..39f257a 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/LockScreen.kt @@ -38,9 +38,9 @@ fun LockScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Image( - painter = painterResource(id = R.drawable.gajumobile_logo_colored), + painter = painterResource(id = R.drawable.gajumobile_icon_colored), contentDescription = null, - modifier = Modifier.size(150.dp), + modifier = Modifier.size(100.dp), ) Spacer(modifier = Modifier.height(48.dp)) Text( diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt new file mode 100644 index 0000000..2504ccd --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageAccountScreen.kt @@ -0,0 +1,209 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.data.models.Account +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField +import swiss.qpq.gajumobile.ui.components.ManageAction +import swiss.qpq.gajumaru.core.formatting.GajuFormat + +@Composable +fun ManageAccountScreen( + account: Account, + walletName: String, + balanceFormat: GajuFormat.Type, + balanceUnit: GajuFormat.Unit, + isUnverified: Boolean, + onViewMnemonic: () -> Unit, + onVerifyMnemonic: () -> Unit, + onRename: (String) -> Unit, + onDelete: () -> Unit +) { + val clipboardManager = LocalClipboardManager.current + var showRenameDialog by remember { mutableStateOf(false) } + var newName by remember(account.label) { mutableStateOf(account.label) } + + if (showRenameDialog) { + androidx.compose.ui.window.Dialog(onDismissRequest = { showRenameDialog = false }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text("Rename Account", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(16.dp)) + GajuTextField( + value = newName, + onValueChange = { newName = it }, + label = "New Name" + ) + Spacer(modifier = Modifier.height(24.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = { showRenameDialog = false }) { Text("CANCEL") } + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + onRename(newName) + showRenameDialog = false + }) { Text("SAVE") } + } + } + } + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (isUnverified) { + ManageAction(label = "Verify mnemonic backup", icon = R.drawable.ic_check_circle, onClick = onVerifyMnemonic) + Spacer(modifier = Modifier.height(8.dp)) + } + ManageAction(label = "View mnemonic phrase", icon = R.drawable.ic_visibility, onClick = onViewMnemonic) + Spacer(modifier = Modifier.height(8.dp)) + ManageAction(label = "Delete this account", icon = R.drawable.ic_delete, isDestructive = true, onClick = onDelete) + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Text( + text = "Manage Account", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Text( + text = walletName, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Surface( + modifier = Modifier + .widthIn(max = 300.dp) + .clickable { showRenameDialog = true }, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(16.dp) + ) { + Text( + text = account.label, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + + Text( + text = account.formattedBalance(balanceFormat, balanceUnit), + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(top = 8.dp), + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Account ID:", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(top = 4.dp) + ) { + Text( + text = account.gajuId, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f, fill = false) + ) + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = { clipboardManager.setText(AnnotatedString(account.gajuId)) }, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_content_copy), + contentDescription = "Copy ID", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + + Spacer(modifier = Modifier.height(48.dp)) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletScreen.kt new file mode 100644 index 0000000..2e79fce --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletScreen.kt @@ -0,0 +1,146 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField +import swiss.qpq.gajumobile.ui.components.ManageAction + +@Composable +fun ManageWalletScreen( + walletName: String, + onRename: (String) -> Unit, + onDelete: () -> Unit +) { + var showRenameDialog by remember { mutableStateOf(false) } + var newName by remember { mutableStateOf(walletName) } + + if (showRenameDialog) { + androidx.compose.ui.window.Dialog(onDismissRequest = { showRenameDialog = false }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text("Rename Wallet", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(16.dp)) + GajuTextField( + value = newName, + onValueChange = { newName = it }, + label = "New Name" + ) + Spacer(modifier = Modifier.height(24.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = { showRenameDialog = false }) { Text("CANCEL") } + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + onRename(newName) + showRenameDialog = false + }) { Text("SAVE") } + } + } + } + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ManageAction(label = "Delete this wallet", icon = R.drawable.ic_delete, isDestructive = true, onClick = onDelete) + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Text( + text = "Manage Wallet", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(32.dp)) + Text(text = walletName, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold) + Icon(painterResource(R.drawable.settings), null, tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text("Accounts within this wallet", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + // Real wallets will be iterated in Overview, here we just show a placeholder or we should pass account list + Text("Select an account from the MANAGE screen to configure individual account settings.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 16.dp) + ) + + Spacer(modifier = Modifier.height(48.dp)) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + ManageAction(label = "Rename this wallet", icon = R.drawable.ic_edit, onClick = { showRenameDialog = true }) + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManagementScreens.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManagementScreens.kt deleted file mode 100644 index e2164bf..0000000 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManagementScreens.kt +++ /dev/null @@ -1,602 +0,0 @@ -package swiss.qpq.gajumobile.ui.screens - -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.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Checkbox -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import swiss.qpq.gajumobile.R -import swiss.qpq.gajumobile.ui.components.GajuBottomNavigation -import swiss.qpq.gajumobile.ui.components.GajuButton -import swiss.qpq.gajumobile.ui.components.GajuHeader - -@Composable -fun SettingsScreen( - onNavigate: (String) -> Unit, - onBack: () -> Unit, -) { - var biometricsEnabled by remember { mutableStateOf(value = true) } - - Scaffold( - bottomBar = { GajuBottomNavigation("settings", onNavigate) }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "SETTINGS", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Icon( - painter = painterResource(id = R.drawable.settings), - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Text( - text = "APP SECURITY", - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp), - ) - } - - Spacer(modifier = Modifier.height(24.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "ENABLE BIOMETRICS LOGIN", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 14.sp, - ) - Switch( - checked = biometricsEnabled, - onCheckedChange = { biometricsEnabled = it }, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.onPrimary, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - checkedTrackColor = MaterialTheme.colorScheme.primary, - uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, - ), - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - IconButton(onClick = onBack, modifier = Modifier.align(Alignment.Start)) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } -} - -@Composable -fun ManageOverviewScreen( - onWalletDetail: (String) -> Unit, - onAccountDetail: (String) -> Unit, - onNavigate: (String) -> Unit, -) { - Scaffold( - bottomBar = { GajuBottomNavigation("manage", onNavigate) }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "MANAGE", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Text( - text = "Every wallet can have multiple accounts.\nEvery account within a wallet has its own mnemonic phrase.\nMake sure you save the mnemonics for each account. Lost mnemonics cannot be retrieved.", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - textAlign = TextAlign.Center, - modifier = Modifier.padding(bottom = 32.dp), - ) - - WalletCard( - name = "PRIMARY WALLET", - accounts = listOf("DAILY", "SHOPPING", "INCIDENTALS"), - onWalletClick = { onWalletDetail("Primary") }, - onAccountClick = onAccountDetail, - ) - - Spacer(modifier = Modifier.height(24.dp)) - - WalletCard( - name = "SAVINGS WALLET", - accounts = listOf("EMERGENCY"), - onWalletClick = { onWalletDetail("Savings") }, - onAccountClick = onAccountDetail, - ) - - Spacer(modifier = Modifier.height(32.dp)) - - FloatingActionButton( - onClick = { /* TODO */ }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - shape = CircleShape, - ) { - Icon(painterResource(R.drawable.ic_favorite), contentDescription = "Add Wallet") - } - Text("Add a new wallet", color = MaterialTheme.colorScheme.onBackground, fontSize = 12.sp, modifier = Modifier.padding(top = 8.dp)) - } - } -} - -@Composable -private fun WalletCard( - name: String, - accounts: List, - onWalletClick: () -> Unit, - onAccountClick: (String) -> Unit, -) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Surface( - modifier = Modifier.fillMaxWidth().clickable { onWalletClick() }, - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Row( - modifier = Modifier.padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.width(32.dp)) - Text(text = name, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold) - Icon(painterResource(R.drawable.settings), null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp)) - } - } - - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 16.dp)) { - Text("Accounts within this wallet:", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - } - - accounts.forEach { account -> - Surface( - modifier = Modifier.padding(top = 8.dp).width(200.dp).clickable { onAccountClick(account) }, - color = MaterialTheme.colorScheme.secondaryContainer, - shape = RoundedCornerShape(8.dp), - ) { - Row( - modifier = Modifier.padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = account, color = MaterialTheme.colorScheme.onSecondaryContainer, fontSize = 14.sp) - Icon(painterResource(R.drawable.settings), null, tint = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.size(16.dp)) - } - } - } - - Row( - modifier = Modifier.padding(top = 16.dp).clickable { /* TODO */ }, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(painterResource(R.drawable.ic_favorite), null, tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.size(20.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text("Add a new account within this wallet", color = MaterialTheme.colorScheme.onSurface, fontSize = 12.sp) - } - } -} - -@Composable -fun ExplorerScreen( - onNavigate: (String) -> Unit, - onBack: () -> Unit, -) { - Scaffold( - bottomBar = { GajuBottomNavigation("transactions", onNavigate) }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "TRANSACTIONS", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Text("Loading Groot", color = MaterialTheme.colorScheme.primary, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) - - Spacer(modifier = Modifier.height(16.dp)) - - Surface( - modifier = Modifier.fillMaxWidth().weight(1f).padding(horizontal = 24.dp, vertical = 8.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(4.dp), - ) { - Box(contentAlignment = Alignment.Center) { - Text("Gaju Explorer Content", color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - - IconButton(onClick = onBack, modifier = Modifier.align(Alignment.Start).padding(16.dp)) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } -} - -@Composable -fun ManageWalletScreen( - walletName: String, - onDelete: () -> Unit, - onBack: () -> Unit, -) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - ManageAction(label = "Delete this wallet", icon = R.drawable.ic_favorite, isDestructive = true, onClick = onDelete) - - Spacer(modifier = Modifier.height(16.dp)) - - IconButton(onClick = onBack, modifier = Modifier.align(Alignment.Start)) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "MANAGE WALLET", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Row( - modifier = Modifier.padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.width(32.dp)) - Text(text = walletName.uppercase(), color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold) - Icon(painterResource(R.drawable.settings), null, tint = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - - Spacer(modifier = Modifier.height(32.dp)) - - Text("Accounts within this wallet", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - Text("DAILY 木 128,990,422.129,853", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 16.dp)) - Text("SHOPPING 木 65,829,121.9,803", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 8.dp)) - Text("INCIDENTALS 木 45,001,442.223,000,034", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 8.dp)) - - Spacer(modifier = Modifier.height(32.dp)) - - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { - ManageAction(label = "Rename this wallet", icon = R.drawable.ic_favorite) - ManageAction(label = "Add a new account", icon = R.drawable.ic_favorite) - } - } - } -} - -@Composable -fun ManageAccountScreen( - accountName: String, - walletName: String, - onDelete: () -> Unit, - onBack: () -> Unit, -) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - ManageAction(label = "Delete this account", icon = R.drawable.ic_favorite, isDestructive = true, onClick = onDelete) - - Spacer(modifier = Modifier.height(16.dp)) - - IconButton(onClick = onBack, modifier = Modifier.align(Alignment.Start)) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "MANAGE ACCOUNT", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline), - ) { - Text( - text = walletName.uppercase(), - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(16.dp), - textAlign = TextAlign.Center, - ) - } - - Spacer(modifier = Modifier.height(32.dp)) - - GajuButton( - text = accountName, - onClick = {}, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.width(200.dp), - ) - Text("木 128,990,422.129,853", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 8.dp)) - - Spacer(modifier = Modifier.height(64.dp)) - - ManageAction(label = "Rename this account", icon = R.drawable.ic_favorite) - } - } -} - -@Composable -fun DeleteConfirmationScreen( - type: String, // "WALLET" or "ACCOUNT" - name: String, - onConfirm: () -> Unit, - onCancel: () -> Unit, -) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - GajuButton( - text = "CANCEL", - onClick = onCancel, - modifier = Modifier.fillMaxWidth(), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.height(16.dp)) - GajuButton( - text = "CONFIRM\nDELETE", - onClick = onConfirm, - modifier = Modifier.fillMaxWidth(), - containerColor = Color(0xFFF44336), - ) - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "DELETE $type", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 24.dp), - ) - - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Text( - text = name.uppercase(), - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(16.dp), - textAlign = TextAlign.Center, - ) - } - - Spacer(modifier = Modifier.height(32.dp)) - - Text("Accounts within this wallet", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - Text("DAILY 木 128,990,422.129,853", color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(top = 16.dp)) - - Spacer(modifier = Modifier.height(32.dp)) - - Text( - text = "WARNING: Deleting a $type will delete all accounts within it.", - color = MaterialTheme.colorScheme.error, - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - textAlign = TextAlign.Center, - ) - - Text( - text = """ - To recover a deleted account, you will need your mnemonic phrase. Lost mnemonic phrases cannot be retrieved. - - All funds within a lost account are also lost. - - Back up your mnemonic phrases for every account before making any changes here to be safe. - - This action cannot be undone. - """.trimIndent(), - color = MaterialTheme.colorScheme.onBackground, - fontSize = 12.sp, - textAlign = TextAlign.Center, - modifier = Modifier.padding(vertical = 16.dp), - ) - - var checked by remember { mutableStateOf(value = false) } - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { checked = !checked }) { - Checkbox(checked = checked, onCheckedChange = { checked = it }) - Text( - "I understand and assume all risks from this action. I would like to proceed.", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onBackground, - ) - } - } - } -} - -@Composable -private fun ManageAction(label: String, icon: Int, isDestructive: Boolean = false, onClick: () -> Unit = {}) { - val color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onClick() }) { - Icon( - painterResource(icon), - null, - tint = color, - modifier = Modifier.size(20.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = label, - color = color, - fontSize = 12.sp, - ) - } -} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt new file mode 100644 index 0000000..f779f64 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicRecoveryScreen.kt @@ -0,0 +1,186 @@ +package swiss.qpq.gajumobile.ui.screens + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.security.AccountAirlock +import swiss.qpq.gajumobile.ui.components.CommittedWordChip +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MnemonicRecoveryScreen( + onMnemonicConfirmed: (Array) -> Unit, + onBack: () -> Unit +) { + var committedWords by remember { mutableStateOf(listOf()) } + var currentDraft by remember { mutableStateOf("") } + var isAddingWord by remember { mutableStateOf(true) } + + val allWords = remember { AccountAirlock.getWordList().toList() } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(isAddingWord) { + if (isAddingWord) focusRequester.requestFocus() + } + + val suggestions = if (currentDraft.length >= 2) { + allWords.filter { it.startsWith(currentDraft) }.take(5) + } else emptyList() + + Scaffold( + modifier = Modifier.fillMaxSize().imePadding(), + bottomBar = { + Column { + if (suggestions.isNotEmpty() && isAddingWord) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = 8.dp + ) { + Row( + modifier = Modifier.padding(8.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + suggestions.forEach { suggestion -> + SuggestionChip( + onClick = { + committedWords = committedWords + suggestion + currentDraft = "" + isAddingWord = false + }, + label = { Text(suggestion) } + ) + } + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + GajuButton( + text = "BACK", + onClick = onBack, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) + Spacer(modifier = Modifier.width(16.dp)) + GajuButton( + text = "RECOVER", + enabled = committedWords.size >= 21 && !isAddingWord, + onClick = { + val phrase = committedWords.map { it.toByteArray(Charsets.UTF_8) }.toTypedArray() + onMnemonicConfirmed(phrase) + }, + modifier = Modifier.weight(1f) + ) + } + } + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()) + ) { + GajuHeader() + Text( + "RECOVER ACCOUNT", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(16.dp)) + + Text( + "Mnemonic Phrase:", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(12.dp)) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + committedWords.forEachIndexed { index, word -> + CommittedWordChip( + index = index + 1, + word = word, + onClick = { + currentDraft = word + committedWords = committedWords.take(index) + isAddingWord = true + } + ) + } + + if (isAddingWord && committedWords.size < 23) { + Box(modifier = Modifier.width(120.dp)) { + GajuTextField( + value = currentDraft, + onValueChange = { currentDraft = it.lowercase() }, + label = "${committedWords.size + 1}", + placeholder = "...", + modifier = Modifier.focusRequester(focusRequester) + ) + } + } else if (committedWords.size < 23) { + AssistChip( + onClick = { isAddingWord = true }, + label = { Text("+ NEXT WORD") }, + colors = AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + labelColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt new file mode 100644 index 0000000..dafa28a --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/MnemonicVerifyScreen.kt @@ -0,0 +1,209 @@ +package swiss.qpq.gajumobile.ui.screens + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.security.AccountAirlock +import swiss.qpq.gajumobile.ui.components.CommittedWordChip +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MnemonicVerifyScreen( + label: String, + expectedPhrase: Array, + onVerified: () -> Unit, + onBack: () -> Unit +) { + var committedWords by remember { mutableStateOf(listOf()) } + var currentDraft by remember { mutableStateOf("") } + var isAddingWord by remember { mutableStateOf(true) } + var verificationError by remember { mutableStateOf(false) } + + val allWords = remember { AccountAirlock.getWordList().toList() } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(isAddingWord) { + if (isAddingWord) focusRequester.requestFocus() + } + + val suggestions = if (currentDraft.length >= 2) { + allWords.filter { it.startsWith(currentDraft) }.take(5) + } else emptyList() + + Scaffold( + modifier = Modifier.fillMaxSize().imePadding(), + bottomBar = { + Column { + if (suggestions.isNotEmpty() && isAddingWord) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = 8.dp + ) { + Row( + modifier = Modifier.padding(8.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + suggestions.forEach { suggestion -> + SuggestionChip( + onClick = { + committedWords = committedWords + suggestion + currentDraft = "" + isAddingWord = false + verificationError = false + }, + label = { Text(suggestion) } + ) + } + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + GajuButton( + text = "BACK", + onClick = onBack, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) + Spacer(modifier = Modifier.width(16.dp)) + GajuButton( + text = "VERIFY", + enabled = committedWords.size == expectedPhrase.size && !isAddingWord, + onClick = { + val inputPhrase = committedWords.map { it.toByteArray(Charsets.UTF_8) } + var match = true + for (i in inputPhrase.indices) { + if (!inputPhrase[i].contentEquals(expectedPhrase[i])) { + match = false + break + } + } + + if (match) { + onVerified() + } else { + verificationError = true + } + }, + modifier = Modifier.weight(1f) + ) + } + } + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()) + ) { + GajuHeader() + Text( + "VERIFY BACKUP", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + label.uppercase(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(16.dp)) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + committedWords.forEachIndexed { index, word -> + CommittedWordChip( + index = index + 1, + word = word, + onClick = { + currentDraft = word + committedWords = committedWords.take(index) + isAddingWord = true + verificationError = false + } + ) + } + + if (isAddingWord && committedWords.size < expectedPhrase.size) { + Box(modifier = Modifier.width(120.dp)) { + GajuTextField( + value = currentDraft, + onValueChange = { currentDraft = it.lowercase() }, + label = "${committedWords.size + 1}", + placeholder = "...", + modifier = Modifier.focusRequester(focusRequester) + ) + } + } else if (committedWords.size < expectedPhrase.size) { + AssistChip( + onClick = { isAddingWord = true }, + label = { Text("+ NEXT WORD") }, + colors = AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + labelColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + } + + if (verificationError) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + "Mnemonic phrase does not match. Please check again.", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/OnboardingFlow.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/OnboardingFlow.kt deleted file mode 100644 index 9abfb52..0000000 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/OnboardingFlow.kt +++ /dev/null @@ -1,299 +0,0 @@ -package swiss.qpq.gajumobile.ui.screens - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SuggestionChip -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import swiss.qpq.gajumobile.R -import swiss.qpq.gajumobile.ui.components.GajuButton -import swiss.qpq.gajumobile.ui.components.GajuTextField -import swiss.qpq.gajumobile.ui.components.GajuHeader -import swiss.qpq.gajumobile.security.AccountAirlock - -@Composable -fun CreateWalletScreen( - onWalletCreated: (String) -> Unit -) { - var walletName by remember { mutableStateOf("") } - - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(24.dp) - .safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - GajuHeader() - Spacer(modifier = Modifier.height(32.dp)) - Text( - "CREATE WALLET", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onBackground - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - "A wallet is a collection of accounts, like a folder for your identities.", - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(32.dp)) - GajuTextField( - value = walletName, - onValueChange = { walletName = it }, - label = "WALLET NAME", - placeholder = "e.g. Personal, Savings" - ) - Spacer(modifier = Modifier.weight(1f)) - GajuButton( - text = "CONTINUE", - enabled = walletName.isNotBlank(), - onClick = { onWalletCreated(walletName) } - ) - } -} - -@Composable -fun SetupChoiceScreen( - onCreate: () -> Unit, - onRecover: () -> Unit, -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(24.dp) - .safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Image( - painter = painterResource(id = R.drawable.gajumobile_icon_colored), - contentDescription = null, - modifier = Modifier.size(100.dp), // Slightly smaller to avoid cut-off in some layouts - ) - - Spacer(modifier = Modifier.height(48.dp)) - - GajuButton( - text = "CREATE ACCOUNT", - onClick = onCreate, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - GajuButton( - text = "RECOVER ACCOUNT", - onClick = onRecover, - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "To recover an existing account, you will need your mnemonic phrase.", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - textAlign = TextAlign.Center, - fontStyle = FontStyle.Italic, - ) - } -} - - -@Composable -fun MnemonicRecoveryScreen( - onMnemonicConfirmed: (Array) -> Unit, - onBack: () -> Unit -) { - var words by remember { mutableStateOf(listOf("")) } - val allWords = remember { AccountAirlock.getWordList().toList() } - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - GajuButton( - text = "BACK", - onClick = onBack, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "RECOVER", - enabled = words.size >= 21 && words.all { it.isNotBlank() }, - onClick = { - val phrase = words.map { it.toByteArray(Charsets.UTF_8) }.toTypedArray() - onMnemonicConfirmed(phrase) - }, - modifier = Modifier.weight(1f) - ) - } - } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 24.dp) - ) { - GajuHeader() - Text( - "RECOVER ACCOUNT", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onBackground - ) - Spacer(modifier = Modifier.height(16.dp)) - - LazyColumn(modifier = Modifier.weight(1f)) { - items(words.size) { index -> - WordInputField( - word = words[index], - onWordChange = { newWord -> - val newList = words.toMutableList() - newList[index] = newWord - words = newList - }, - suggestions = if (words[index].length >= 2) { - allWords.filter { it.startsWith(words[index]) }.take(5) - } else emptyList(), - onSuggestionSelected = { selected -> - val newList = words.toMutableList() - newList[index] = selected - if (index == words.size - 1 && words.size < 23) { - newList.add("") - } - words = newList - } - ) - } - item { - if (words.size < 23) { - TextButton(onClick = { words = words + "" }) { - Text("+ Add Word", color = MaterialTheme.colorScheme.primary) - } - } - } - } - } - } -} - -@Composable -fun WordInputField( - word: String, - onWordChange: (String) -> Unit, - suggestions: List, - onSuggestionSelected: (String) -> Unit -) { - Column { - GajuTextField( - value = word, - onValueChange = onWordChange, - label = "WORD", - placeholder = "Type word..." - ) - if (suggestions.isNotEmpty()) { - Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { - suggestions.forEach { suggestion -> - SuggestionChip( - onClick = { onSuggestionSelected(suggestion) }, - label = { Text(suggestion) }, - modifier = Modifier.padding(end = 4.dp) - ) - } - } - } - } -} - -@Composable -fun AccountNamingScreen( - publicKey: String, - onAccountNamed: (String) -> Unit -) { - var name by remember { mutableStateOf("") } - - Column( - modifier = Modifier.fillMaxSize().padding(24.dp).safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - GajuHeader() - Text( - "NAME YOUR ACCOUNT", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onBackground - ) - Spacer(modifier = Modifier.height(24.dp)) - - Text( - "Account ID:", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - publicKey, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp) - ) - - Spacer(modifier = Modifier.height(32.dp)) - - GajuTextField( - value = name, - onValueChange = { name = it }, - label = "ACCOUNT NAME", - placeholder = "Optional (defaults to ID)" - ) - - Spacer(modifier = Modifier.weight(1f)) - - GajuButton( - text = "FINISH", - onClick = { onAccountNamed(name.ifBlank { publicKey }) } - ) - } -} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt new file mode 100644 index 0000000..645e062 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt @@ -0,0 +1,98 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader + +@Composable +fun QRReceiveScreen( + onDone: () -> Unit +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(horizontal = 24.dp, vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuButton( + text = "DONE", + onClick = onDone, + modifier = Modifier.width(160.dp), + ) + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Spacer(modifier = Modifier.weight(1f)) + + // QR Code Placeholder + Surface( + modifier = Modifier.size(240.dp), + color = Color.White, // QR Codes are usually black on white for scanning + shape = RoundedCornerShape(16.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + painter = painterResource(id = R.drawable.ic_qr_code), + contentDescription = "QR Code", + modifier = Modifier.size(180.dp), + tint = Color.Black, + ) + } + } + + Text( + text = "Sender must scan this QR.", + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(top = 24.dp), + ) + Text( + text = "You can also send them a screenshot of this QR.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, + ) + + Spacer(modifier = Modifier.weight(1f)) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt new file mode 100644 index 0000000..6e0df88 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt @@ -0,0 +1,86 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.ScannerAction + +@Composable +fun QRScannerScreen( + onGenerate: () -> Unit, + onUpload: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Text( + text = "SCAN QR", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 20.sp, + fontWeight = FontWeight.Light, + modifier = Modifier.padding(top = 16.dp), + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Scanner Viewfinder Mockup + Box( + modifier = Modifier + .size(280.dp) + .border(2.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_qr_code), + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 48.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + ScannerAction(label = "Generate QR", icon = R.drawable.ic_qr_code, onClick = onGenerate) + ScannerAction(label = "Upload QR", icon = R.drawable.ic_qr_code, onClick = onUpload) + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt new file mode 100644 index 0000000..630c83f --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt @@ -0,0 +1,135 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField + +@Composable +fun SendFormScreen( + onReview: () -> Unit, + onBack: () -> Unit, +) { + var toAddress by remember { mutableStateOf(value = "") } + var amount by remember { mutableStateOf(value = "") } + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + ) { + GajuButton( + text = "BACK", + onClick = onBack, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ) + Spacer(modifier = Modifier.width(16.dp)) + GajuButton( + text = "REVIEW", + onClick = onReview, + modifier = Modifier.weight(1f), + ) + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Text( + text = "SEND", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Spacer(modifier = Modifier.width(8.dp)) + Text("Daily", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) + Icon(painterResource(R.drawable.gajumobile_icon_colored), null, tint = Color.Unspecified, modifier = Modifier.size(16.dp)) // Dropdown arrow replacement + } + Text( + text = "Primary Wallet", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + modifier = Modifier.align(Alignment.End), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + GajuTextField( + value = toAddress, + onValueChange = { toAddress = it }, + label = "SENDING TO", + placeholder = "ak_...", + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + GajuTextField( + value = amount, + onValueChange = { amount = it }, + label = "AMOUNT", + placeholder = "0.0", + modifier = Modifier.fillMaxWidth(), + trailingIcon = { Text("木", color = MaterialTheme.colorScheme.onSurface) }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text("CURRENCY", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Spacer(modifier = Modifier.width(8.dp)) + Text("木 Gajus", color = MaterialTheme.colorScheme.onBackground) + Icon(painterResource(R.drawable.gajumobile_icon_colored), null, tint = Color.Unspecified, modifier = Modifier.size(16.dp)) + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt new file mode 100644 index 0000000..d78784d --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt @@ -0,0 +1,140 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader + +@Composable +fun SendReviewScreen( + onSend: () -> Unit, + onBack: () -> Unit, +) { + var confirmed by remember { mutableStateOf(value = false) } + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(24.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { confirmed = !confirmed }.padding(bottom = 24.dp) + ) { + Checkbox(checked = confirmed, onCheckedChange = { confirmed = it }) + Text( + "I confirm that the above details are correct and I would like to proceed.", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onBackground, + ) + } + + Row(modifier = Modifier.fillMaxWidth()) { + GajuButton( + text = "BACK", + onClick = onBack, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ) + Spacer(modifier = Modifier.width(16.dp)) + GajuButton( + text = "SEND", + onClick = onSend, + modifier = Modifier.weight(1f), + containerColor = if (confirmed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + contentColor = if (confirmed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GajuHeader() + + Text( + text = "SEND", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) + Text("Daily Account", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) + Text("Primary Wallet", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 10.sp) + + Spacer(modifier = Modifier.height(16.dp)) + + Text("SENDING TO", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) + Text("Alice S....van", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) + Text( + text = "ak_9jzmTHpVCbXfMSfyvxGV1ZLbzR8YqdK4jpSL5WgUKLGM4XPVp", + fontSize = 10.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Text("You are sending a payment of", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("木 22,980.78991", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) + Text("to", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("ak_9jzm...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + + Spacer(modifier = Modifier.height(16.dp)) + Text("THIS ACTION CANNOT BE UNDONE.", fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("Are you sure you want to proceed?", fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt new file mode 100644 index 0000000..77d04b3 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SettingsScreen.kt @@ -0,0 +1,110 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuTopBar +import swiss.qpq.gajumaru.core.formatting.GajuFormat + +@Composable +fun SettingsScreen( + balanceFormat: GajuFormat.Type, + onFormatChange: (GajuFormat.Type) -> Unit, + onNavigate: (String) -> Unit +) { + Scaffold( + topBar = { GajuTopBar(onNavigate = onNavigate) }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Settings", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Icon( + painter = painterResource(id = R.drawable.settings), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + ) { + Text( + text = "DISPLAY PREFERENCES", + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "BALANCE FORMAT", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.align(Alignment.Start) + ) + + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + GajuFormat.Type.entries.filter { it != GajuFormat.Type.LEGACY }.forEach { type -> + val isSelected = type == balanceFormat + AssistChip( + onClick = { onFormatChange(type) }, + label = { Text(type.name) }, + colors = AssistChipDefaults.assistChipColors( + containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + labelColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt new file mode 100644 index 0000000..ae8247d --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SetupChoiceScreen.kt @@ -0,0 +1,83 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuButton + +@Composable +fun SetupChoiceScreen( + onCreate: () -> Unit, + onRecover: () -> Unit, + onBack: (() -> Unit)? = null +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(24.dp) + .safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.gajumobile_icon_colored), + contentDescription = null, + modifier = Modifier.size(100.dp), + ) + + Spacer(modifier = Modifier.height(48.dp)) + + GajuButton( + text = "Create Account", + onClick = onCreate, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + GajuButton( + text = "Recover Account", + onClick = onRecover, + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "To recover an existing account, you will need your mnemonic phrase.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + textAlign = TextAlign.Center, + fontStyle = FontStyle.Italic, + ) + + if (onBack != null) { + Spacer(modifier = Modifier.height(32.dp)) + TextButton(onClick = onBack) { + Text("Cancel") + } + } + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionScreens.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionScreens.kt deleted file mode 100644 index 5b006ef..0000000 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionScreens.kt +++ /dev/null @@ -1,451 +0,0 @@ -package swiss.qpq.gajumobile.ui.screens - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -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.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import swiss.qpq.gajumobile.R -import swiss.qpq.gajumobile.ui.components.GajuButton -import swiss.qpq.gajumobile.ui.components.GajuHeader -import swiss.qpq.gajumobile.ui.components.GajuTextField -import swiss.qpq.gajumobile.ui.components.SuccessLayout - -@Composable -fun QRScannerScreen( - onGenerate: () -> Unit, - onUpload: () -> Unit, - onBack: () -> Unit, -) { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .safeDrawingPadding(), - ) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "SCAN QR", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Light, - modifier = Modifier.padding(top = 16.dp), - ) - - Spacer(modifier = Modifier.weight(1f)) - - // Scanner Viewfinder Mockup - Box( - modifier = Modifier - .size(280.dp) - .border(2.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(16.dp)), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), // Replace with scan icon - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 48.dp), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - ScannerAction(label = "GENERATE QR", icon = R.drawable.ic_favorite, onClick = onGenerate) - ScannerAction(label = "UPLOAD QR", icon = R.drawable.ic_favorite, onClick = onUpload) - } - } - - // Back arrow button from mockup - IconButton( - onClick = onBack, - modifier = Modifier.align(Alignment.CenterStart).padding(start = 16.dp).size(48.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), // Replace with back arrow - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp), - ) - } - } -} - -@Composable -private fun ScannerAction(label: String, icon: Int, onClick: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Surface( - modifier = Modifier.size(48.dp).clickable { onClick() }, - shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - Text( - text = label, - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(top = 8.dp), - ) - } -} - -@Composable -fun QRReceiveScreen( - onDone: () -> Unit, - onBack: () -> Unit, -) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(horizontal = 24.dp, vertical = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuButton( - text = "DONE", - onClick = onDone, - modifier = Modifier.width(160.dp), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - IconButton(onClick = onBack, modifier = Modifier.align(Alignment.Start)) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Spacer(modifier = Modifier.weight(1f)) - - // QR Code Placeholder - Surface( - modifier = Modifier.size(240.dp), - color = Color.White, // QR Codes are usually black on white for scanning - shape = RoundedCornerShape(16.dp), - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "QR Code", - modifier = Modifier.size(180.dp), - tint = Color.Black, - ) - } - } - - Text( - text = "Sender must scan this QR.", - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(top = 24.dp), - ) - Text( - text = "You can also send them a screenshot of this QR.", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, - ) - - Spacer(modifier = Modifier.weight(1f)) - } - } -} - -@Composable -fun SendFormScreen( - onReview: () -> Unit, - onBack: () -> Unit, -) { - var toAddress by remember { mutableStateOf(value = "") } - var amount by remember { mutableStateOf(value = "") } - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - GajuButton( - text = "BACK", - onClick = onBack, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "REVIEW", - onClick = onReview, - modifier = Modifier.weight(1f), - ) - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "SEND", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - Spacer(modifier = Modifier.width(8.dp)) - Text("DAILY", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) - Icon(painterResource(R.drawable.ic_favorite), null, tint = MaterialTheme.colorScheme.onSurfaceVariant) // Dropdown arrow - } - Text( - text = "PRIMARY WALLET", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - modifier = Modifier.align(Alignment.End), - ) - - Spacer(modifier = Modifier.height(24.dp)) - - GajuTextField( - value = toAddress, - onValueChange = { toAddress = it }, - label = "SENDING TO", - placeholder = "ak_...", - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(24.dp)) - - GajuTextField( - value = amount, - onValueChange = { amount = it }, - label = "AMOUNT", - placeholder = "0.0", - modifier = Modifier.fillMaxWidth(), - trailingIcon = { Text("木", color = MaterialTheme.colorScheme.onSurface) }, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text("CURRENCY", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - Spacer(modifier = Modifier.width(8.dp)) - Text("木 Gajus", color = MaterialTheme.colorScheme.onBackground) - Icon(painterResource(R.drawable.ic_favorite), null, tint = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - } -} - -@Composable -fun SendReviewScreen( - onSend: () -> Unit, - onBack: () -> Unit, -) { - var confirmed by remember { mutableStateOf(value = false) } - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { confirmed = !confirmed }.padding(bottom = 24.dp) - ) { - Checkbox(checked = confirmed, onCheckedChange = { confirmed = it }) - Text( - "I confirm that the above details are correct and I would like to proceed.", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onBackground, - ) - } - - Row(modifier = Modifier.fillMaxWidth()) { - GajuButton( - text = "BACK", - onClick = onBack, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "SEND", - onClick = onSend, - modifier = Modifier.weight(1f), - containerColor = if (confirmed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, - contentColor = if (confirmed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "SEND", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) - Text("DAILY Account", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) - Text("PRIMARY WALLET", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 10.sp) - - Spacer(modifier = Modifier.height(16.dp)) - - Text("SENDING TO", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) - Text("ALICE S....VAN", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) - Text( - text = "ak_9jzmTHpVCbXfMSfyvxGV1ZLbzR8YqdK4jpSL5WgUKLGM4XPVp", - fontSize = 10.sp, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(16.dp), - ) { - Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text("You are sending a payment of", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text("木 22,980.78991", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) - Text("to", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text("ak_9jzm...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) - - Spacer(modifier = Modifier.height(16.dp)) - Text("THIS ACTION CANNOT BE UNDONE.", fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text("Are you sure you want to proceed?", fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - } - } -} - -@Composable -fun TransactionSuccessScreen( - onExit: () -> Unit, -) { - SuccessLayout( - title = "SEND", - proceedText = "EXIT", - onProceed = onExit, - content = { - Text("Transaction Successful!", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(32.dp)) - Text("You have successfully sent a payment of", fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) - Text("木 22,980.78991", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) - Text("to", fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) - Text("ak_9jzm...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) - - Spacer(modifier = Modifier.height(32.dp)) - Text("The amount has been subtracted from the DAILY account in your PRIMARY WALLET.", textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) - - Spacer(modifier = Modifier.height(32.dp)) - Text("You can track this transaction on-chain here.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) - Text("VIEW TRANSACTION ON-CHAIN", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) - }, - ) -} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt new file mode 100644 index 0000000..9552a46 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt @@ -0,0 +1,40 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.ui.components.SuccessLayout + +@Composable +fun TransactionSuccessScreen( + onExit: () -> Unit, +) { + SuccessLayout( + title = "SEND", + proceedText = "EXIT", + onProceed = onExit, + content = { + Text("Transaction Successful!", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(32.dp)) + Text("You have successfully sent a payment of", fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) + Text("木 22,980.78991", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) + Text("to", fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) + Text("ak_9jzm...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + + Spacer(modifier = Modifier.height(32.dp)) + Text("The amount has been subtracted from the DAILY account in your PRIMARY WALLET.", textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + + Spacer(modifier = Modifier.height(32.dp)) + Text("You can track this transaction on-chain here.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + Text("VIEW TRANSACTION ON-CHAIN", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) + }, + ) +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ViewMnemonicScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ViewMnemonicScreen.kt new file mode 100644 index 0000000..6c3db6e --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ViewMnemonicScreen.kt @@ -0,0 +1,66 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.MnemonicReveal + +@Composable +fun ViewMnemonicScreen( + label: String, + phrase: Array, + onBack: () -> Unit +) { + val mnemonicString = remember(phrase) { + phrase.joinToString(" ") { String(it, Charsets.UTF_8) } + } + + Column( + modifier = Modifier.fillMaxSize().padding(24.dp).safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + GajuHeader() + Text( + text = "BACKUP ACCOUNT", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = label.uppercase(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(32.dp)) + + MnemonicReveal(mnemonic = mnemonicString) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Write these words down in order and store them in a secure, offline location. This is the only way to recover your funds if you lose your phone.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.weight(1f)) + + GajuButton( + text = "DONE", + onClick = onBack + ) + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/WalletSetupScreens.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/WalletSetupScreens.kt deleted file mode 100644 index 518f138..0000000 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/WalletSetupScreens.kt +++ /dev/null @@ -1,507 +0,0 @@ -package swiss.qpq.gajumobile.ui.screens - -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.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.itemsIndexed -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import swiss.qpq.gajumobile.R -import swiss.qpq.gajumobile.ui.components.GajuButton -import swiss.qpq.gajumobile.ui.components.GajuHeader -import swiss.qpq.gajumobile.ui.components.GajuTextField -import swiss.qpq.gajumobile.ui.components.MnemonicReveal -import swiss.qpq.gajumobile.ui.components.SuccessLayout -import swiss.qpq.gajumobile.ui.components.WarningBox - -@Composable -fun CreateAccountMnemonicScreen( - onNext: () -> Unit, - onCancel: () -> Unit, -) { - var accountName by remember { mutableStateOf(value = "") } - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - GajuButton( - text = "CANCEL", - onClick = onCancel, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "NEXT", - onClick = onNext, - modifier = Modifier.weight(1f), - ) - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "CREATE NEW ACCOUNT", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 24.dp), - ) - - GajuTextField( - value = accountName, - onValueChange = { accountName = it }, - label = "ACCOUNT NAME", - placeholder = "Name your account", - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "MNEMONIC PHRASE", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 14.sp, - modifier = Modifier.align(Alignment.Start), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Surface( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 200.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - WarningBox( - title = "IMPORTANT", - content = "Do not let anyone else see this.\nThis may be used to access your funds.", - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "You will need to verify this mnemonic phrase in the next window to proceed. Get ready to back this up on a piece of paper. Make sure nobody else can access it.", - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - MnemonicReveal(mnemonic = "abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual") - } - } - } - } -} - -@Composable -fun MnemonicGridScreen( - onNext: () -> Unit, - onBack: () -> Unit, -) { - val mnemonic = "abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual".split(" ") - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - GajuButton( - text = "BACK", - onClick = onBack, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "NEXT", - onClick = onNext, - modifier = Modifier.weight(1f), - ) - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "CREATE NEW ACCOUNT", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 24.dp), - ) - - GajuTextField( - value = "DAILY", - onValueChange = {}, - label = "ACCOUNT NAME", - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "MNEMONIC PHRASE", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 14.sp, - modifier = Modifier.align(Alignment.Start), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - var hidden by remember { mutableStateOf(value = false) } - - Surface( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp), - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (hidden) { - Box( - modifier = Modifier - .fillMaxSize() - .clickable { hidden = false }, - contentAlignment = Alignment.Center, - ) { - Text( - text = "TAP TO SHOW", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - ) - } - } else { - LazyVerticalGrid( - columns = GridCells.Fixed(count = 2), - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - itemsIndexed(mnemonic) { index, word -> - Text( - text = "${index + 1}. $word", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, - ) - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { hidden = true } - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "TAP TO HIDE.", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - ) - } - } - } - } - } - } -} - -@Composable -fun MnemonicVerifyScreen( - onFinish: () -> Unit, - onBack: () -> Unit, -) { - val wordsToVerify = listOf("Word 9", "Word 22", "Word 3", "Word 12", "Word 17", "Word 15") - - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - GajuButton( - text = "BACK", - onClick = onBack, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "FINISH", - onClick = onFinish, - modifier = Modifier.weight(1f), - ) - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "CREATE NEW ACCOUNT", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 24.dp), - ) - - GajuTextField( - value = "", - onValueChange = {}, - label = "ACCOUNT NAME", - placeholder = "Name your account", - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Verify your mnemonic", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 14.sp, - modifier = Modifier - .align(Alignment.Start) - .padding(bottom = 8.dp), - ) - - wordsToVerify.forEach { label -> - GajuTextField( - value = "", - onValueChange = {}, - label = "", - placeholder = label, - modifier = Modifier.padding(vertical = 4.dp), - ) - } - } - } -} - -@Composable -fun RecoverMnemonicScreen( - onRecover: () -> Unit, - onBack: () -> Unit, -) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(24.dp), - ) { - GajuButton( - text = "BACK", - onClick = onBack, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondary, - contentColor = MaterialTheme.colorScheme.onSecondary, - ) - Spacer(modifier = Modifier.width(16.dp)) - GajuButton( - text = "RECOVER\nACCOUNT", - onClick = onRecover, - modifier = Modifier.weight(1f), - ) - } - }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - GajuHeader() - - Text( - text = "CREATE NEW ACCOUNT", // Mockup uses same title for recovery - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 24.dp), - ) - - GajuTextField( - value = "", - onValueChange = {}, - label = "ACCOUNT NAME", - placeholder = "Name your account", - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Input your mnemonic", - color = MaterialTheme.colorScheme.onBackground, - fontSize = 14.sp, - modifier = Modifier - .align(Alignment.Start) - .padding(bottom = 8.dp), - ) - - for (i in 0 until 12) { - Row(modifier = Modifier.padding(vertical = 4.dp)) { - GajuTextField( - value = "", - onValueChange = {}, - label = "", - placeholder = "Word ${i + 1}", - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(12.dp)) - GajuTextField( - value = "", - onValueChange = {}, - label = "", - placeholder = "Word ${i + 13}", - modifier = Modifier.weight(1f), - ) - } - } - } - } -} - -@Composable -fun SetupSuccessScreen( - isRecovered: Boolean, - onProceed: () -> Unit, -) { - SuccessLayout( - title = "CREATE NEW ACCOUNT", - onProceed = onProceed, - content = { - GajuTextField( - value = "DAILY", - onValueChange = {}, - label = "ACCOUNT NAME", - modifier = Modifier.fillMaxWidth(), - ) - - Text( - text = if (isRecovered) "Account successfully recovered!" else "Account successfully created!", - color = MaterialTheme.colorScheme.primary, - fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Text( - text = """ - Do not lose your mnemonic phrase! - Lost mnemonic phrases cannot be recovered. - Losing your mnemonic phrase can result in lost funds! - - Each account has a different mnemonic phrase. - You must back up each account separately. - - You can recheck mnemonic phrases under your MANAGE ACCOUNTS interface. - """.trimIndent(), - color = MaterialTheme.colorScheme.onBackground, - fontSize = 12.sp, - textAlign = TextAlign.Start, - modifier = Modifier.padding(top = 16.dp), - ) - }, - ) -} diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..bdd99f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..c7c23f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 0000000..a1ad7ea --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..1b78f89 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..cbc77be --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000..82f81e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000..0a80a17 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 0000000..259c657 --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility.xml b/app/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 0000000..4f5b695 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,9 @@ + + +