fix: tapping on sync completed notification navigates to history
This commit is contained in:
@@ -38,6 +38,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import cafe.adriel.lyricist.ProvideStrings
|
||||
import com.bitcointxoko.gudariwallet.data.HistoricalSyncStore
|
||||
import com.bitcointxoko.gudariwallet.util.NotificationConstants
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -70,6 +71,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// WalletScreen observes this and navigates to the detail screen.
|
||||
// MutableState so Compose recomposes when it changes.
|
||||
private val pendingPaymentHash = mutableStateOf<String?>(null)
|
||||
val pendingNavigateToHistory = mutableStateOf(false)
|
||||
|
||||
// Holds a payment URI that arrived via deep link / intent (BTCPay, external apps).
|
||||
private val pendingPaymentUri = mutableStateOf<String?>(null)
|
||||
@@ -102,6 +104,10 @@ class MainActivity : AppCompatActivity() {
|
||||
// Read hash from the intent that launched the activity (cold start from notification)
|
||||
pendingPaymentHash.value =
|
||||
intent.getStringExtra(NotificationHelper.EXTRA_PAYMENT_HASH)
|
||||
|
||||
if (intent.getBooleanExtra(NotificationConstants.NAVIGATE_TO_HISTORY, false)) {
|
||||
pendingNavigateToHistory.value = true
|
||||
}
|
||||
// Read payment URI from the intent that launched the activity (cold start from deep link)
|
||||
pendingPaymentUri.value = IntentUtils.extractPaymentUri(intent)
|
||||
|
||||
@@ -132,6 +138,7 @@ class MainActivity : AppCompatActivity() {
|
||||
vm = vm,
|
||||
nfcVm = nfcVm,
|
||||
pendingPaymentHash = pendingPaymentHash,
|
||||
pendingNavigateToHistory = pendingNavigateToHistory,
|
||||
pendingPaymentUri = pendingPaymentUri,
|
||||
lyricist = lyricist
|
||||
)
|
||||
@@ -187,6 +194,10 @@ class MainActivity : AppCompatActivity() {
|
||||
pendingPaymentHash.value = hash
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra(NotificationConstants.NAVIGATE_TO_HISTORY, false)) {
|
||||
pendingNavigateToHistory.value = true
|
||||
}
|
||||
|
||||
// Deep link / external app URI
|
||||
val uri = IntentUtils.extractPaymentUri(intent)
|
||||
if (uri != null) {
|
||||
|
||||
@@ -87,6 +87,7 @@ fun WalletScreen(
|
||||
vm : WalletViewModel,
|
||||
nfcVm : NfcViewModel,
|
||||
pendingPaymentHash: MutableState<String?>,
|
||||
pendingNavigateToHistory : MutableState<Boolean>,
|
||||
pendingPaymentUri : MutableState<String?>,
|
||||
lyricist : Lyricist<AppStrings>
|
||||
) {
|
||||
@@ -141,6 +142,15 @@ fun WalletScreen(
|
||||
pendingPaymentHash.value = null
|
||||
}
|
||||
}
|
||||
val navigateToHistory = pendingNavigateToHistory.value
|
||||
LaunchedEffect(navigateToHistory) {
|
||||
if (navigateToHistory) {
|
||||
navController.navigate(ROUTE_HISTORY) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
pendingNavigateToHistory.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val activity = context as MainActivity
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BatteryFull
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
@@ -28,7 +30,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -39,9 +40,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
@@ -52,6 +55,7 @@ import androidx.core.net.toUri
|
||||
import androidx.work.WorkInfo
|
||||
import com.bitcointxoko.gudariwallet.i18n.AppStrings
|
||||
import com.bitcointxoko.gudariwallet.sync.HistoricalSyncWorker
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// ── Page 0: Welcome ────────────────────────────────────────────────────────────
|
||||
@Composable
|
||||
@@ -81,7 +85,7 @@ internal fun WelcomePage(strings: AppStrings) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page 1: Server URL ─────────────────────────────────────────────────────────
|
||||
// ── Page 1: Server URL ───────────────────────────────────────────────────────
|
||||
@Composable
|
||||
internal fun ServerUrlPage(
|
||||
strings: AppStrings,
|
||||
@@ -89,9 +93,12 @@ internal fun ServerUrlPage(
|
||||
onBaseUrl: (String) -> Unit,
|
||||
error: String?
|
||||
) {
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
OnboardingPageScaffold(
|
||||
title = strings.onboarding.serverUrl, // ← "Server URL"
|
||||
subtitle = strings.onboarding.serverUrlHint // ← "https://bitcointxoko.org"
|
||||
title = strings.onboarding.serverUrl,
|
||||
subtitle = strings.onboarding.serverUrlHint
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = baseUrl,
|
||||
@@ -101,13 +108,26 @@ internal fun ServerUrlPage(
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
isError = error != null
|
||||
isError = error != null,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
clipboard.getClipEntry()
|
||||
?.clipData?.getItemAt(0)?.text?.toString()
|
||||
?.let { onBaseUrl(it) }}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentPaste,
|
||||
contentDescription = "Paste server URL"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ErrorText(error)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page 2: Invoice Key ────────────────────────────────────────────────────────
|
||||
// ── Page 2: Invoice Key ──────────────────────────────────────────────────────
|
||||
@Composable
|
||||
internal fun InvoiceKeyPage(
|
||||
strings: AppStrings,
|
||||
@@ -115,8 +135,11 @@ internal fun InvoiceKeyPage(
|
||||
onInvoiceKey: (String) -> Unit,
|
||||
error: String?
|
||||
) {
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
OnboardingPageScaffold(
|
||||
title = strings.onboarding.invoiceKey, // ← "Invoice Key (read-only)"
|
||||
title = strings.onboarding.invoiceKey,
|
||||
subtitle = "Used to generate payment requests."
|
||||
) {
|
||||
OutlinedTextField(
|
||||
@@ -127,13 +150,27 @@ internal fun InvoiceKeyPage(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
isError = error != null
|
||||
isError = error != null,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
clipboard.getClipEntry()
|
||||
?.clipData?.getItemAt(0)?.text?.toString()
|
||||
?.let { onInvoiceKey(it) }
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentPaste,
|
||||
contentDescription = "Paste invoice key"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ErrorText(error)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page 3: Admin Key ──────────────────────────────────────────────────────────
|
||||
// ── Page 3: Admin Key ────────────────────────────────────────────────────────
|
||||
@Composable
|
||||
internal fun AdminKeyPage(
|
||||
strings: AppStrings,
|
||||
@@ -143,8 +180,11 @@ internal fun AdminKeyPage(
|
||||
onToggleVis: () -> Unit,
|
||||
error: String?
|
||||
) {
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
OnboardingPageScaffold(
|
||||
title = strings.onboarding.adminKey, // ← "Admin Key (for sending)"
|
||||
title = strings.onboarding.adminKey,
|
||||
subtitle = "Required to send payments."
|
||||
) {
|
||||
OutlinedTextField(
|
||||
@@ -158,6 +198,20 @@ internal fun AdminKeyPage(
|
||||
else PasswordVisualTransformation(),
|
||||
isError = error != null,
|
||||
trailingIcon = {
|
||||
// Two icons: paste + visibility toggle
|
||||
Row {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
clipboard.getClipEntry()
|
||||
?.clipData?.getItemAt(0)?.text?.toString()
|
||||
?.let { onAdminKey(it) }
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentPaste,
|
||||
contentDescription = "Paste admin key"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onToggleVis) {
|
||||
Icon(
|
||||
imageVector = if (adminVisible) Icons.Default.VisibilityOff
|
||||
@@ -167,6 +221,7 @@ internal fun AdminKeyPage(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
ErrorText(error)
|
||||
}
|
||||
@@ -348,7 +403,7 @@ fun SyncHistoryPage(
|
||||
// ── Scheduled (deferred, waiting for delay + network) ─────────────
|
||||
SyncUiState.Scheduled -> {
|
||||
Text(
|
||||
text = "Sync scheduled — will run when your device is online.",
|
||||
text = "Sync scheduled — will run when your device is online. You will get a notification when the sync is done. You can already start using the app.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
|
||||
@@ -64,6 +64,21 @@ fun OnboardingScreen(
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) { error = null }
|
||||
|
||||
var previousPage by remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
val current = pagerState.currentPage
|
||||
if (current > previousPage) {
|
||||
// User moved forward — save whatever the previous page owned
|
||||
when (previousPage) {
|
||||
PAGE_SERVER_URL -> secretStore.saveBaseUrl(baseUrl)
|
||||
PAGE_INVOICE -> secretStore.saveInvoiceKey(invoiceKey)
|
||||
PAGE_ADMIN -> secretStore.saveAdminKey(adminKey)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
previousPage = current
|
||||
}
|
||||
|
||||
fun validateCurrentPage(): Boolean {
|
||||
error = when (pagerState.currentPage) {
|
||||
PAGE_SERVER_URL -> if (baseUrl.isBlank()) strings.onboarding.fieldsRequired else null
|
||||
@@ -83,7 +98,9 @@ fun OnboardingScreen(
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f),
|
||||
userScrollEnabled = true
|
||||
userScrollEnabled = pagerState.currentPage !in listOf(
|
||||
PAGE_SERVER_URL, PAGE_INVOICE, PAGE_ADMIN
|
||||
)
|
||||
) { page ->
|
||||
when (page) {
|
||||
PAGE_WELCOME -> WelcomePage(strings)
|
||||
@@ -152,6 +169,7 @@ fun OnboardingScreen(
|
||||
val isLastPage = pagerState.currentPage == PAGE_BATTERY
|
||||
|
||||
Button(onClick = {
|
||||
if (!validateCurrentPage()) return@Button
|
||||
scope.launch {
|
||||
// Save each field as soon as the user advances past it
|
||||
when (pagerState.currentPage) {
|
||||
@@ -163,7 +181,7 @@ fun OnboardingScreen(
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
}) {
|
||||
Text(if (isLastPage) strings.onboarding.next else strings.onboarding.next)
|
||||
Text(strings.onboarding.next)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ object NotificationConstants {
|
||||
const val CHANNEL_PAYMENTS = "gudari_payments"
|
||||
const val CHANNEL_SERVICE_STATUS = "gudari_service"
|
||||
const val CHANNEL_SYNC = "gudari_sync"
|
||||
const val NAVIGATE_TO_HISTORY = "navigate_to_history"
|
||||
|
||||
// ── Notification IDs ──────────────────────────────────────────────────────
|
||||
// ID 1 is reserved for the foreground service — Android requires a stable,
|
||||
|
||||
@@ -106,8 +106,6 @@ object NotificationHelper {
|
||||
|
||||
/**
|
||||
* Posts a notification when the one-time historical payment sync completes.
|
||||
* No tap destination needed — the user is already in the app or will land
|
||||
* on the main screen via the default MainActivity launch.
|
||||
*/
|
||||
fun notifySyncComplete(context: Context, paymentCount: Int) {
|
||||
val title = context.getString(R.string.notif_sync_complete_title)
|
||||
@@ -120,10 +118,12 @@ object NotificationHelper {
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(syncCompletePendingIntent(context))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ── Foreground service notification ───────────────────────────────────────
|
||||
|
||||
fun buildServiceNotification(context: Context): Notification {
|
||||
@@ -194,6 +194,23 @@ object NotificationHelper {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a PendingIntent that opens MainActivity and signals it to navigate
|
||||
* to the payment history screen via [NotificationConstants.NAVIGATE_TO_HISTORY].
|
||||
*/
|
||||
private fun syncCompletePendingIntent(context: Context): PendingIntent {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(NotificationConstants.NAVIGATE_TO_HISTORY, true)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
NotificationConstants.NOTIF_ID_SYNC_COMPLETE, // stable, unique requestCode
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun notify(context: Context, id: Int, notification: Notification) {
|
||||
runCatching {
|
||||
NotificationManagerCompat.from(context).notify(id, notification)
|
||||
|
||||
Reference in New Issue
Block a user