WIP
This commit is contained in:
@@ -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
Reference in New Issue
Block a user