This commit is contained in:
2026-08-22 18:45:28 +09:00
parent 2c2af3d049
commit 8d7d7b5f84
42 changed files with 3035 additions and 2238 deletions
+92 -58
View File
@@ -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 -2
View File
@@ -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),
)
},
)
}
+9
View File
@@ -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>
+9
View File
@@ -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>
+9
View File
@@ -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>
+9
View File
@@ -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>
+9
View File
@@ -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>