WIP
This commit is contained in:
@@ -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 <info@qpq.swiss>. All rights reserved. Project:
|
||||
Gajumaru Core Java Libraries
|
||||
Copyright (c) 2026 QPQ AG <info@qpq.swiss>. 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 <craigeverett@qpq.swiss>
|
||||
- Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
@@ -2,9 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
||||
@@ -2,12 +2,14 @@ package swiss.qpq.gajumobile
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -25,19 +27,24 @@ import swiss.qpq.gajumobile.security.BiometricHelper
|
||||
import swiss.qpq.gajumobile.security.SecurityManager
|
||||
import swiss.qpq.gajumobile.security.KeyManager
|
||||
import swiss.qpq.gajumobile.security.AccountAirlock
|
||||
import swiss.qpq.gajumobile.ui.screens.LockScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.SetupChoiceScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.AccountNamingScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.DashboardScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.QRScannerScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.DeleteConfirmationScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.EnvironmentInfoScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.ExplorerScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.LockScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.ManageAccountScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.ManageWalletScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.MnemonicVerifyScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.QRReceiveScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.QRScannerScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.SettingsScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.ManageOverviewScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.SetupChoiceScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.ViewMnemonicScreen
|
||||
import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme
|
||||
import swiss.qpq.gajumaru.core.encoding.ApiEncoder
|
||||
import android.widget.Toast
|
||||
import swiss.qpq.gajumaru.core.formatting.GajuFormat
|
||||
import java.util.UUID
|
||||
|
||||
class MainActivity : FragmentActivity() {
|
||||
@@ -60,18 +67,17 @@ class MainActivity : FragmentActivity() {
|
||||
|
||||
MyApplicationTheme {
|
||||
if (isLocked) {
|
||||
LockScreen(
|
||||
errorMessage = lockErrorMessage,
|
||||
) {
|
||||
LockScreen(errorMessage = lockErrorMessage) {
|
||||
BiometricHelper.showBiometricPrompt(
|
||||
activity = this,
|
||||
onSuccess = {
|
||||
lockErrorMessage = null
|
||||
SecurityManager.unlock()
|
||||
},
|
||||
) { err ->
|
||||
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<Account?>(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<Account?>(null) }
|
||||
var viewMnemonicPhrase by remember { mutableStateOf<Array<ByteArray>?>(null) }
|
||||
|
||||
var verifyMnemonicAccount by remember { mutableStateOf<Account?>(null) }
|
||||
var verifyMnemonicPhrase by remember { mutableStateOf<Array<ByteArray>?>(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = 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<AppState> = _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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Pair<String, String>>
|
||||
) {
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Wallet>,
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<ByteArray>) -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
var committedWords by remember { mutableStateOf(listOf<String>()) }
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ByteArray>,
|
||||
onVerified: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
var committedWords by remember { mutableStateOf(listOf<String>()) }
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ByteArray>) -> 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<String>,
|
||||
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 }) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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<ByteArray>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 12,10 12,-4.48 12,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L12,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83l3.75,3.75L20.71,7.04z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2H9V6zM18,20H6V10h12v10zM12,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2s2,-0.9 2,-2s-0.9,-2 -2,-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5zM3,21h8v-8H3V21zM5,15h4v4H5V15zM13,3v8h8V3H13zM19,9h-4V5h4V9zM19,19v-2h2v2H19zM13,13h2v2h-2V13zM15,15h2v2h-2V15zM13,17h2v2h-2V17zM17,13h2v2h-2V13zM17,17h2v2h-2V17zM19,13h2v2h-2V13zM15,19h2v2h-2V19z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user