This commit is contained in:
2026-08-23 18:16:30 +09:00
parent 27c6e1c521
commit 9f0114e963
9 changed files with 430 additions and 56 deletions
+2 -1
View File
@@ -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">
<meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="barcode_ui"/>
@@ -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<Map<String, Any?>?>(null) }
var nodeStatusError by remember { mutableStateOf<String?>(null) }
var pendingScanResult by remember { mutableStateOf<Grids.ParseResult?>(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)
@@ -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<String, Any?>? {
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<String, Any?>? {
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<String> {
val dir = File(context.filesDir, "wallets")
if (!dir.exists()) return emptyList()
@@ -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)
)
}
}
@@ -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.
*/
@@ -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),
)
}
}
}
@@ -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<Endpoint>) -> Unit,
onAddEndpoint: (String, String, Int, Boolean) -> Unit,
onCheckStatus: (String, Int) -> Unit,
statusResult: Map<String, Any?>? = null,
statusError: String? = null,
onClearStatus: () -> Unit,
onBack: () -> Unit,
) {
var walletToRename by remember { mutableStateOf<Wallet?>(null) }
@@ -56,6 +62,41 @@ fun ManageWalletsScreen(
var walletForNewEndpoint by remember { mutableStateOf<Wallet?>(null) }
var newHost by remember { mutableStateOf("") }
var newPort by remember { mutableStateOf("3013") }
var newUseTls by remember { mutableStateOf(value = false) }
var selectedEndpoint by remember { mutableStateOf<Pair<String, Int>?>(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)
}
}
}
}
@@ -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)
}
}
+1 -1
Submodule gm-java updated: 11516b1cca...20e462ec03