diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e888828..7e54a92 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.biometric) + implementation(libs.play.services.code.scanner) implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.kotlinx.serialization.json) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84f5e6b..8178413 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + + + + - lockErrorMessage = err - } - ) + ) { err -> + lockErrorMessage = err + } } } else { val routerError: (String) -> Unit = { message -> @@ -106,14 +107,15 @@ enum class Screen { EXPLORER, VIEW_MNEMONIC, VERIFY_MNEMONIC, - ENVIRONMENT_INFO + ENVIRONMENT_INFO, + SEND_FORM } @Composable fun GajuRouter( walletRepo: WalletRepository, appStateManager: AppStateManager, - onError: (String) -> Unit + onError: (String) -> Unit, ) { val context = LocalContext.current val appState by appStateManager.state.collectAsState() @@ -140,8 +142,9 @@ fun GajuRouter( } var pendingWalletName by rememberSaveable { mutableStateOf("") } + var pendingNetworkId by rememberSaveable { mutableStateOf("groot.mainnet") } var pendingAccount by remember { mutableStateOf(null) } - var setupRecovered by rememberSaveable { mutableStateOf(false) } + var setupRecovered by rememberSaveable { mutableStateOf(value = false) } var selectedWalletId by rememberSaveable { mutableStateOf("") } var selectedAccountId by rememberSaveable { mutableStateOf("") } @@ -158,15 +161,18 @@ fun GajuRouter( var activeWalletIndex by rememberSaveable { mutableIntStateOf(0) } var activeAccountIndex by rememberSaveable { mutableIntStateOf(0) } + var pendingScanResult by remember { mutableStateOf(null) } + when (currentScreen) { Screen.CREATE_WALLET -> { CreateWalletScreen( - onWalletCreated = { name -> + onWalletCreated = { name, networkId -> pendingWalletName = name + pendingNetworkId = networkId selectedWalletId = "" navigateTo(Screen.SETUP_CHOICE) }, - onBack = if (screenStack.size > 1) { { navigateBack() } } else null + onBack = if (screenStack.size > 1) { { navigateBack() } } else null, ) } Screen.SETUP_CHOICE -> { @@ -189,7 +195,7 @@ fun GajuRouter( setupRecovered = true navigateTo(Screen.MNEMONIC_RECOVERY) }, - onBack = { navigateBack() } + onBack = { navigateBack() }, ) } Screen.MNEMONIC_RECOVERY -> { @@ -206,7 +212,7 @@ fun GajuRouter( onError("Failed to recover account: ${e.message}") } }, - onBack = { navigateBack() } + onBack = { navigateBack() }, ) } Screen.ACCOUNT_NAMING -> { @@ -220,14 +226,17 @@ fun GajuRouter( if (selectedWalletId.isNotEmpty()) { val wallet = walletRepo.loadWallet(selectedWalletId) - if (wallet != null) { - walletRepo.saveWallet(wallet.copy(accounts = wallet.accounts + finalAccount)) + wallet?.let { + walletRepo.saveWallet(it.copy(accounts = it.accounts + finalAccount)) } } else { + val endpoints = if (pendingNetworkId == "groot.mainnet") Wallet.DEFAULT_MAINNET else Wallet.DEFAULT_TESTNET val wallet = Wallet( id = UUID.randomUUID().toString(), name = pendingWalletName.ifBlank { "My Wallet" }, - accounts = listOf(finalAccount) + networkId = pendingNetworkId, + endpoints = endpoints, + accounts = listOf(finalAccount), ) walletRepo.saveWallet(wallet) } @@ -241,7 +250,7 @@ fun GajuRouter( screenStack.clear() screenStack.add(Screen.DASHBOARD) }, - onBack = { navigateBack() } + onBack = { navigateBack() }, ) } Screen.VIEW_MNEMONIC -> { @@ -323,7 +332,7 @@ fun GajuRouter( "manage_wallets" -> Screen.MANAGE_WALLETS else -> null } - if (screen != null) navigateTo(screen) + screen?.let { navigateTo(it) } }, ) } @@ -353,14 +362,14 @@ fun GajuRouter( navigateTo(Screen.DELETE_CONFIRM) } }, - onUpdateNetwork = { id, network -> + onUpdateEndpoints = { id, endpoints -> val wallet = walletRepo.loadWallet(id) wallet?.let { - walletRepo.saveWallet(it.copy(network = network)) + walletRepo.saveWallet(it.copy(endpoints = endpoints)) appStateManager.notifyWalletsChanged() } }, - onBack = { navigateBack() } + onBack = { navigateBack() }, ) } Screen.MANAGE_ACCOUNT -> { @@ -436,19 +445,96 @@ fun GajuRouter( 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 - "manage_wallets" -> Screen.MANAGE_WALLETS - else -> null + Screen.QR_SCANNER -> QRScannerScreen( + onScanResult = { url -> + try { + val result = Grids.parse(url) + pendingScanResult = result + navigateTo(Screen.SEND_FORM) + } catch (e: Exception) { + onError("Invalid GRIDS URL: ${e.message}") + } + }, + onGenerate = { navigateTo(Screen.QR_RECEIVE) }, + onUpload = {}, + onManualEntry = { + pendingScanResult = null + navigateTo(Screen.SEND_FORM) + }, + onBack = { navigateBack() }, + ) + Screen.SEND_FORM -> { + val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } + val activeWallet = wallets.getOrNull(activeWalletIndex) + val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) + + if (activeAccount == null) { + onError("No active account selected.") + navigateBack() + return } - if (screen != null) { - screenStack.clear() - screenStack.add(screen) + + val result = pendingScanResult + val initialRecipient = result?.recipient ?: "" + val initialAmount = if (result?.amount != null) { + val type = GajuFormat.Type.valueOf(appState.balanceFormat) + val unit = GajuFormat.Unit.valueOf(appState.balanceUnit) + GajuFormat.amount(GajuFormat.standardSpec(type, unit), result.amount) + } else "" + val initialPayload = if (result?.payload != null) String(result.payload, Charsets.UTF_8) else "" + + SendFormScreen( + fromAccount = activeAccount, + balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), + balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), + initialRecipient = initialRecipient, + initialAmount = initialAmount, + initialPayload = initialPayload, + onReview = { recipient, amountStr, payload, gas, gasPrice, ttl -> + try { + val pucks = GajuFormat.read(amountStr) + val pucksBI = java.math.BigInteger(pucks) + onError("Reviewing: To $recipient, $pucksBI Pucks, Msg: $payload, Gas: $gas, Price: $gasPrice, TTL: $ttl") + } catch (e: Exception) { + onError("Invalid amount: ${e.message}") + } + }, + onBack = { navigateBack() }, + ) + } + Screen.QR_RECEIVE -> { + val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } + val activeWallet = wallets.getOrNull(activeWalletIndex) + val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) + + if (activeAccount == null) { + onError("No active account selected.") + navigateBack() + return } - }) + + QRReceiveScreen( + account = activeAccount, + balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), + balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), + onDone = { navigateBack() }, + ) + } + Screen.EXPLORER -> { + ExplorerScreen( + onNavigate = { dest -> + val screen = when (dest) { + "dashboard" -> Screen.DASHBOARD + "settings" -> Screen.SETTINGS + "manage_wallets" -> Screen.MANAGE_WALLETS + else -> null + } + if (screen != null) { + screenStack.clear() + screenStack.add(screen) + } + } + ) + } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt b/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt index 7209599..44e2ce5 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/data/models/Account.kt @@ -33,8 +33,7 @@ data class Account( */ 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) + return GajuFormat.amount(GajuFormat.standardSpec(type, unit), bi) } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/models/Wallet.kt b/app/src/main/java/swiss/qpq/gajumobile/data/models/Wallet.kt index 4a037d4..d80229f 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/data/models/Wallet.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/data/models/Wallet.kt @@ -2,11 +2,30 @@ package swiss.qpq.gajumobile.data.models import kotlinx.serialization.Serializable +@Serializable +data class Endpoint( + val host: String, + val port: Int = 3013, +) + @Serializable data class Wallet( val id: String, val name: String, + val networkId: String = "groot.mainnet", val accounts: List = emptyList(), val createdAt: Long = System.currentTimeMillis(), - val network: String = "Mainnet" -) + val endpoints: List = emptyList() +) { + companion object { + val DEFAULT_MAINNET = listOf( + Endpoint("groot.mainnet.gajumaru.io", 3013), + Endpoint("tsuriai.jp", 3013) + ) + + val DEFAULT_TESTNET = listOf( + Endpoint("groot.testnet.gajumaru.io", 3013), + Endpoint("tsuriai.jp", 4013) + ) + } +} diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt index 83d997e..c614957 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/components/GajuComponents.kt @@ -81,7 +81,7 @@ fun GajuButton( @Composable fun GajuTopBar( - onNavigate: (String) -> Unit + onNavigate: (String) -> Unit, ) { var menuExpanded by remember { mutableStateOf(value = false) } @@ -260,6 +260,7 @@ fun GajuTextField( modifier: Modifier = Modifier, placeholder: String = "", trailingIcon: @Composable (() -> Unit)? = null, + singleLine: Boolean = false, ) { Column(modifier = modifier) { Text( @@ -273,6 +274,7 @@ fun GajuTextField( onValueChange = onValueChange, placeholder = { Text(placeholder, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)) }, modifier = Modifier.fillMaxWidth(), + singleLine = singleLine, colors = OutlinedTextFieldDefaults.colors( unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, @@ -414,7 +416,7 @@ fun SecurityDataSection( color = MaterialTheme.colorScheme.onSurfaceVariant ) } - if (index < data.size - 1) { + if (index < (data.size - 1)) { HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt index 41a810d..b6670bf 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/CreateWalletScreen.kt @@ -1,6 +1,7 @@ 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 @@ -10,6 +11,8 @@ 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.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,10 +30,11 @@ import swiss.qpq.gajumobile.ui.components.GajuTextField @Composable fun CreateWalletScreen( - onWalletCreated: (String) -> Unit, + onWalletCreated: (name: String, networkId: String) -> Unit, onBack: (() -> Unit)? = null ) { var walletName by remember { mutableStateOf("") } + var selectedNetwork by remember { mutableStateOf("groot.mainnet") } Column( modifier = Modifier @@ -55,12 +59,41 @@ fun CreateWalletScreen( color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(32.dp)) + GajuTextField( value = walletName, onValueChange = { walletName = it }, label = "Wallet Name", - placeholder = "e.g. Personal, Savings" + placeholder = "e.g. Personal, Savings", + singleLine = true ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + "NETWORK", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.align(Alignment.Start) + ) + + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + listOf("groot.mainnet" to "Mainnet", "groot.testnet" to "Testnet").forEach { (id, label) -> + val isSelected = selectedNetwork == id + AssistChip( + onClick = { selectedNetwork = id }, + label = { Text(label) }, + 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)) Row(modifier = Modifier.fillMaxWidth()) { @@ -77,7 +110,7 @@ fun CreateWalletScreen( GajuButton( text = "Continue", enabled = walletName.isNotBlank(), - onClick = { onWalletCreated(walletName) }, + onClick = { onWalletCreated(walletName, selectedNetwork) }, modifier = Modifier.weight(1f) ) } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt index 9a7475c..63bd154 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/EnvironmentInfoScreen.kt @@ -88,7 +88,7 @@ fun EnvironmentInfoScreen( title = "GajuMobile", data = listOf( "App Version" to appVersion, - "Network" to "Mainnet (Production)" + "Theme" to "Arboreal (Gajumaru)" ) ) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt index 2138934..3307e8d 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManageWalletsScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -34,6 +35,7 @@ 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.data.models.Endpoint import swiss.qpq.gajumobile.data.models.Wallet import swiss.qpq.gajumobile.ui.components.GajuHeader import swiss.qpq.gajumobile.ui.components.GajuTextField @@ -45,18 +47,22 @@ fun ManageWalletsScreen( onAddWallet: () -> Unit, onRenameWallet: (String, String) -> Unit, onDeleteWallet: (String) -> Unit, - onUpdateNetwork: (String, String) -> Unit, - onBack: () -> Unit + onUpdateEndpoints: (String, List) -> Unit, + onBack: () -> Unit, ) { var walletToRename by remember { mutableStateOf(null) } var newName by remember { mutableStateOf("") } + var walletForNewEndpoint by remember { mutableStateOf(null) } + var newHost by remember { mutableStateOf("") } + var newPort by remember { mutableStateOf("3013") } + if (walletToRename != null) { androidx.compose.ui.window.Dialog(onDismissRequest = { walletToRename = null }) { Surface( shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surface, - tonalElevation = 8.dp + tonalElevation = 8.dp, ) { Column(modifier = Modifier.padding(24.dp)) { Text("Rename Wallet", style = MaterialTheme.typography.headlineSmall) @@ -70,10 +76,60 @@ fun ManageWalletsScreen( Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { TextButton(onClick = { walletToRename = null }) { Text("CANCEL") } Spacer(modifier = Modifier.width(8.dp)) - TextButton(onClick = { - walletToRename?.let { onRenameWallet(it.id, newName) } - walletToRename = null - }) { Text("SAVE") } + TextButton( + onClick = { + walletToRename?.let { onRenameWallet(it.id, newName) } + walletToRename = null + } + ) { Text("SAVE") } + } + } + } + } + } + + if (walletForNewEndpoint != null) { + androidx.compose.ui.window.Dialog(onDismissRequest = { walletForNewEndpoint = null }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp, + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text("Add Endpoint", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(16.dp)) + GajuTextField( + value = newHost, + onValueChange = { newHost = it }, + label = "Host (IP or Domain)", + singleLine = true, + placeholder = "groot.gajumaru.io" + ) + Spacer(modifier = Modifier.height(16.dp)) + GajuTextField( + value = newPort, + onValueChange = { newPort = it }, + label = "Port", + singleLine = true, + placeholder = "3013" + ) + Spacer(modifier = Modifier.height(24.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = { walletForNewEndpoint = null }) { Text("CANCEL") } + Spacer(modifier = Modifier.width(8.dp)) + TextButton( + enabled = (newHost.isNotBlank() && newPort.toIntOrNull() != null), + onClick = { + walletForNewEndpoint?.let { wallet -> + val port = newPort.toIntOrNull() ?: 3013 + val newList = wallet.endpoints + Endpoint(newHost, port) + onUpdateEndpoints(wallet.id, newList) + } + walletForNewEndpoint = null + newHost = "" + newPort = "3013" + } + ) { Text("ADD") } } } } @@ -107,7 +163,11 @@ fun ManageWalletsScreen( walletToRename = wallet }, onDelete = { onDeleteWallet(wallet.id) }, - onUpdateNetwork = { onUpdateNetwork(wallet.id, it) } + onAddEndpoint = { walletForNewEndpoint = wallet }, + onDeleteEndpoint = { index -> + val newList = wallet.endpoints.toMutableList().apply { removeAt(index) } + onUpdateEndpoints(wallet.id, newList) + }, ) Spacer(modifier = Modifier.height(16.dp)) } @@ -134,50 +194,90 @@ private fun WalletManagementCard( wallet: Wallet, onRename: () -> Unit, onDelete: () -> Unit, - onUpdateNetwork: (String) -> Unit + onAddEndpoint: () -> Unit, + onDeleteEndpoint: (Int) -> Unit, ) { Surface( color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(12.dp), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = wallet.name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.clickable { onRename() } - ) + Column { + Text( + text = wallet.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.clickable { onRename() }, + ) + Text( + text = wallet.networkId.uppercase(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } IconButton(onClick = onDelete) { Icon( painter = painterResource(id = R.drawable.ic_delete), contentDescription = "Delete Wallet", tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), ) } } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) - GajuTextField( - value = wallet.network, - onValueChange = onUpdateNetwork, - label = "Network", - placeholder = "e.g. Mainnet, Testnet" + Text( + text = "NETWORK ENDPOINTS", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f) ) Spacer(modifier = Modifier.height(8.dp)) + wallet.endpoints.forEachIndexed { index, endpoint -> + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "${endpoint.host}:${endpoint.port}", + style = MaterialTheme.typography.bodySmall.copy(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (wallet.endpoints.size > 1) { + Icon( + painter = painterResource(id = R.drawable.ic_delete), + contentDescription = "Remove Endpoint", + modifier = Modifier.size(16.dp).clickable { onDeleteEndpoint(index) }, + tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f) + ) + } + } + } + + TextButton( + onClick = onAddEndpoint, + modifier = Modifier.align(Alignment.End) + ) { + Icon(painterResource(R.drawable.ic_add), null, modifier = Modifier.size(14.dp)) + Spacer(Modifier.width(4.dp)) + Text("ADD ENDPOINT", fontSize = 11.sp) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + Text( text = "${wallet.accounts.size} account(s)", style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ) } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt index 645e062..1707ade 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRReceiveScreen.kt @@ -1,8 +1,13 @@ package swiss.qpq.gajumobile.ui.screens +import android.content.ClipData +import androidx.compose.foundation.Canvas 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 @@ -13,41 +18,109 @@ 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.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider 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.rememberCoroutineScope +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.ClipEntry +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch 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.gajumaru.core.encoding.QR +import swiss.qpq.gajumaru.core.formatting.GajuFormat +import java.math.BigInteger +import java.net.URLEncoder @Composable fun QRReceiveScreen( - onDone: () -> Unit + account: swiss.qpq.gajumobile.data.models.Account, + balanceFormat: GajuFormat.Type, + balanceUnit: GajuFormat.Unit, + onDone: () -> Unit, ) { + var amountStr by remember { mutableStateOf(value = "") } + var payload by remember { mutableStateOf(value = "") } + var isGenerated by remember { mutableStateOf(value = false) } + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + + val amountPucks = remember(amountStr) { + try { + if (amountStr.isBlank()) BigInteger.ZERO else BigInteger(GajuFormat.read(amountStr)) + } catch (_: Exception) { + BigInteger.ZERO + } + } + + val formattedAmount = remember(amountPucks, balanceFormat, balanceUnit) { + GajuFormat.amount(GajuFormat.standardSpec(balanceFormat, balanceUnit), amountPucks) + } + + val generatedUrl = remember(amountPucks, payload, account.gajuId) { + try { + val baseUrl = "grids://gaju/1/s/${account.gajuId}?a=$amountPucks" + if (payload.isNotBlank()) { + val p = URLEncoder.encode(payload, "UTF-8") + "$baseUrl&p=$p" + } else { + baseUrl + } + } catch (_: Exception) { + "" + } + } + + val qrMatrix = remember(generatedUrl) { + if (generatedUrl.isNotEmpty()) { + try { QR.encode(generatedUrl) } catch (_: Exception) { null } + } else { + null + } + } + Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { - Column( + Row( modifier = Modifier .fillMaxWidth() .windowInsetsPadding(WindowInsets.navigationBars) - .padding(horizontal = 24.dp, vertical = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, + .padding(24.dp), ) { GajuButton( - text = "DONE", - onClick = onDone, - modifier = Modifier.width(160.dp), + text = if (isGenerated) "EDIT" else "BACK", + onClick = { if (isGenerated) isGenerated = false else onDone() }, + modifier = Modifier.weight(1f), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ) + Spacer(modifier = Modifier.width(16.dp)) + GajuButton( + text = if (isGenerated) "DONE" else "GENERATE", + onClick = { if (isGenerated) onDone() else isGenerated = true }, + modifier = Modifier.weight(1f), ) } }, @@ -57,42 +130,200 @@ fun QRReceiveScreen( .fillMaxSize() .background(MaterialTheme.colorScheme.background) .padding(innerPadding) - .padding(horizontal = 24.dp), + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { GajuHeader() - Spacer(modifier = Modifier.weight(1f)) + Text( + text = if (isGenerated) "TRANSFER REQUEST" else "RECEIVE", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 16.dp), + ) - // 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) { + if (!isGenerated) { + // Input Phase + Column(modifier = Modifier.fillMaxWidth()) { + Text("RECIPIENT", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) + Spacer(modifier = Modifier.height(4.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + text = account.label, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ) + Text( + text = account.gajuId, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Amount (String input) + val unitLabel = if (balanceUnit == GajuFormat.Unit.GAJU) "GAJU" else "PUCK" + GajuTextField( + value = amountStr, + onValueChange = { amountStr = it }, + label = "REQUESTED AMOUNT (OPTIONAL)", + placeholder = "0.0", + modifier = Modifier.fillMaxWidth(), + trailingIcon = { Text(unitLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) }, + singleLine = true, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + GajuTextField( + value = payload, + onValueChange = { payload = it }, + label = "PAYLOAD / MESSAGE (OPTIONAL)", + placeholder = "Add a note...", + modifier = Modifier.fillMaxWidth(), + ) + } else { + // Display Phase + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp)) + .padding(16.dp), + ) { + DetailRow(label = "RECIPIENT", value = account.label) + Text( + text = account.gajuId, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + ) + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ) + DetailRow(label = "AMOUNT", value = formattedAmount) + if (payload.isNotBlank()) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ) + DetailRow(label = "MESSAGE", value = payload) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Surface( + modifier = Modifier.size(280.dp), + color = Color.White, + shape = RoundedCornerShape(16.dp), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(16.dp)) { + if (qrMatrix != null) { + QRCanvas(qrMatrix) + } else { + Icon( + painter = painterResource(id = R.drawable.ic_qr_code), + contentDescription = "QR Code", + modifier = Modifier.size(180.dp), + tint = Color.Black.copy(alpha = 0.1f), + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "GRIDS URL", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp, + ) Icon( - painter = painterResource(id = R.drawable.ic_qr_code), - contentDescription = "QR Code", - modifier = Modifier.size(180.dp), - tint = Color.Black, + painter = painterResource(id = R.drawable.ic_content_copy), + contentDescription = "Copy URL", + modifier = Modifier + .size(20.dp) + .clickable { + scope.launch { + clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("GRIDS URL", generatedUrl))) + } + }, + tint = MaterialTheme.colorScheme.primary, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = generatedUrl, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + modifier = Modifier.padding(12.dp), + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "Scan to send to this account", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 14.sp, + fontWeight = FontWeight.Light, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Composable +fun DetailRow(label: String, value: String) { + Column(modifier = Modifier.fillMaxWidth()) { + Text(text = label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)) + Text(text = value, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold) + } +} + +@Composable +fun QRCanvas(matrix: Array) { + val size = matrix.size + Canvas(modifier = Modifier.fillMaxSize()) { + val cellSize = this.size.width / size + val offsetX = (this.size.width - (cellSize * size)) / 2 + val offsetY = (this.size.height - (cellSize * size)) / 2 + + for (y in 0 until size) { + for (x in 0 until size) { + if (matrix[y][x]) { + drawRect( + color = Color.Black, + topLeft = androidx.compose.ui.geometry.Offset(offsetX + (x * cellSize), offsetY + (y * cellSize)), + size = androidx.compose.ui.geometry.Size(cellSize, cellSize), ) } } - - Text( - text = "Sender must scan this QR.", - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(top = 24.dp), - ) - Text( - text = "You can also send them a screenshot of this QR.", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, - ) - - Spacer(modifier = Modifier.weight(1f)) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt index 6e0df88..244b7c6 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/QRScannerScreen.kt @@ -2,6 +2,7 @@ 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 @@ -9,30 +10,58 @@ 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.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.draw.clip +import androidx.compose.ui.platform.LocalContext 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 com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions import swiss.qpq.gajumobile.R import swiss.qpq.gajumobile.ui.components.GajuHeader import swiss.qpq.gajumobile.ui.components.ScannerAction +import swiss.qpq.gajumobile.ui.components.GajuButton @Composable fun QRScannerScreen( + onScanResult: (String) -> Unit, onGenerate: () -> Unit, - onUpload: () -> Unit + onUpload: () -> Unit, + onManualEntry: () -> Unit, + onBack: () -> Unit, ) { + val context = LocalContext.current + + val scannerOptions = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .enableAutoZoom() + .build() + + val scanner = GmsBarcodeScanning.getClient(context, scannerOptions) + + fun startScan() { + scanner.startScan() + .addOnSuccessListener { barcode -> + barcode.rawValue?.let { onScanResult(it) } + } + .addOnFailureListener { + // Handle error if needed + } + } + Box( modifier = Modifier .fillMaxSize() @@ -55,21 +84,41 @@ fun QRScannerScreen( Spacer(modifier = Modifier.weight(1f)) - // Scanner Viewfinder Mockup + // Scanner Prompt Area Box( modifier = Modifier .size(280.dp) - .border(2.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(16.dp)), + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(16.dp)) + .clickable { startScan() }, contentAlignment = Alignment.Center, ) { - Icon( - painter = painterResource(id = R.drawable.ic_qr_code), - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colorScheme.primary, - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = painterResource(id = R.drawable.ic_qr_code), + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "TAP TO SCAN", + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ) + } } + Spacer(modifier = Modifier.weight(0.5f)) + + GajuButton( + text = "MANUAL ENTRY", + onClick = onManualEntry, + modifier = Modifier.padding(horizontal = 48.dp), + ) + Spacer(modifier = Modifier.weight(1f)) Row( @@ -78,6 +127,7 @@ fun QRScannerScreen( .padding(bottom = 48.dp), horizontalArrangement = Arrangement.SpaceEvenly, ) { + ScannerAction(label = "Back", icon = R.drawable.ic_home, onClick = onBack) ScannerAction(label = "Generate QR", icon = R.drawable.ic_qr_code, onClick = onGenerate) ScannerAction(label = "Upload QR", icon = R.drawable.ic_qr_code, onClick = onUpload) } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt index 630c83f..6980ef5 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendFormScreen.kt @@ -1,6 +1,7 @@ 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 @@ -13,36 +14,47 @@ 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.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.FontFamily 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.gajumaru.core.formatting.GajuFormat @Composable fun SendFormScreen( - onReview: () -> Unit, + fromAccount: swiss.qpq.gajumobile.data.models.Account, + balanceFormat: GajuFormat.Type, + balanceUnit: GajuFormat.Unit, + initialRecipient: String = "", + initialAmount: String = "", + initialPayload: String = "", + onReview: (recipient: String, amount: String, payload: String, gas: String, gasPrice: String, ttl: String) -> Unit, onBack: () -> Unit, ) { - var toAddress by remember { mutableStateOf(value = "") } - var amount by remember { mutableStateOf(value = "") } + var toAddress by remember { mutableStateOf(value = initialRecipient) } + var amount by remember { mutableStateOf(value = initialAmount) } + var payload by remember { mutableStateOf(value = initialPayload) } + var gas by remember { mutableStateOf(value = "20000") } // Default gas + var gasPrice by remember { mutableStateOf(value = "1000000000") } // Default gas price + var ttl by remember { mutableStateOf(value = "500") } // Default TTL + var showAdvanced by remember { mutableStateOf(value = false) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -63,8 +75,9 @@ fun SendFormScreen( Spacer(modifier = Modifier.width(16.dp)) GajuButton( text = "REVIEW", - onClick = onReview, + onClick = { onReview(toAddress, amount, payload, gas, gasPrice, ttl) }, modifier = Modifier.weight(1f), + enabled = toAddress.isNotBlank() && amount.isNotBlank(), ) } }, @@ -88,48 +101,125 @@ fun SendFormScreen( modifier = Modifier.padding(vertical = 16.dp), ) - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + // 0. Sender Account (Read-only) + Column(modifier = Modifier.fillMaxWidth()) { 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 + Spacer(modifier = Modifier.height(4.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + text = fromAccount.label, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ) + Text( + text = "Balance: ${fromAccount.formattedBalance(balanceFormat, balanceUnit)}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ) + Text( + text = fromAccount.gajuId, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + ) + } + } } - Text( - text = "Primary Wallet", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 12.sp, - modifier = Modifier.align(Alignment.End), - ) Spacer(modifier = Modifier.height(24.dp)) + // 1. Recipient Account ID GajuTextField( value = toAddress, onValueChange = { toAddress = it }, - label = "SENDING TO", + label = "RECIPIENT ACCOUNT", placeholder = "ak_...", modifier = Modifier.fillMaxWidth(), + singleLine = true, ) Spacer(modifier = Modifier.height(24.dp)) + // 2. Amount (String input) + val unitLabel = if (balanceUnit == GajuFormat.Unit.GAJU) "GAJU" else "PUCK" GajuTextField( value = amount, onValueChange = { amount = it }, label = "AMOUNT", placeholder = "0.0", modifier = Modifier.fillMaxWidth(), - trailingIcon = { Text("木", color = MaterialTheme.colorScheme.onSurface) }, + trailingIcon = { Text(unitLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) }, + singleLine = true, ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(24.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)) + // 3. Payload/Message + GajuTextField( + value = payload, + onValueChange = { payload = it }, + label = "PAYLOAD / MESSAGE", + placeholder = "Optional message...", + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = if (showAdvanced) "HIDE DETAILS" else "SHOW DETAILS", + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .clickable { showAdvanced = !showAdvanced } + .padding(vertical = 8.dp), + textAlign = TextAlign.Center, + ) + + if (showAdvanced) { + Spacer(modifier = Modifier.height(16.dp)) + + // 4. Gas + GajuTextField( + value = gas, + onValueChange = { gas = it }, + label = "GAS", + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + // 5. Gas Price + GajuTextField( + value = gasPrice, + onValueChange = { gasPrice = it }, + label = "GAS PRICE", + modifier = Modifier.weight(1f), + singleLine = true, + ) + Spacer(modifier = Modifier.width(16.dp)) + // 6. TTL + GajuTextField( + value = ttl, + onValueChange = { ttl = it }, + label = "TTL", + modifier = Modifier.weight(1f), + singleLine = true, + ) + } } + + Spacer(modifier = Modifier.height(32.dp)) } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/theme/GajuColors.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/theme/GajuColors.kt index ba5c83f..2ece1e8 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/theme/GajuColors.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/theme/GajuColors.kt @@ -2,31 +2,31 @@ package swiss.qpq.gajumobile.ui.theme import androidx.compose.ui.graphics.Color -// Neon Dark Palette -val NeonDarkBackground = Color(0xFF1C1B1F) -val NeonDarkSurface = Color(0xFF252429) -val NeonDarkSurfaceVariant = Color(0xFF121212) -val NeonDarkPrimary = Color(0xFF9F72FF) -val NeonDarkSecondary = Color(0xFFE0E0E0) -val NeonDarkAccent = Color(0xFF00E5FF) -val NeonDarkOnPrimary = Color(0xFFFFFFFF) -val NeonDarkOnSecondary = Color(0xFF000000) -val NeonDarkOnBackground = Color(0xFFFFFFFF) -val NeonDarkOnSurface = Color(0xFFFFFFFF) -val NeonDarkOnSurfaceVariant = Color(0xFFE0E0E0) +// Arboreal Dark Palette (Gajumaru Theme) +val GajuDarkBackground = Color(0xFF1B1C17) // Dark bark/olive +val GajuDarkSurface = Color(0xFF23251D) +val GajuDarkSurfaceVariant = Color(0xFF181A12) +val GajuDarkPrimary = Color(0xFF81C784) // Fresh leaf green +val GajuDarkSecondary = Color(0xFFA1887F) // Light bark brown +val GajuDarkAccent = Color(0xFFD4E157) // Moss yellow-green +val GajuDarkOnPrimary = Color(0xFF003300) +val GajuDarkOnSecondary = Color(0xFF3E2723) +val GajuDarkOnBackground = Color(0xFFE3E3DC) +val GajuDarkOnSurface = Color(0xFFE3E3DC) +val GajuDarkOnSurfaceVariant = Color(0xFFC7C7BC) -// Classic Light Palette -val ClassicLightBackground = Color(0xFFFFFFFF) -val ClassicLightSurface = Color(0xFFF5F5F5) -val ClassicLightSurfaceVariant = Color(0xFFEEEEEE) -val ClassicLightPrimary = Color(0xFF6200EE) -val ClassicLightSecondary = Color(0xFF757575) -val ClassicLightOnPrimary = Color(0xFFFFFFFF) -val ClassicLightOnSecondary = Color(0xFFFFFFFF) -val ClassicLightOnBackground = Color(0xFF000000) -val ClassicLightOnSurface = Color(0xFF000000) -val ClassicLightOnSurfaceVariant = Color(0xFF424242) +// Arboreal Light Palette +val GajuLightBackground = Color(0xFFFCFCF5) +val GajuLightSurface = Color(0xFFF1F1EA) +val GajuLightSurfaceVariant = Color(0xFFE3E3DC) +val GajuLightPrimary = Color(0xFF2E7D32) // Forest green +val GajuLightSecondary = Color(0xFF5D4037) // Deep wood brown +val GajuLightOnPrimary = Color(0xFFFFFFFF) +val GajuLightOnSecondary = Color(0xFFFFFFFF) +val GajuLightOnBackground = Color(0xFF1B1C17) +val GajuLightOnSurface = Color(0xFF1B1C17) +val GajuLightOnSurfaceVariant = Color(0xFF46483C) -// Logo Gradient Colors (approximate from image) -val GajuLogoPurple = Color(0xFF9D39FF) -val GajuLogoTeal = Color(0xFF39FFEA) +// Logo Gradient Colors (Arboreal Shift) +val GajuLogoGreen = Color(0xFF4CAF50) +val GajuLogoBrown = Color(0xFF795548) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/theme/Theme.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/theme/Theme.kt index cf1e396..b6bd603 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/theme/Theme.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/theme/Theme.kt @@ -10,31 +10,31 @@ import androidx.compose.runtime.SideEffect import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat -private val NeonDarkColorScheme = darkColorScheme( - primary = NeonDarkPrimary, - secondary = NeonDarkSecondary, - tertiary = NeonDarkAccent, - background = NeonDarkBackground, - surface = NeonDarkSurface, - surfaceVariant = NeonDarkSurfaceVariant, - onPrimary = NeonDarkOnPrimary, - onSecondary = NeonDarkOnSecondary, - onBackground = NeonDarkOnBackground, - onSurface = NeonDarkOnSurface, - onSurfaceVariant = NeonDarkOnSurfaceVariant, +private val ArborealDarkColorScheme = darkColorScheme( + primary = GajuDarkPrimary, + secondary = GajuDarkSecondary, + tertiary = GajuDarkAccent, + background = GajuDarkBackground, + surface = GajuDarkSurface, + surfaceVariant = GajuDarkSurfaceVariant, + onPrimary = GajuDarkOnPrimary, + onSecondary = GajuDarkOnSecondary, + onBackground = GajuDarkOnBackground, + onSurface = GajuDarkOnSurface, + onSurfaceVariant = GajuDarkOnSurfaceVariant, ) -private val ClassicLightColorScheme = lightColorScheme( - primary = ClassicLightPrimary, - secondary = ClassicLightSecondary, - background = ClassicLightBackground, - surface = ClassicLightSurface, - surfaceVariant = ClassicLightSurfaceVariant, - onPrimary = ClassicLightOnPrimary, - onSecondary = ClassicLightOnSecondary, - onBackground = ClassicLightOnBackground, - onSurface = ClassicLightOnSurface, - onSurfaceVariant = ClassicLightOnSurfaceVariant, +private val ArborealLightColorScheme = lightColorScheme( + primary = GajuLightPrimary, + secondary = GajuLightSecondary, + background = GajuLightBackground, + surface = GajuLightSurface, + surfaceVariant = GajuLightSurfaceVariant, + onPrimary = GajuLightOnPrimary, + onSecondary = GajuLightOnSecondary, + onBackground = GajuLightOnBackground, + onSurface = GajuLightOnSurface, + onSurfaceVariant = GajuLightOnSurfaceVariant, ) @Composable @@ -42,7 +42,7 @@ fun MyApplicationTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val colorScheme = if (darkTheme) NeonDarkColorScheme else ClassicLightColorScheme + val colorScheme = if (darkTheme) ArborealDarkColorScheme else ArborealLightColorScheme val view = LocalView.current if (!view.isInEditMode) { diff --git a/gm-java b/gm-java index 7493b47..11516b1 160000 --- a/gm-java +++ b/gm-java @@ -1 +1 @@ -Subproject commit 7493b473a7fa2bdf6c606fa8b45593794fde9afa +Subproject commit 11516b1cca5ae9ef7a743e599b50e59af76223e6 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3aa37bd..baa26f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" kotlin = "2.2.10" composeBom = "2025.12.00" +playServicesCodeScanner = "16.1.0" coreSplashScreen = "1.2.0" biometric = "1.1.0" lifecycleProcess = "2.6.1" @@ -31,6 +32,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashScreen" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" } +play-services-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "playServicesCodeScanner" } androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }