From 9f0114e96379d8dc5b259b54831016a6804dd81e Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 23 Aug 2026 18:16:30 +0900 Subject: [PATCH] WIP --- app/src/main/AndroidManifest.xml | 3 +- .../java/swiss/qpq/gajumobile/MainActivity.kt | 193 +++++++++++++++++- .../qpq/gajumobile/data/WalletRepository.kt | 58 ++++++ .../qpq/gajumobile/data/models/Wallet.kt | 9 +- .../gajumobile/security/AccountAirlock.java | 27 +++ .../gajumobile/ui/screens/DashboardScreen.kt | 41 ++-- .../ui/screens/ManageWalletsScreen.kt | 130 ++++++++++-- .../gajumobile/ui/screens/SendReviewScreen.kt | 23 ++- gm-java | 2 +- 9 files changed, 430 insertions(+), 56 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8178413..813ce66 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.GajuMobile"> + android:theme="@style/Theme.GajuMobile" + android:usesCleartextTraffic="true"> diff --git a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt index 727fa65..9fff3b5 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt @@ -13,12 +13,16 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalContext import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ProcessLifecycleOwner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import swiss.qpq.gajumobile.data.AppStateManager import swiss.qpq.gajumobile.data.WalletRepository import swiss.qpq.gajumobile.data.models.Account @@ -42,12 +46,17 @@ 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.SendFormScreen +import swiss.qpq.gajumobile.ui.screens.SendReviewScreen import swiss.qpq.gajumobile.ui.screens.SettingsScreen 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.formatting.GajuFormat import swiss.qpq.gajumaru.core.tools.Grids +import swiss.qpq.gajumaru.core.tools.NodeClient +import swiss.qpq.gajumaru.core.tools.CryptoUtils +import swiss.qpq.gajumaru.core.tools.TransactionService +import java.math.BigInteger import java.util.UUID class MainActivity : FragmentActivity() { @@ -108,7 +117,8 @@ enum class Screen { VIEW_MNEMONIC, VERIFY_MNEMONIC, ENVIRONMENT_INFO, - SEND_FORM + SEND_FORM, + SEND_REVIEW } @Composable @@ -119,6 +129,7 @@ fun GajuRouter( ) { val context = LocalContext.current val appState by appStateManager.state.collectAsState() + val scope = rememberCoroutineScope() val initialScreen = remember { if (walletRepo.listWallets().isEmpty()) Screen.CREATE_WALLET else Screen.DASHBOARD @@ -161,7 +172,17 @@ fun GajuRouter( var activeWalletIndex by rememberSaveable { mutableIntStateOf(0) } var activeAccountIndex by rememberSaveable { mutableIntStateOf(0) } + var nodeStatusResult by remember { mutableStateOf?>(null) } + var nodeStatusError by remember { mutableStateOf(null) } + var pendingScanResult by remember { mutableStateOf(null) } + + var pendingRecipient by rememberSaveable { mutableStateOf("") } + var pendingAmountStr by rememberSaveable { mutableStateOf("") } + var pendingPayload by rememberSaveable { mutableStateOf("") } + var pendingGas by rememberSaveable { mutableStateOf("") } + var pendingGasPrice by rememberSaveable { mutableStateOf("") } + var pendingTTL by rememberSaveable { mutableStateOf("") } when (currentScreen) { Screen.CREATE_WALLET -> { @@ -324,6 +345,16 @@ fun GajuRouter( selectedAccountId = accountId navigateTo(Screen.MANAGE_ACCOUNT) }, + onRefresh = { walletId, accountId -> + scope.launch { + val updated = walletRepo.refreshAccount(walletId, accountId) + if (updated != null) { + appStateManager.notifyWalletsChanged() + } else { + onError("Refresh failed. Check network endpoints.") + } + } + }, onNavigate = { dest -> val screen = when (dest) { "settings" -> Screen.SETTINGS @@ -369,6 +400,85 @@ fun GajuRouter( appStateManager.notifyWalletsChanged() } }, + onAddEndpoint = { walletId, host, port, useTls -> + scope.launch { + try { + val wallet = walletRepo.loadWallet(walletId) ?: return@launch + val endpoint = swiss.qpq.gajumobile.data.models.Endpoint(host, port, useTls) + val client = NodeClient(listOf(NodeClient.Endpoint(host, port, useTls))) + + val status = withContext(Dispatchers.IO) { client.status() } + val reportedNetworkId = status["network_id"] as? String + + if ((reportedNetworkId != null && reportedNetworkId != wallet.networkId)) { + val allWallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } + val targetWallet = allWallets.find { it.networkId == reportedNetworkId } + + if (targetWallet != null) { + val targetEndpoints = targetWallet.endpoints + endpoint + walletRepo.saveWallet(targetWallet.copy(endpoints = targetEndpoints)) + appStateManager.notifyWalletsChanged() + onError("Network mismatch: Node reports '$reportedNetworkId'. Added to wallet '${targetWallet.name}'.") + } else { + nodeStatusError = "CRITICAL MISMATCH: Node reports '$reportedNetworkId' but wallet is '${wallet.networkId}'. Please create a wallet for '$reportedNetworkId' first." + } + } else { + val newList = wallet.endpoints + endpoint + walletRepo.saveWallet(wallet.copy(endpoints = newList)) + appStateManager.notifyWalletsChanged() + onError("Endpoint verified and added.") + } + } catch (e: Exception) { + // Even if status check fails, we might still want to add it? + // User said: "We should test the endpoints and fall back to HTTP" + // If it fails completely, maybe don't add it or warn the user. + nodeStatusError = "Failed to verify endpoint: ${e.message}. It has not been added." + } + } + }, + onCheckStatus = { walletId, index -> + scope.launch { + try { + val wallet = walletRepo.loadWallet(walletId) ?: return@launch + val endpoint = wallet.endpoints.getOrNull(index) ?: return@launch + val client = NodeClient(listOf(NodeClient.Endpoint(endpoint.host, endpoint.port, endpoint.useTls))) + + val status = withContext(Dispatchers.IO) { client.status() } + val reportedNetworkId = status["network_id"] as? String + + if ((reportedNetworkId != null && reportedNetworkId != wallet.networkId)) { + // Mismatch logic + val allWallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } + val targetWallet = allWallets.find { it.networkId == reportedNetworkId } + + if (targetWallet != null) { + // Move endpoint + val sourceEndpoints = wallet.endpoints.toMutableList().apply { removeAt(index) } + val targetEndpoints = targetWallet.endpoints + endpoint + + walletRepo.saveWallet(wallet.copy(endpoints = sourceEndpoints)) + walletRepo.saveWallet(targetWallet.copy(endpoints = targetEndpoints)) + + appStateManager.notifyWalletsChanged() + onError("Network mismatch: Node reports '$reportedNetworkId'. Moved to wallet '${targetWallet.name}'.") + } else { + // Warn for discard + nodeStatusError = "CRITICAL MISMATCH: Node reports '$reportedNetworkId' but wallet is configured for '${wallet.networkId}'. Fix this endpoint or it will be discarded from this wallet." + } + } else { + nodeStatusResult = status + } + } catch (e: Exception) { + nodeStatusError = e.message ?: "Unknown error" + } + } + }, + statusResult = nodeStatusResult, + statusError = nodeStatusError, + onClearStatus = { + nodeStatusResult = null + nodeStatusError = null + }, onBack = { navigateBack() }, ) } @@ -491,17 +601,84 @@ fun GajuRouter( 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}") - } + pendingRecipient = recipient + pendingAmountStr = amountStr + pendingPayload = payload + pendingGas = gas + pendingGasPrice = gasPrice + pendingTTL = ttl + navigateTo(Screen.SEND_REVIEW) }, onBack = { navigateBack() }, ) } + Screen.SEND_REVIEW -> { + 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 + } + + SendReviewScreen( + senderLabel = activeAccount.label, + recipientLabel = "Recipient", + recipientId = pendingRecipient, + amountStr = pendingAmountStr, + onSend = { + scope.launch { + try { + // 1. Get next nonce and current height + val (nonce, height) = withContext(Dispatchers.IO) { + val client = NodeClient(activeWallet.endpoints.map { NodeClient.Endpoint(it.host, it.port) }) + client.nextNonce(activeAccount.gajuId) to client.topHeight() + } + + // 2. Derive seckey from airlock + val masterKey = KeyManager.getMasterKey(context) + val seckey = AccountAirlock.derivePrivateKey(activeAccount.privateKeyEnvelope, masterKey) + + // 3. Build and sign tx + val amountPucks = BigInteger(GajuFormat.read(pendingAmountStr)) + val signedTx = TransactionService.buildSpendTx( + activeWallet.networkId, + activeAccount.gajuId, + pendingRecipient, + amountPucks, + BigInteger(pendingGasPrice), + BigInteger(pendingGas), + height + pendingTTL.toLong(), + nonce, + pendingPayload, + seckey + ) + + // 4. Wipe seckey + CryptoUtils.wipe(seckey) + + // 5. Post tx + val result = withContext(Dispatchers.IO) { + val client = NodeClient(activeWallet.endpoints.map { NodeClient.Endpoint(it.host, it.port) }) + client.postTx(signedTx) + } + + val hash = result["hash"] as? String ?: "Unknown" + onError("Transaction posted! Hash: $hash") + + // Success! Go back to dashboard + screenStack.clear() + screenStack.add(Screen.DASHBOARD) + } catch (e: Exception) { + onError("Send failed: ${e.message}") + } + } + }, + onBack = { navigateBack() } + ) + } Screen.QR_RECEIVE -> { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val activeWallet = wallets.getOrNull(activeWalletIndex) diff --git a/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt b/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt index 815b32a..7423c35 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/data/WalletRepository.kt @@ -7,7 +7,11 @@ import swiss.qpq.gajumobile.data.models.Wallet import swiss.qpq.gajumobile.security.AccountAirlock import swiss.qpq.gajumobile.security.EncryptionService import swiss.qpq.gajumobile.security.KeyManager +import swiss.qpq.gajumaru.core.tools.NodeClient +import swiss.qpq.gajumobile.data.models.CachedBalance import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class WalletRepository(private val context: Context) { @@ -59,6 +63,60 @@ class WalletRepository(private val context: Context) { return AccountAirlock.sign(account.privateKeyEnvelope, masterKey, message) } + suspend fun refreshAccount(walletId: String, accountId: String): Account? { + val wallet = loadWallet(walletId) ?: return null + val account = wallet.accounts.find { it.id == accountId } ?: return null + + val client = NodeClient(wallet.endpoints.map { NodeClient.Endpoint(it.host, it.port, it.useTls) }) + return try { + val accountInfo = withContext(Dispatchers.IO) { + client.account(account.gajuId) + } + + val balance = accountInfo["balance"]?.toString() ?: "0" + val nonce = (accountInfo["nonce"] as? Number)?.toLong() ?: 0L + + val updatedAccount = account.copy( + cachedBalance = CachedBalance( + totalPucks = balance, + chainId = wallet.networkId, + timestamp = System.currentTimeMillis() + ) + ) + + val updatedAccounts = wallet.accounts.map { + if (it.id == account.id) updatedAccount else it + } + saveWallet(wallet.copy(accounts = updatedAccounts)) + updatedAccount + } catch (e: Exception) { + android.util.Log.e("WalletRepository", "Failed to refresh account", e) + null + } + } + + suspend fun getStatus(wallet: Wallet): Map? { + val client = NodeClient(wallet.endpoints.map { NodeClient.Endpoint(it.host, it.port, it.useTls) }) + return try { + withContext(Dispatchers.IO) { + client.status() + } + } catch (e: Exception) { + null + } + } + + suspend fun postTransaction(wallet: Wallet, signedTx: String): Map? { + val client = NodeClient(wallet.endpoints.map { NodeClient.Endpoint(it.host, it.port, it.useTls) }) + return try { + withContext(Dispatchers.IO) { + client.postTx(signedTx) + } + } catch (e: Exception) { + null + } + } + fun listWallets(): List { val dir = File(context.filesDir, "wallets") if (!dir.exists()) return emptyList() 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 d80229f..488f2c0 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 @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable data class Endpoint( val host: String, val port: Int = 3013, + val useTls: Boolean = false, ) @Serializable @@ -19,13 +20,13 @@ data class Wallet( ) { companion object { val DEFAULT_MAINNET = listOf( - Endpoint("groot.mainnet.gajumaru.io", 3013), - Endpoint("tsuriai.jp", 3013) + Endpoint("groot.mainnet.gajumaru.io", 3013, useTls = true), + Endpoint("tsuriai.jp", 3013, useTls = true) ) val DEFAULT_TESTNET = listOf( - Endpoint("groot.testnet.gajumaru.io", 3013), - Endpoint("tsuriai.jp", 4013) + Endpoint("groot.testnet.gajumaru.io", 3013, useTls = true), + Endpoint("tsuriai.jp", 4013, useTls = true) ) } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java index 6fda690..105277c 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java +++ b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java @@ -164,6 +164,33 @@ public final class AccountAirlock { } } + /** + * Decrypts an account's private key (seed). + * WARNING: Result must be wiped with CryptoUtils.wipe() immediately after use. + */ + public static byte[] derivePrivateKey( + EncryptedEnvelope envelope, + SecretKey masterKey + ) throws Exception { + byte[] plaintextDek = null; + try { + plaintextDek = Vault.decrypt( + masterKey, + envelope.getDekIv(), + envelope.getEncryptedDek() + ); + SecretKey dek = new SecretKeySpec(plaintextDek, "AES"); + + return Vault.decrypt( + dek, + envelope.getDataIv(), + envelope.getEncryptedData() + ); + } finally { + if (plaintextDek != null) CryptoUtils.wipe(plaintextDek); + } + } + /** * Decrypts an account's private key (seed) and derives the mnemonic. */ diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt index e23d32a..f287430 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/DashboardScreen.kt @@ -60,6 +60,7 @@ fun DashboardScreen( onReceive: () -> Unit, onAddAccount: (String) -> Unit, onAccountSettings: (String, String) -> Unit, + onRefresh: (String, String) -> Unit, onNavigate: (String) -> Unit, ) { if (wallets.isEmpty()) { @@ -240,18 +241,34 @@ fun DashboardScreen( verticalAlignment = Alignment.CenterVertically, ) { Text("BALANCE", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp) - 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), - ) + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton( + onClick = { + currentAccount?.let { onRefresh(currentWallet.id, it.id) } + }, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_circle), // Reuse an icon for refresh for now + contentDescription = "Refresh Balance", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + Spacer(modifier = Modifier.width(8.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), + ) + } } } 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 3307e8d..99e4ccb 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 @@ -30,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -48,6 +49,11 @@ fun ManageWalletsScreen( onRenameWallet: (String, String) -> Unit, onDeleteWallet: (String) -> Unit, onUpdateEndpoints: (String, List) -> Unit, + onAddEndpoint: (String, String, Int, Boolean) -> Unit, + onCheckStatus: (String, Int) -> Unit, + statusResult: Map? = null, + statusError: String? = null, + onClearStatus: () -> Unit, onBack: () -> Unit, ) { var walletToRename by remember { mutableStateOf(null) } @@ -56,6 +62,41 @@ fun ManageWalletsScreen( var walletForNewEndpoint by remember { mutableStateOf(null) } var newHost by remember { mutableStateOf("") } var newPort by remember { mutableStateOf("3013") } + var newUseTls by remember { mutableStateOf(value = false) } + + var selectedEndpoint by remember { mutableStateOf?>(null) } + + if (statusResult != null || statusError != null) { + androidx.compose.ui.window.Dialog(onDismissRequest = onClearStatus) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp, + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text("Node Status", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(16.dp)) + if (statusError != null) { + Text("Error: $statusError", color = MaterialTheme.colorScheme.error) + } else if (statusResult != null) { + val scrollState = rememberScrollState() + Column(modifier = Modifier.height(300.dp).verticalScroll(scrollState)) { + statusResult.forEach { (k, v) -> + Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { + Text("$k: ", fontWeight = FontWeight.Bold, fontSize = 12.sp) + Text(v.toString(), fontSize = 12.sp, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace) + } + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + TextButton(onClick = onClearStatus, modifier = Modifier.align(Alignment.End)) { + Text("CLOSE") + } + } + } + } + } if (walletToRename != null) { androidx.compose.ui.window.Dialog(onDismissRequest = { walletToRename = null }) { @@ -70,7 +111,7 @@ fun ManageWalletsScreen( GajuTextField( value = newName, onValueChange = { newName = it }, - label = "New Name" + label = "New Name", ) Spacer(modifier = Modifier.height(24.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { @@ -89,6 +130,7 @@ fun ManageWalletsScreen( } if (walletForNewEndpoint != null) { + val placeholderHost = if (walletForNewEndpoint?.networkId == "groot.testnet") "groot.testnet.gajumaru.io" else "groot.mainnet.gajumaru.io" androidx.compose.ui.window.Dialog(onDismissRequest = { walletForNewEndpoint = null }) { Surface( shape = RoundedCornerShape(16.dp), @@ -103,7 +145,7 @@ fun ManageWalletsScreen( onValueChange = { newHost = it }, label = "Host (IP or Domain)", singleLine = true, - placeholder = "groot.gajumaru.io" + placeholder = placeholderHost, ) Spacer(modifier = Modifier.height(16.dp)) GajuTextField( @@ -113,6 +155,15 @@ fun ManageWalletsScreen( singleLine = true, placeholder = "3013" ) + Spacer(modifier = Modifier.height(16.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + androidx.compose.material3.Switch( + checked = newUseTls, + onCheckedChange = { newUseTls = it } + ) + Spacer(Modifier.width(12.dp)) + Text("Use TLS (HTTPS)", style = MaterialTheme.typography.bodyMedium) + } Spacer(modifier = Modifier.height(24.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { TextButton(onClick = { walletForNewEndpoint = null }) { Text("CANCEL") } @@ -122,12 +173,12 @@ fun ManageWalletsScreen( onClick = { walletForNewEndpoint?.let { wallet -> val port = newPort.toIntOrNull() ?: 3013 - val newList = wallet.endpoints + Endpoint(newHost, port) - onUpdateEndpoints(wallet.id, newList) + onAddEndpoint(wallet.id, newHost, port, newUseTls) } walletForNewEndpoint = null newHost = "" newPort = "3013" + newUseTls = false } ) { Text("ADD") } } @@ -144,7 +195,7 @@ fun ManageWalletsScreen( .padding(innerPadding) .padding(horizontal = 24.dp) .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { GajuHeader() Text( @@ -158,6 +209,7 @@ fun ManageWalletsScreen( wallets.forEach { wallet -> WalletManagementCard( wallet = wallet, + selectedEndpointIndex = if (selectedEndpoint?.first == wallet.id) selectedEndpoint?.second else null, onRename = { newName = wallet.name walletToRename = wallet @@ -167,7 +219,20 @@ fun ManageWalletsScreen( onDeleteEndpoint = { index -> val newList = wallet.endpoints.toMutableList().apply { removeAt(index) } onUpdateEndpoints(wallet.id, newList) + if (selectedEndpoint?.first == wallet.id && selectedEndpoint?.second == index) { + selectedEndpoint = null + } }, + onEndpointClick = { index -> + selectedEndpoint = if (selectedEndpoint?.first == wallet.id && selectedEndpoint?.second == index) { + null + } else { + wallet.id to index + } + }, + onCheckStatus = { index -> + onCheckStatus(wallet.id, index) + } ) Spacer(modifier = Modifier.height(16.dp)) } @@ -192,10 +257,13 @@ fun ManageWalletsScreen( @Composable private fun WalletManagementCard( wallet: Wallet, + selectedEndpointIndex: Int?, onRename: () -> Unit, onDelete: () -> Unit, onAddEndpoint: () -> Unit, onDeleteEndpoint: (Int) -> Unit, + onEndpointClick: (Int) -> Unit, + onCheckStatus: (Int) -> Unit, ) { Surface( color = MaterialTheme.colorScheme.surfaceVariant, @@ -242,23 +310,45 @@ private fun WalletManagementCard( 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 + val isSelected = selectedEndpointIndex == index + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.08f) else androidx.compose.ui.graphics.Color.Transparent) + .clickable { onEndpointClick(index) } + .padding(horizontal = 8.dp, vertical = 6.dp) ) { - 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) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = (if (endpoint.useTls) "https://" else "http://") + "${endpoint.host}:${endpoint.port}", + style = MaterialTheme.typography.bodySmall.copy(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace), + color = if (isSelected) MaterialTheme.colorScheme.primary else 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.6f) + ) + } + } + if (isSelected) { + Spacer(Modifier.height(4.dp)) + TextButton( + onClick = { onCheckStatus(index) }, + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 0.dp), + modifier = Modifier.height(28.dp).align(Alignment.Start) + ) { + Icon(painterResource(R.drawable.ic_check_circle), null, modifier = Modifier.size(14.dp)) + Spacer(Modifier.width(6.dp)) + Text("CHECK STATUS", fontSize = 10.sp, fontWeight = FontWeight.Bold) + } } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt index d78784d..9afe5d0 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/SendReviewScreen.kt @@ -37,6 +37,10 @@ import swiss.qpq.gajumobile.ui.components.GajuHeader @Composable fun SendReviewScreen( + senderLabel: String, + recipientLabel: String, + recipientId: String, + amountStr: String, onSend: () -> Unit, onBack: () -> Unit, ) { @@ -76,8 +80,7 @@ fun SendReviewScreen( 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, + enabled = confirmed, ) } } @@ -95,7 +98,7 @@ fun SendReviewScreen( GajuHeader() Text( - text = "SEND", + text = "REVIEW SEND", color = MaterialTheme.colorScheme.onBackground, fontSize = 24.sp, fontWeight = FontWeight.Bold, @@ -103,18 +106,18 @@ fun SendReviewScreen( ) 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) + Text(senderLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) 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(recipientLabel, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) Text( - text = "ak_9jzmTHpVCbXfMSfyvxGV1ZLbzR8YqdK4jpSL5WgUKLGM4XPVp", + text = recipientId, fontSize = 10.sp, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace ) Spacer(modifier = Modifier.height(24.dp)) @@ -126,12 +129,12 @@ fun SendReviewScreen( ) { 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(amountStr, 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) + Text(recipientId.take(12) + "...", 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("THIS ACTION CANNOT BE UNDONE.", fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.error) Text("Are you sure you want to proceed?", fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, color = MaterialTheme.colorScheme.onSurfaceVariant) } } diff --git a/gm-java b/gm-java index 11516b1..20e462e 160000 --- a/gm-java +++ b/gm-java @@ -1 +1 @@ -Subproject commit 11516b1cca5ae9ef7a743e599b50e59af76223e6 +Subproject commit 20e462ec03c8b45b36bad0c11edd57723a10d2f4