diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 813ce66..d20b8a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.App.Starting" + android:screenOrientation="portrait" + android:resizeableActivity="false" android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt index acd515c..366bdb6 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt @@ -42,11 +42,11 @@ import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen import swiss.qpq.gajumobile.ui.screens.DashboardScreen import swiss.qpq.gajumobile.ui.screens.DeleteConfirmationScreen import swiss.qpq.gajumobile.ui.screens.EnvironmentInfoScreen -import swiss.qpq.gajumobile.ui.screens.ExplorerScreen import swiss.qpq.gajumobile.ui.screens.FetchingScreen import swiss.qpq.gajumobile.ui.screens.LockScreen import swiss.qpq.gajumobile.ui.screens.ManageAccountScreen import swiss.qpq.gajumobile.ui.screens.ManageWalletsScreen +import swiss.qpq.gajumobile.ui.screens.ManualGridsEntryScreen import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen import swiss.qpq.gajumobile.ui.screens.MnemonicVerifyScreen import swiss.qpq.gajumobile.ui.screens.QRReceiveScreen @@ -56,6 +56,7 @@ 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.SignRequestScreen +import swiss.qpq.gajumobile.ui.screens.TransactionSuccessScreen import swiss.qpq.gajumobile.ui.screens.ViewMnemonicScreen import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme import swiss.qpq.gajumaru.core.formatting.GajuFormat @@ -120,16 +121,19 @@ enum class Screen { MANAGE_ACCOUNT, MANAGE_WALLETS, DELETE_CONFIRM, - EXPLORER, VIEW_MNEMONIC, VERIFY_MNEMONIC, ENVIRONMENT_INFO, SEND_FORM, SEND_REVIEW, SIGN_REQUEST, - FETCHING_SIGN_REQUEST + FETCHING_SIGN_REQUEST, + MANUAL_GRIDS_ENTRY, + TRANSACTION_SUCCESS } +enum class ScannerMode { SEND, GRIDS } + @Composable fun GajuRouter( context: android.content.Context, @@ -182,6 +186,8 @@ fun GajuRouter( var activeWalletIndex by rememberSaveable { mutableIntStateOf(0) } var activeAccountIndex by rememberSaveable { mutableIntStateOf(0) } + var currentScannerMode by rememberSaveable { mutableStateOf(ScannerMode.SEND) } + var nodeStatusResult by remember { mutableStateOf?>(null) } var nodeStatusError by remember { mutableStateOf(null) } @@ -194,10 +200,67 @@ fun GajuRouter( var pendingGasPrice by rememberSaveable { mutableStateOf("") } var pendingTTL by rememberSaveable { mutableStateOf("") } + var lastTxHash by rememberSaveable { mutableStateOf("") } + var lastTxAmount by rememberSaveable { mutableStateOf("") } + var lastTxRecipient by rememberSaveable { mutableStateOf("") } + var lastTxPayload by rememberSaveable { mutableStateOf("") } + var lastTxNetworkId by rememberSaveable { mutableStateOf("") } + var lastTxSenderLabel by rememberSaveable { mutableStateOf("") } + var lastTxWalletName by rememberSaveable { mutableStateOf("") } + var pendingSignRequest by remember { mutableStateOf(null) } var pendingSignUrl by rememberSaveable { mutableStateOf("") } var activeFetchJob by remember { mutableStateOf(null) } + fun processGridsUrl(url: String) { + try { + val result = Grids.parse(url) + if (result.verb == Grids.Verb.SIGN) { + pendingSignUrl = result.url + navigateTo(Screen.FETCHING_SIGN_REQUEST) + + activeFetchJob = scope.launch { + try { + android.util.Log.i("GajuRouter", "Fetching GRIDS request from: ${result.url}") + @Suppress("UNCHECKED_CAST") + val response = withContext(Dispatchers.IO) { + Http.get(result.url) as? Map + } + if (response != null) { + pendingSignRequest = response.toGridsSignRequest() + // Remove fetching screen and add request screen + screenStack.removeAt(screenStack.size - 1) + navigateTo(Screen.SIGN_REQUEST) + } else { + onError(context.getString(R.string.err_fetch_sig_failed)) + navigateBack() + } + } catch (e: Exception) { + if (activeFetchJob?.isCancelled == false) { + onError(context.getString(R.string.err_fetch_failed, e.message ?: "")) + navigateBack() + } + } finally { + activeFetchJob = null + } + } + } else { + pendingScanResult = result + pendingRecipient = result.recipient ?: "" + pendingAmountStr = 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 "" + pendingPayload = if (result.payload != null) String(result.payload, Charsets.UTF_8) else "" + + navigateTo(Screen.SEND_FORM) + } + } catch (e: Exception) { + onError(context.getString(R.string.err_invalid_grids_url, e.message ?: "")) + } + } + when (currentScreen) { Screen.CREATE_WALLET -> { CreateWalletScreen( @@ -353,8 +416,28 @@ fun GajuRouter( onAccountIndexChange = { activeAccountIndex = it }, balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), - onSend = { navigateTo(Screen.QR_SCANNER) }, - onGrids = { navigateTo(Screen.QR_SCANNER) }, + onSend = { + currentScannerMode = ScannerMode.SEND + pendingRecipient = "" + pendingAmountStr = "" + pendingPayload = "" + pendingGas = "20000" + pendingGasPrice = "1000000000" + pendingTTL = "500" + pendingScanResult = null + navigateTo(Screen.QR_SCANNER) + }, + onGrids = { + currentScannerMode = ScannerMode.GRIDS + pendingRecipient = "" + pendingAmountStr = "" + pendingPayload = "" + pendingGas = "20000" + pendingGasPrice = "1000000000" + pendingTTL = "500" + pendingScanResult = null + navigateTo(Screen.QR_SCANNER) + }, onReceive = { navigateTo(Screen.QR_RECEIVE) }, onAddAccount = { walletId -> selectedWalletId = walletId @@ -378,7 +461,6 @@ fun GajuRouter( onNavigate = { dest -> val screen = when (dest) { "settings" -> Screen.SETTINGS - "transactions" -> Screen.EXPLORER "environment_info" -> Screen.ENVIRONMENT_INFO "manage_wallets" -> Screen.MANAGE_WALLETS "CREATE_WALLET" -> Screen.CREATE_WALLET @@ -573,7 +655,6 @@ fun GajuRouter( } else { val screen = when (dest) { "dashboard" -> Screen.DASHBOARD - "transactions" -> Screen.EXPLORER "environment_info" -> Screen.ENVIRONMENT_INFO "manage_wallets" -> Screen.MANAGE_WALLETS else -> null @@ -598,55 +679,30 @@ fun GajuRouter( } ) } - Screen.QR_SCANNER -> QRScannerScreen( - onScanResult = { url -> - try { - val result = Grids.parse(url) - if (result.verb == Grids.Verb.SIGN) { - pendingSignUrl = result.url - navigateTo(Screen.FETCHING_SIGN_REQUEST) - - activeFetchJob = scope.launch { - try { - android.util.Log.i("GajuRouter", "Fetching GRIDS request from: ${result.url}") - @Suppress("UNCHECKED_CAST") - val response = withContext(Dispatchers.IO) { - Http.get(result.url) as? Map - } - if (response != null) { - pendingSignRequest = response.toGridsSignRequest() - // Remove fetching screen and add request screen - screenStack.removeAt(screenStack.size - 1) - navigateTo(Screen.SIGN_REQUEST) - } else { - onError(context.getString(R.string.err_fetch_sig_failed)) - navigateBack() - } - } catch (e: Exception) { - if (activeFetchJob?.isCancelled == false) { - onError(context.getString(R.string.err_fetch_failed, e.message ?: "")) - navigateBack() - } - } finally { - activeFetchJob = null - } - } + Screen.QR_SCANNER -> { + QRScannerScreen( + onScanResult = { processGridsUrl(it) }, + onManualEntry = { + if (currentScannerMode == ScannerMode.GRIDS) { + navigateTo(Screen.MANUAL_GRIDS_ENTRY) } else { - pendingScanResult = result - navigateTo(Screen.SEND_FORM) + pendingScanResult = null + navigateTo(Screen.SEND_FORM) } - } catch (e: Exception) { - onError(context.getString(R.string.err_invalid_grids_url, e.message ?: "")) - } - }, - onGenerate = { navigateTo(Screen.QR_RECEIVE) }, - onUpload = {}, - onManualEntry = { - pendingScanResult = null - navigateTo(Screen.SEND_FORM) - }, - onBack = { navigateBack() }, - ) + }, + onBack = { navigateBack() }, + ) + } + Screen.MANUAL_GRIDS_ENTRY -> { + ManualGridsEntryScreen( + onConfirm = { url -> + // Remove manual entry screen and process result + screenStack.removeAt(screenStack.size - 1) + processGridsUrl(url) + }, + onBack = { navigateBack() } + ) + } Screen.SEND_FORM -> { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val activeWallet = wallets.getOrNull(activeWalletIndex) @@ -658,22 +714,16 @@ fun GajuRouter( return } - 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, + initialRecipient = pendingRecipient, + initialAmount = pendingAmountStr, + initialPayload = pendingPayload, + initialGas = pendingGas, + initialGasPrice = pendingGasPrice, + initialTTL = pendingTTL, onReview = { recipient, amountStr, payload, gas, gasPrice, ttl -> pendingRecipient = recipient pendingAmountStr = amountStr @@ -699,9 +749,12 @@ fun GajuRouter( SendReviewScreen( senderLabel = activeAccount.label, + walletName = activeWallet.name, + networkId = activeWallet.networkId, recipientLabel = "Recipient", recipientId = pendingRecipient, amountStr = pendingAmountStr, + payload = pendingPayload, onSend = { scope.launch { try { @@ -739,12 +792,31 @@ fun GajuRouter( client.postTx(signedTx) } - val hash = result["hash"] as? String ?: "Unknown" - onError(context.getString(R.string.msg_tx_posted, hash)) + val hash = (result["tx_hash"] ?: result["hash"]) as? String ?: "Unknown" - // Success! Go back to dashboard - screenStack.clear() - screenStack.add(Screen.DASHBOARD) + lastTxHash = hash + lastTxAmount = GajuFormat.amount( + GajuFormat.standardSpec( + GajuFormat.Type.valueOf(appState.balanceFormat), + GajuFormat.Unit.valueOf(appState.balanceUnit) + ), + amountPucks + ) + lastTxRecipient = pendingRecipient + lastTxPayload = pendingPayload + lastTxNetworkId = activeWallet.networkId + lastTxSenderLabel = activeAccount.label + lastTxWalletName = activeWallet.name + + // 6. Schedule a balance refresh in 3 seconds + scope.launch { + kotlinx.coroutines.delay(3000) + walletRepo.refreshAccount(activeWallet.id, activeAccount.id) + appStateManager.notifyWalletsChanged() + } + + // Success! Go to success screen + navigateTo(Screen.TRANSACTION_SUCCESS) } catch (e: Throwable) { val msg = e.message ?: e.toString() onError(context.getString(R.string.err_send_failed, msg)) @@ -853,19 +925,18 @@ fun GajuRouter( 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) - } + Screen.TRANSACTION_SUCCESS -> { + TransactionSuccessScreen( + amount = lastTxAmount, + recipient = lastTxRecipient, + payload = lastTxPayload, + txHash = lastTxHash, + senderLabel = lastTxSenderLabel, + walletName = lastTxWalletName, + networkId = lastTxNetworkId, + onExit = { + screenStack.clear() + screenStack.add(Screen.DASHBOARD) } ) } 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 4474644..c37378e 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 @@ -138,14 +138,6 @@ fun GajuTopBar( }, leadingIcon = { Icon(painterResource(R.drawable.ic_account_box), null, modifier = Modifier.size(18.dp)) } ) - DropdownMenuItem( - text = { Text(stringResource(R.string.menu_transactions)) }, - onClick = { - menuExpanded = false - onNavigate("transactions") - }, - leadingIcon = { Icon(painterResource(R.drawable.gajumobile_icon), null, modifier = Modifier.size(18.dp), tint = Color.Unspecified) } - ) DropdownMenuItem( text = { Text(stringResource(R.string.menu_settings)) }, onClick = { @@ -296,6 +288,7 @@ fun SuccessLayout( content: @Composable ColumnScope.() -> Unit, onProceed: () -> Unit, proceedText: String = stringResource(R.string.proceed_to_dashboard), + requireRiskAcknowledgment: Boolean = true, ) { var checked by remember { mutableStateOf(value = false) } @@ -308,34 +301,37 @@ fun SuccessLayout( .windowInsetsPadding(WindowInsets.navigationBars) .padding(24.dp), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(bottom = 24.dp) - .clickable { checked = !checked }, - ) { - Checkbox( - checked = checked, - onCheckedChange = { checked = it }, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.primary, - uncheckedColor = MaterialTheme.colorScheme.onBackground, - ), - ) - Text( - text = stringResource(R.string.understand_risks_success_label), - color = MaterialTheme.colorScheme.onBackground, - fontSize = 14.sp, - modifier = Modifier.padding(start = 8.dp), - ) + if (requireRiskAcknowledgment) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(bottom = 24.dp) + .clickable { checked = !checked }, + ) { + Checkbox( + checked = checked, + onCheckedChange = { checked = it }, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary, + uncheckedColor = MaterialTheme.colorScheme.onBackground, + ), + ) + Text( + text = stringResource(R.string.understand_risks_success_label), + color = MaterialTheme.colorScheme.onBackground, + fontSize = 14.sp, + modifier = Modifier.padding(start = 8.dp), + ) + } } GajuButton( text = proceedText, onClick = onProceed, - containerColor = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, - contentColor = if (checked) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, + containerColor = if (!requireRiskAcknowledgment || checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + contentColor = if (!requireRiskAcknowledgment || checked) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth(), + enabled = !requireRiskAcknowledgment || checked ) } }, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt index 9145d19..d6e644c 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/AccountNamingScreen.kt @@ -8,10 +8,13 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -57,7 +60,12 @@ fun AccountNamingScreen( } Column( - modifier = Modifier.fillMaxSize().padding(24.dp).safeDrawingPadding(), + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(24.dp) + .safeDrawingPadding() + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { GajuHeader() 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 7b361d8..4262dd3 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 @@ -1,6 +1,8 @@ package swiss.qpq.gajumobile.ui.screens import android.content.ClipData +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,6 +30,7 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -65,6 +68,7 @@ fun DashboardScreen( onRefresh: (String, String) -> Unit, onNavigate: (String) -> Unit, ) { + val context = androidx.compose.ui.platform.LocalContext.current if (wallets.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -107,6 +111,14 @@ fun DashboardScreen( val currentWallet = wallets[walletPagerState.currentPage] val currentAccount = currentWallet.accounts.getOrNull(selectedAccountIndex) + // Automatically refresh balance after 1 second of staying on an account + LaunchedEffect(currentAccount?.id) { + currentAccount?.let { account -> + kotlinx.coroutines.delay(1000) + onRefresh(currentWallet.id, account.id) + } + } + Scaffold( topBar = { GajuTopBar(onNavigate = onNavigate) }, ) { innerPadding -> @@ -249,6 +261,23 @@ fun DashboardScreen( ) } } + + // View on Explorer Link + androidx.compose.material3.TextButton( + onClick = { + val url = "https://${currentWallet.networkId}.gajumaru.io/account/${account.gajuId}" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + }, + modifier = Modifier.padding(bottom = 12.dp) + ) { + Text( + text = stringResource(R.string.view_account_in_explorer), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + } } // Balance Box @@ -315,6 +344,20 @@ fun DashboardScreen( fontSize = 24.sp, fontWeight = FontWeight.Bold, ) + + currentAccount?.cachedBalance?.let { balance -> + val timeStr = remember(balance.timestamp) { + val instant = java.time.Instant.ofEpochMilli(balance.timestamp) + val dateTime = java.time.LocalDateTime.ofInstant(instant, java.time.ZoneId.systemDefault()) + dateTime.format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) + } + Text( + text = stringResource(R.string.last_balance_check_label, timeStr), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.padding(top = 4.dp) + ) + } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt deleted file mode 100644 index 60439e2..0000000 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ExplorerScreen.kt +++ /dev/null @@ -1,60 +0,0 @@ -package swiss.qpq.gajumobile.ui.screens - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import swiss.qpq.gajumobile.R -import swiss.qpq.gajumobile.ui.components.GajuTopBar - -@Composable -fun ExplorerScreen( - onNavigate: (String) -> Unit -) { - Scaffold( - topBar = { GajuTopBar(onNavigate = onNavigate) }, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource(R.string.explorer_title), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(vertical = 16.dp), - ) - - Text(stringResource(R.string.loading_groot), color = MaterialTheme.colorScheme.primary, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) - - Spacer(modifier = Modifier.height(16.dp)) - - Surface( - modifier = Modifier.fillMaxWidth().weight(1f).padding(horizontal = 24.dp, vertical = 8.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(4.dp), - ) { - Box(contentAlignment = Alignment.Center) { - Text(stringResource(R.string.explorer_content_placeholder), color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - } - } -} 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 12e4e94..0e824af 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 @@ -9,6 +9,7 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -194,6 +195,7 @@ fun ManageWalletsScreen( .fillMaxSize() .background(MaterialTheme.colorScheme.background) .padding(innerPadding) + .imePadding() .padding(horizontal = 24.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManualGridsEntryScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManualGridsEntryScreen.kt new file mode 100644 index 0000000..5d04b3c --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/ManualGridsEntryScreen.kt @@ -0,0 +1,82 @@ +package swiss.qpq.gajumobile.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import swiss.qpq.gajumobile.R +import swiss.qpq.gajumobile.ui.components.GajuButton +import swiss.qpq.gajumobile.ui.components.GajuHeader +import swiss.qpq.gajumobile.ui.components.GajuTextField + +@Composable +fun ManualGridsEntryScreen( + onConfirm: (String) -> Unit, + onBack: () -> Unit +) { + var url by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .imePadding() + .padding(24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + GajuHeader() + + Text( + text = "MANUAL GRIDS ENTRY", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 24.dp) + ) + + GajuTextField( + value = url, + onValueChange = { url = it }, + label = "GRIDS URL", + placeholder = "grids://...", + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.weight(1f)) + + GajuButton( + text = "CONFIRM", + onClick = { if (url.isNotBlank()) onConfirm(url) }, + enabled = url.isNotBlank() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + GajuButton( + text = stringResource(id = R.string.back_button_caps), + onClick = onBack, + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) + } +} 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 2a498ed..829b78d 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 @@ -33,14 +33,11 @@ import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions import androidx.compose.ui.res.stringResource 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, onManualEntry: () -> Unit, onBack: () -> Unit, ) { @@ -114,24 +111,26 @@ fun QRScannerScreen( Spacer(modifier = Modifier.weight(0.5f)) - GajuButton( - text = stringResource(id = R.string.manual_entry_button), - onClick = onManualEntry, - modifier = Modifier.padding(horizontal = 48.dp), - ) - - Spacer(modifier = Modifier.weight(1f)) - - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = 48.dp), - horizontalArrangement = Arrangement.SpaceEvenly, + .padding(horizontal = 48.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - ScannerAction(label = stringResource(id = R.string.back_action), icon = R.drawable.ic_home, onClick = onBack) - ScannerAction(label = stringResource(id = R.string.generate_qr_action), icon = R.drawable.ic_qr_code, onClick = onGenerate) - ScannerAction(label = stringResource(id = R.string.upload_qr_action), icon = R.drawable.ic_qr_code, onClick = onUpload) + GajuButton( + text = stringResource(id = R.string.manual_entry_button), + onClick = onManualEntry, + ) + + GajuButton( + text = stringResource(id = R.string.back_button_caps), + onClick = onBack, + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + ) } + + Spacer(modifier = Modifier.weight(1f)) } } } 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 a4e6915..d8403b9 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 @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -47,15 +48,18 @@ fun SendFormScreen( initialRecipient: String = "", initialAmount: String = "", initialPayload: String = "", + initialGas: String = "20000", + initialGasPrice: String = "1000000000", + initialTTL: String = "500", onReview: (recipient: String, amount: String, payload: String, gas: String, gasPrice: String, ttl: String) -> Unit, onBack: () -> Unit, ) { 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 gas by remember { mutableStateOf(value = initialGas) } + var gasPrice by remember { mutableStateOf(value = initialGasPrice) } + var ttl by remember { mutableStateOf(value = initialTTL) } var showAdvanced by remember { mutableStateOf(value = false) } Scaffold( @@ -89,6 +93,7 @@ fun SendFormScreen( .fillMaxSize() .background(MaterialTheme.colorScheme.background) .padding(innerPadding) + .imePadding() .padding(horizontal = 24.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, 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 a13fe38..45f1dec 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 @@ -40,9 +40,12 @@ import swiss.qpq.gajumobile.ui.components.GajuHeader @Composable fun SendReviewScreen( senderLabel: String, + walletName: String, + networkId: String, recipientLabel: String, recipientId: String, amountStr: String, + payload: String = "", onSend: () -> Unit, onBack: () -> Unit, ) { @@ -108,7 +111,16 @@ fun SendReviewScreen( ) Text(stringResource(id = R.string.sending_from_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) - Text(senderLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) + Text( + text = "$senderLabel ($walletName)", + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(id = R.string.on_network_label, networkId), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) Spacer(modifier = Modifier.height(16.dp)) @@ -135,6 +147,12 @@ fun SendReviewScreen( Text(stringResource(id = R.string.payment_summary_to), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(recipientId.take(12) + "...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + if (payload.isNotBlank()) { + Spacer(modifier = Modifier.height(16.dp)) + Text(stringResource(id = R.string.payload_label), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(payload, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) + } + Spacer(modifier = Modifier.height(16.dp)) Text(stringResource(id = R.string.cannot_be_undone_warning), fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.error) Text(stringResource(id = R.string.proceed_confirmation_question), fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, color = MaterialTheme.colorScheme.onSurfaceVariant) diff --git a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt index 518c551..418af1e 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/ui/screens/TransactionSuccessScreen.kt @@ -1,11 +1,16 @@ package swiss.qpq.gajumobile.ui.screens +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -16,26 +21,58 @@ import swiss.qpq.gajumobile.ui.components.SuccessLayout @Composable fun TransactionSuccessScreen( + amount: String, + recipient: String, + payload: String, + txHash: String, + senderLabel: String, + walletName: String, + networkId: String, onExit: () -> Unit, ) { + val context = LocalContext.current SuccessLayout( title = stringResource(R.string.send_title), proceedText = stringResource(R.string.exit_button_caps), onProceed = onExit, + requireRiskAcknowledgment = false, content = { Text(stringResource(R.string.transaction_success_title), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(32.dp)) Text(stringResource(R.string.transaction_success_payment_prefix), fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) - Text("木 22,980.78991", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) + Text(amount, fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.transaction_success_to), fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground) - Text("ak_9jzm...", fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + Text(recipient, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground, maxLines = 1) + if (payload.isNotBlank()) { + Spacer(modifier = Modifier.height(16.dp)) + Text(stringResource(id = R.string.payload_label), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(payload, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) + } + Spacer(modifier = Modifier.height(32.dp)) - Text(stringResource(R.string.transaction_success_subtracted_msg), textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) + Text( + text = stringResource(R.string.transaction_success_subtracted_msg, senderLabel, walletName, networkId), + textAlign = TextAlign.Center, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onBackground + ) Spacer(modifier = Modifier.height(32.dp)) Text(stringResource(R.string.transaction_success_track_hint), fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground) - Text(stringResource(R.string.view_transaction_on_chain), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) + Text( + text = stringResource(R.string.view_transaction_on_chain), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + modifier = Modifier + .clickable { + val url = "https://$networkId.gajumaru.io/tx/$txHash" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } + .padding(8.dp) + ) }, ) } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5207932..0da90d4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -18,7 +18,6 @@ Wallets verwalten - Transaktionen Einstellungen Umgebungsinformationen ZUM ANZEIGEN TIPPEN @@ -173,7 +172,7 @@ Transaktion erfolgreich! Sie haben erfolgreich eine Zahlung gesendet von an - Der Betrag wurde vom TÄGLICHEN Konto in Ihrer PRIMÄREN WALLET abgezogen. + Der Betrag wurde vom %1$s-Konto in Ihrer %2$s-Wallet auf %3$s abgebucht. Sie können diese Transaktion hier on-chain verfolgen. TRANSAKTION ON-CHAIN ANZEIGEN Ich verstehe und übernehme alle Risiken. @@ -246,4 +245,6 @@ LÖSCHEN\nBESTÄTIGEN WALLET KONTO + KONTO IM EXPLORER ANZEIGEN + Letzte Prüfung: %1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f0b9f0d..534866b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -18,7 +18,6 @@ Gérer les portefeuilles - Transactions Paramètres Informations sur l\'environnement APPUYER POUR RÉVÉLER @@ -173,7 +172,7 @@ Transaction réussie ! Vous avez envoyé avec succès un paiement de à - Le montant a été déduit du compte QUOTIDIEN de votre PORTEFEUILLE PRINCIPAL. + Le montant a été déduit du compte %1$s de votre portefeuille %2$s sur %3$s. Vous pouvez suivre cette transaction sur la chaîne ici. VOIR LA TRANSACTION SUR LA CHAÎNE Je comprends et assume pleinement les risques. @@ -246,4 +245,6 @@ CONFIRMER LA\nSUPPRESSION PORTEFEUILLE COMPTE + VOIR LE COMPTE DANS L\'EXPLORATEUR + Dernière vérification: %1$s diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2192e29..dd2c044 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -18,7 +18,6 @@ Gestisci portafogli - Transazioni Impostazioni Informazioni sull\'ambiente TOCCA PER RIVELARE @@ -173,7 +172,7 @@ Transazione riuscita! Hai inviato con successo un pagamento di a - L\'importo è stato detratto dal conto GIORNALIERO nel tuo PORTAFOGLIO PRINCIPALE. + L\'importo è stato detratto dal conto %1$s nel tuo portafoglio %2$s su %3$s. Puoi tracciare questa transazione on-chain qui. VISUALIZZA TRANSAZIONE ON-CHAIN Comprendo e mi assumo pienamente i rischi. @@ -246,4 +245,6 @@ CONFERMA\nELIMINA PORTAFOGLIO ACCOUNT + VISUALIZZA ACCOUNT NELL\'EXPLORER + Ultima verifica: %1$s diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index eda7b7a..3b812d2 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -18,7 +18,6 @@ ウォレットを管理 - トランザクション 設定 環境情報 タップして表示 @@ -107,11 +106,6 @@ メインネット テストネット - - トランザクション - Grootを読み込み中 - Gajuエクスプローラーのコンテンツ - アカウントを作成 アカウントを復元 @@ -173,7 +167,7 @@ トランザクション成功! 以下の送金に成功しました 送信先 - 金額はプライマリウォレットの日常アカウントから差し引かれました。 + 金額は%3$s上の%2$sウォレットの%1$sアカウントから差し引かれました。 ここでチェーン上のトランザクションを追跡できます。 チェーン上でトランザクションを表示 私はリスクを理解し、完全に引き受けます。 @@ -246,4 +240,6 @@ 削除を確定 ウォレット アカウント + エクスプローラーでアカウントを表示 + 最終確認: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c2267c..d3862ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,6 @@ Manage Wallets - Transactions Settings Environment Info TAP TO REVEAL @@ -89,6 +88,7 @@ BALANCE ON %1$s Refresh Balance + Last check at %1$s Account Settings Send GRIDS @@ -107,11 +107,6 @@ Mainnet Testnet - - Transactions - Loading Groot - Gaju Explorer Content - Create Account Recover Account @@ -173,9 +168,10 @@ Transaction Successful! You have successfully sent a payment of to - The amount has been subtracted from the DAILY account in your PRIMARY WALLET. + The amount has been subtracted from the %1$s account in your %2$s wallet on %3$s. You can track this transaction on-chain here. VIEW TRANSACTION ON-CHAIN + VIEW ACCOUNT IN EXPLORER I understand and fully assume the risks.