commit c134d73306daaf95c403011b1e3430ce1c576343 parent b0f076e7bf5ff3582d238ee5e170825bc58f760a Author: Iván Ávalos <avalos@disroot.org> Date: Tue, 27 Jan 2026 15:45:04 +0100 [wallet] move Main*.kt to dedicated main/ subfolder Diffstat:
45 files changed, 1263 insertions(+), 1234 deletions(-)
diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml @@ -53,7 +53,7 @@ android:value="true" /> <activity - android:name=".MainActivity" + android:name=".main.MainActivity" android:exported="true" android:launchMode="singleInstance" android:theme="@style/AppTheme.NoActionBar" diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt b/wallet/src/main/java/net/taler/wallet/HandleUriFragment.kt @@ -48,6 +48,8 @@ import java.net.HttpURLConnection import java.net.URL import java.util.Locale import androidx.core.net.toUri +import net.taler.wallet.main.MainViewModel +import net.taler.wallet.main.TAG class HandleUriFragment: Fragment() { private val model: MainViewModel by activityViewModels() diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -1,367 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2025 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet - -import android.content.Intent -import android.content.Intent.ACTION_VIEW -import android.nfc.NdefMessage -import android.nfc.NfcAdapter -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup.MarginLayoutParams -import android.widget.Toast -import android.widget.Toast.LENGTH_SHORT -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricPrompt -import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS -import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.setupActionBarWithNavController -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.zxing.client.android.Intents.Scan.MIXED_SCAN -import com.google.zxing.client.android.Intents.Scan.SCAN_TYPE -import com.journeyapps.barcodescanner.ScanContract -import com.journeyapps.barcodescanner.ScanOptions -import com.journeyapps.barcodescanner.ScanOptions.QR_CODE -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch -import net.taler.common.EventObserver -import net.taler.lib.android.TalerNfcService -import net.taler.wallet.databinding.ActivityMainBinding -import net.taler.wallet.events.ObservabilityDialog -import net.taler.wallet.transactions.TransactionPeerPullCredit -import net.taler.wallet.transactions.TransactionPeerPushDebit - -class MainActivity : AppCompatActivity(), OnPreferenceStartFragmentCallback { - private val model: MainViewModel by viewModels() - - private lateinit var ui: ActivityMainBinding - private lateinit var nav: NavController - private lateinit var biometricPrompt: BiometricPrompt - private lateinit var promptInfo: BiometricPrompt.PromptInfo - - private val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> - model.unlockWallet() // hack to prevent from locking after scanning QR - if (result == null || result.contents == null) return@registerForActivityResult - if (model.checkScanQrContext(result.contents)) { - handleTalerUri(result.contents, "QR code") - } else { - confirmTalerUri(result.contents, "QR code") - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() - super.onCreate(savedInstanceState) - ui = ActivityMainBinding.inflate(layoutInflater) - setContentView(ui.root) - setupInsets() - setupBiometrics() - - TalerNfcService.startService(this) - - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment - nav = navHostFragment.navController - - setSupportActionBar(ui.toolbar) - setupActionBarWithNavController(nav) - ui.toolbar.setNavigationOnClickListener { - if (onBackPressedDispatcher.hasEnabledCallbacks()) { - onBackPressedDispatcher.onBackPressed() - } else { - nav.navigateUp() - } - } - - model.startWallet() - - // TODO: refactor and unify progress bar handling - // model.showProgressBar.observe(this) { show -> - // ui.content.progressBar.visibility = if (show) VISIBLE else INVISIBLE - // } - - handleIntents(intent) - - // Update devMode in model from Datastore API - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - model.settingsManager.getDevModeEnabled(this@MainActivity).collect { enabled -> - model.setDevMode(enabled) { error -> - showError(error) - } - } - } - } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - model.transactionManager.selectedTransaction.collect { tx -> - TalerNfcService.clearUri(this@MainActivity) - - when (tx) { - is TransactionPeerPushDebit -> tx.talerUri - is TransactionPeerPullCredit -> tx.talerUri - else -> return@collect - }?.let { uri -> - Log.d(TAG, "Transaction ${tx.transactionId} selected with URI $uri") - TalerNfcService.setUri(this@MainActivity, uri) - } - } - } - } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - model.viewMode.collect { tx -> - model.settingsManager.saveViewMode(this@MainActivity, tx) - } - } - } - - model.scanCodeEvent.observe(this, EventObserver { - val scanOptions = ScanOptions().apply { - setPrompt("") - setBeepEnabled(true) - setOrientationLocked(false) - setDesiredBarcodeFormats(QR_CODE) - addExtra(SCAN_TYPE, MIXED_SCAN) - } - if (it) barcodeLauncher.launch(scanOptions) - }) - - model.networkManager.networkStatus.observe(this) { online -> - ui.offlineBanner.visibility = if (online) GONE else VISIBLE - model.hintNetworkAvailability(online) - } - - model.devMode.observe(this) { - invalidateMenu() - } - } - - private fun setupInsets() { - // We really don't want to deal with cutouts! - ViewCompat.setOnApplyWindowInsetsListener(ui.root) { v, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) - v.updateLayoutParams<MarginLayoutParams> { - leftMargin = insets.left - rightMargin = insets.right - } - windowInsets - } - - ViewCompat.setOnApplyWindowInsetsListener(ui.toolbar) { v, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.updateLayoutParams<MarginLayoutParams> { - leftMargin = insets.left - rightMargin = insets.right - } - windowInsets - } - } - - private fun setupBiometrics() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - combine( - model.authenticated, - model.settingsManager.getBiometricLockEnabled(this@MainActivity) - ) { a, b -> a to b }.collect { c -> - val authenticated = c.first - val biometricEnabled = c.second - if (!authenticated && biometricEnabled) { - ui.biometricOverlay.visibility = VISIBLE - biometricPrompt.authenticate(promptInfo) - } else { - ui.biometricOverlay.visibility = GONE - } - } - } - } - - ui.unlockButton.setOnClickListener { - biometricPrompt.authenticate(promptInfo) - } - - biometricPrompt = BiometricPrompt( - this, - ContextCompat.getMainExecutor(this), - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - if (errorCode == ERROR_NO_BIOMETRICS || errorCode == ERROR_NO_DEVICE_CREDENTIAL) { - model.unlockWallet() - } - Toast.makeText(this@MainActivity, getString(R.string.biometric_auth_error, errString), LENGTH_SHORT).show() - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - model.unlockWallet() - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Toast.makeText(this@MainActivity, getString(R.string.biometric_auth_failed), LENGTH_SHORT).show() - } - }, - ) - - promptInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.biometric_prompt_title)) - .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) - .setConfirmationRequired(true) - .build() - } else { - BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.biometric_prompt_title)) - .setDeviceCredentialAllowed(true) - .setConfirmationRequired(true) - .build() - } - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - handleIntents(intent) - } - - private fun handleIntents(intent: Intent?) { - if (intent == null) return - - if (intent.action == ACTION_VIEW) intent.dataString?.let { uri -> - handleTalerUri(uri, "intent") - } - - if (intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED) { - val messages: Array<NdefMessage> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, NdefMessage::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)?.let { rawMessages -> - rawMessages.map { it as NdefMessage } - }?.toTypedArray() - } ?: return - - messages.forEach { message -> - message.records?.forEach { record -> - record.toUri()?.let { uri -> - handleTalerUri(uri.toString(), "nfc") - } - } - } - } - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - if (model.devMode.value == true) { - menuInflater.inflate(R.menu.global_dev, menu) - } - - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_show_logs -> { - ObservabilityDialog().show(supportFragmentManager, "OBSERVABILITY") - } - } - return super.onOptionsItemSelected(item) - } - - private fun confirmTalerUri(uri: String, from: String) { - MaterialAlertDialogBuilder(this).apply { - setTitle(R.string.qr_scan_context_title) - setMessage(when (model.getScanQrContext()) { - ScanQrContext.Send -> R.string.qr_scan_context_send_message - ScanQrContext.Receive -> R.string.qr_scan_context_receive_message - else -> error("invalid value") - }) - - setNegativeButton(R.string.ok) { _, _ -> - handleTalerUri(uri, from) - } - - setNeutralButton(R.string.cancel) { dialog, _ -> - dialog.dismiss() - } - }.show() - } - - private fun handleTalerUri(uri: String, from: String) { - val args = bundleOf("uri" to uri, "from" to from) - nav.navigate(R.id.action_global_handle_uri, args) - } - - override fun onPreferenceStartFragment( - caller: PreferenceFragmentCompat, - pref: Preference, - ): Boolean { - when (pref.key) { - "pref_exchanges" -> nav.navigate(R.id.nav_settings_exchanges) - "pref_accounts" -> nav.navigate(R.id.bankAccountsFragment) - "pref_donau" -> nav.navigate(R.id.nav_settings_donau) - } - return true - } - - override fun onResume() { - super.onResume() - TalerNfcService.setDefaultHandler(this) - } - - override fun onPause() { - super.onPause() - TalerNfcService.unsetDefaultHandler(this) - } - - override fun onStop() { - super.onStop() - model.lockWallet() - } - - override fun onDestroy() { - super.onDestroy() - TalerNfcService.stopService(this) - TalerNfcService.clearUri(this) - model.stopWallet() - } -} diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt @@ -1,477 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2025 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.compose.BackHandler -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LargeFloatingActionButton -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SheetState -import androidx.compose.material3.Text -import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.rememberTooltipState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.fragment.compose.AndroidFragment -import androidx.fragment.compose.FragmentState -import androidx.fragment.compose.rememberFragmentState -import androidx.navigation.fragment.findNavController -import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import net.taler.wallet.balances.BalanceState -import net.taler.wallet.compose.DemandAttention -import net.taler.wallet.compose.GridMenu -import net.taler.wallet.compose.GridMenuItem -import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.main.MainComposable -import net.taler.wallet.main.ViewMode -import net.taler.wallet.settings.SettingsFragment -import net.taler.wallet.transactions.Transaction -import net.taler.wallet.transactions.TransactionMajorState -import net.taler.wallet.transactions.TransactionPayment -import net.taler.wallet.transactions.TransactionState -import net.taler.wallet.transactions.TransactionStateFilter.Nonfinal -import kotlin.math.roundToInt - -class MainFragment: Fragment() { - - enum class Tab { ASSETS, SETTINGS } - - private val model: MainViewModel by activityViewModels() - - @OptIn(ExperimentalMaterial3Api::class) - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = ComposeView(requireContext()).apply { - setContent { - TalerSurface { - var tab by rememberSaveable { mutableStateOf(Tab.ASSETS) } - var showSheet by remember { mutableStateOf(false) } - val sheetState = rememberModalBottomSheetState() - - val settingsFragmentState = rememberFragmentState() - - val context = LocalContext.current - val online by model.networkManager.networkStatus.observeAsState(false) - val balanceState by model.balanceManager.state.observeAsState(BalanceState.None) - val viewMode by model.viewMode.collectAsStateLifecycleAware() - val devMode by model.devMode.observeAsState(false) - val txResult by remember(viewMode) { - val v = viewMode as? ViewMode.Transactions - model.transactionManager.transactionsFlow(v?.selectedScope, stateFilter = v?.stateFilter) - }.collectAsStateLifecycleAware() - val actionButtonUsed by remember { model.settingsManager.getActionButtonUsed(context) }.collectAsStateLifecycleAware(true) - - Scaffold( - bottomBar = { - NavigationBar { - NavigationBarItem( - icon = { Icon(Icons.Default.BarChart, contentDescription = null) }, - label = { Text(stringResource(R.string.assets_title)) }, - selected = tab == Tab.ASSETS, - onClick = { - tab = Tab.ASSETS - if (viewMode !is ViewMode.Assets) - model.showAssets() - } - ) - - TalerActionButton( - demandAttention = !actionButtonUsed, - onShowSheet = { - showSheet = true - model.settingsManager.saveActionButtonUsed(context) - }, - onScanQr = { - onScanQr() - model.settingsManager.saveActionButtonUsed(context) - }, - ) - - NavigationBarItem( - icon = { Icon(Icons.Default.Settings, contentDescription = null) }, - label = { Text(stringResource(R.string.menu_settings)) }, - selected = tab == Tab.SETTINGS, - onClick = { tab = Tab.SETTINGS }, - ) - } - }, - contentWindowInsets = WindowInsets.systemBars.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom - ) - ) { innerPadding -> - LaunchedEffect(Unit) { - val viewMode = model.settingsManager.getViewMode(context).first() - model.setViewMode(viewMode) - } - - LaunchedEffect(tab, viewMode) { - setTitle(tab, viewMode) - } - - BackHandler(viewMode !is ViewMode.Assets) { - model.showAssets() - } - - when (tab) { - Tab.ASSETS -> MainComposable( - innerPadding = innerPadding, - state = balanceState, - txResult = txResult, - viewMode = viewMode, - devMode = devMode, - onGetDemoMoneyClicked = { - model.withdrawManager.withdrawTestBalance() - Snackbar.make( - requireView(), - getString(R.string.settings_test_withdrawal), - LENGTH_LONG - ).show() - }, - onBalanceClicked = { - model.showTransactions(it.scopeInfo) - }, - onPendingClicked = { - model.showTransactions(it.scopeInfo, Nonfinal) - }, - onTransactionClicked = { tx -> - onTransactionClicked(tx) - }, - onTransactionsDelete = { txIds -> - model.transactionManager.deleteTransactions(txIds) { error -> - Toast.makeText(context, error.userFacingMsg, Toast.LENGTH_LONG) - .show() - } - }, - onShowBalancesClicked = { - model.showAssets() - }, - onStatementClicked = { - findNavController().navigate( - R.id.nav_donau_statement, - bundleOf("host" to it), - ) - } - ) - Tab.SETTINGS -> SettingsView( - innerPadding = innerPadding, - settingsFragmentState = settingsFragmentState, - ) - } - } - - val disableActions = remember(balanceState, online) { - !online || (balanceState as? BalanceState.Success)?.balances?.isEmpty() ?: true - } - - val disablePeer = remember(balanceState, viewMode) { - val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope - val balances = (balanceState as? BalanceState.Success)?.balances - ?.filter { !it.disablePeerPayments } - ?: emptyList() - selectedScope?.let { - balances.find { it.scopeInfo == selectedScope } == null - } ?: balances.isEmpty() - } - - TalerActionsModal( - showSheet = showSheet, - sheetState = sheetState, - onDismiss = { showSheet = false }, - disableActions = disableActions, - disablePeer = disablePeer, - onSend = this@MainFragment::onSend, - onReceive = this@MainFragment::onReceive, - onScanQr = this@MainFragment::onScanQr, - onDeposit = this@MainFragment::onDeposit, - onWithdraw = this@MainFragment::onWithdraw, - onEnterUri = this@MainFragment::onEnterUri, - ) - } - } - } - - private fun onTransactionClicked(tx: Transaction) { - val showTxDetails = { - if (tx.detailPageNav != 0) { - model.transactionManager.selectTransaction(tx) - findNavController().navigate(tx.detailPageNav) - } - } - - when (tx.txState) { - // unfinished transactions (dialog) - TransactionState(TransactionMajorState.Dialog) -> when (tx) { - is TransactionPayment -> { - model.paymentManager.preparePay(tx.transactionId) { - findNavController().navigate(R.id.action_global_promptPayment) - } - } - - else -> showTxDetails() - } - - else -> showTxDetails() - } - } - - override fun onStart() { - super.onStart() - model.balanceManager.loadAssets(model.viewMode.value is ViewMode.Assets) - } - - private fun setTitle(tab: Tab, viewMode: ViewMode?) { - (requireActivity() as AppCompatActivity).apply { - supportActionBar?.title = when (tab) { - Tab.ASSETS -> when(viewMode) { - is ViewMode.Assets -> getString(R.string.assets_title) - is ViewMode.Transactions -> getString(R.string.transactions_title) - null -> getString(R.string.loading) - } - - Tab.SETTINGS -> getString(R.string.menu_settings) - } - } - } - - private fun onSend() { - findNavController().navigate(R.id.nav_peer_push) - } - - private fun onReceive() { - findNavController().navigate(R.id.nav_peer_pull) - } - - private fun onDeposit() { - findNavController().navigate(R.id.nav_deposit) - } - - private fun onWithdraw() { - model.withdrawManager.resetWithdrawal() - findNavController().navigate(R.id.promptWithdraw) - } - - private fun onScanQr() { - model.scanCode() - } - - private fun onEnterUri() { - findNavController().navigate(R.id.nav_uri_input) - } -} - -@Composable -fun SettingsView( - innerPadding: PaddingValues, - settingsFragmentState: FragmentState, -) { - AndroidFragment( - SettingsFragment::class.java, - modifier = Modifier.padding(innerPadding), - fragmentState = settingsFragmentState, - ) -} - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun TalerActionButton( - demandAttention: Boolean, - onShowSheet: () -> Unit, - onScanQr: () -> Unit, -) { - val tooltipState = rememberTooltipState() - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { PlainTooltip { Text(stringResource(R.string.actions)) } }, - state = tooltipState, - ) { - val offsetY = remember { Animatable(0f) } - var cancelled by remember { mutableStateOf(false) } - - DemandAttention(demandAttention = demandAttention) { - LargeFloatingActionButton( - modifier = Modifier - .requiredSize(86.dp) - .padding(8.dp) - .offset { IntOffset(0, offsetY.value.roundToInt() / 6) } - .draggable( - orientation = Orientation.Vertical, - state = rememberDraggableState { delta -> - runBlocking { offsetY.snapTo(offsetY.value + delta) } - if (delta > 0) { - cancelled = true - } - }, - onDragStopped = { - offsetY.animateTo(0.0f) - if (!cancelled) { - onScanQr() - } - cancelled = false - }, - ), - shape = CircleShape, - onClick = { onShowSheet() }, - ) { - if (offsetY.value == 0.0f) { - Icon( - painterResource(R.drawable.ic_actions), - modifier = Modifier.size(38.dp), - contentDescription = stringResource(R.string.actions), - ) - } else { - Icon( - painterResource(R.drawable.ic_scan_qr), - contentDescription = stringResource(R.string.actions), - ) - } - } - } - } - - LaunchedEffect(demandAttention) { - if (demandAttention) { - tooltipState.show() - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TalerActionsModal( - showSheet: Boolean, - sheetState: SheetState, - disableActions: Boolean, - disablePeer: Boolean, - onDismiss: () -> Unit, - onSend: () -> Unit, - onReceive: () -> Unit, - onScanQr: () -> Unit, - onDeposit: () -> Unit, - onWithdraw: () -> Unit, - onEnterUri: () -> Unit, -) { - if (showSheet) { - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - ) { - GridMenu( - contentPadding = PaddingValues( - start = 8.dp, - end = 8.dp, - bottom = 16.dp + WindowInsets - .systemBars - .asPaddingValues() - .calculateBottomPadding(), - ), - ) { - GridMenuItem( - icon = R.drawable.ic_link, - title = R.string.enter_uri, - onClick = { onEnterUri(); onDismiss() }, - ) - - GridMenuItem( - icon = R.drawable.transaction_deposit, - title = R.string.send_deposit_button_label, - onClick = { onDeposit(); onDismiss() }, - enabled = !disableActions - ) - - GridMenuItem( - icon = R.drawable.ic_scan_qr, - title = R.string.button_scan_qr_code_label, - onClick = { onScanQr(); onDismiss() }, - ) - - GridMenuItem( - icon = R.drawable.transaction_p2p_incoming, - title = R.string.transactions_receive_funds, - onClick = { onReceive(); onDismiss() }, - enabled = !disableActions && !disablePeer, - ) - - GridMenuItem( - icon = R.drawable.transaction_withdrawal, - title = R.string.withdraw_button_label, - onClick = { onWithdraw(); onDismiss() }, - enabled = !disableActions, - ) - - GridMenuItem( - icon = R.drawable.transaction_p2p_outgoing, - title = R.string.transactions_send_funds, - onClick = { onSend(); onDismiss() }, - enabled = !disableActions && !disablePeer, - ) - } - } - } -} -\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -1,338 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2025 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -package net.taler.wallet - -import android.app.Application -import android.util.Log -import androidx.annotation.UiThread -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.getAndUpdate -import kotlinx.coroutines.launch -import net.taler.common.Amount -import net.taler.common.AmountParserException -import net.taler.common.Event -import net.taler.common.toEvent -import net.taler.wallet.accounts.AccountManager -import net.taler.wallet.backend.BackendManager -import net.taler.wallet.backend.NotificationPayload -import net.taler.wallet.backend.NotificationReceiver -import net.taler.wallet.backend.TalerErrorInfo -import net.taler.wallet.backend.VersionReceiver -import net.taler.wallet.backend.WalletBackendApi -import net.taler.wallet.backend.WalletCoreVersion -import net.taler.wallet.backend.WalletRunConfig -import net.taler.wallet.backend.WalletRunConfig.Features -import net.taler.wallet.backend.WalletRunConfig.Testing -import net.taler.wallet.balances.BalanceManager -import net.taler.wallet.balances.ScopeInfo -import net.taler.wallet.deposit.DepositManager -import net.taler.wallet.events.ObservabilityEvent -import net.taler.wallet.exchanges.ExchangeManager -import net.taler.wallet.payment.PaymentManager -import net.taler.wallet.peer.PeerManager -import net.taler.wallet.refund.RefundManager -import net.taler.wallet.settings.SettingsManager -import net.taler.wallet.transactions.TransactionManager -import net.taler.wallet.transactions.TransactionStateFilter -import net.taler.wallet.withdraw.WithdrawManager -import androidx.core.net.toUri -import net.taler.wallet.donau.DonauManager -import net.taler.wallet.main.ViewMode - -const val TAG = "taler-wallet" -const val OBSERVABILITY_LIMIT = 100 - -private val transactionNotifications = listOf( - "transaction-state-transition", -) - -private val observabilityNotifications = listOf( - "task-observability-event", - "request-observability-event", -) - -private val sendUriActions = listOf( - "pay", - "tip", - "pay-pull", - "pay-template", -) - -private val receiveUriActions = listOf( - "withdraw", - "refund", - "pay-push", -) - -class MainViewModel( - app: Application, -) : AndroidViewModel(app), VersionReceiver, NotificationReceiver { - - private val mDevMode = MutableLiveData(BuildConfig.DEBUG) - val devMode: LiveData<Boolean> = mDevMode - - val showProgressBar = MutableLiveData<Boolean>() - var walletVersion: String? = null - private set - var walletVersionHash: String? = null - private set - var exchangeVersion: String? = null - private set - var merchantVersion: String? = null - private set - - @set:Synchronized - private var walletConfig = WalletRunConfig( - testing = Testing( - emitObservabilityEvents = true, - devModeActive = devMode.value == true, - ), - features = Features( - enableV1Contracts = true, - ), - logLevel = if (devMode.value == true) "TRACE" else "INFO", - ) - - private val api = WalletBackendApi(app, walletConfig, this, this) - - val networkManager = NetworkManager(app.applicationContext) - val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) - val balanceManager = BalanceManager(api, viewModelScope, exchangeManager) - val paymentManager = PaymentManager(api, viewModelScope, exchangeManager) - val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) - val refundManager = RefundManager(api, viewModelScope) - val withdrawManager = WithdrawManager(api, viewModelScope, exchangeManager, transactionManager) - val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope) - val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope, balanceManager) - val accountManager: AccountManager = AccountManager(api, viewModelScope) - val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager) - val donauManager: DonauManager = DonauManager(api, viewModelScope, exchangeManager) - - private val mAuthenticated = MutableStateFlow(false) - val authenticated: StateFlow<Boolean> = mAuthenticated - - private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>() - val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent - - private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList()) - val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog - - private val mScanCodeEvent = MutableLiveData<Event<Boolean>>() - val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent - - private val mViewMode = MutableStateFlow<ViewMode>(ViewMode.Assets) - val viewMode: StateFlow<ViewMode> = mViewMode - - @set:Synchronized - private var scanQrContext = ScanQrContext.Unknown - - fun startWallet() { - api.startWallet() - } - - fun stopWallet() { - api.stopWallet() - } - - override fun onVersionReceived(versionInfo: WalletCoreVersion) { - walletVersion = versionInfo.implementationSemver - walletVersionHash = versionInfo.implementationGitHash - exchangeVersion = versionInfo.exchange - merchantVersion = versionInfo.merchant - } - - override fun onNotificationReceived(payload: NotificationPayload) { - if (payload.type == "waiting-for-retry") return // ignore ping) - - val str = BackendManager.json.encodeToString(payload) - Log.i(TAG, "Received notification from wallet-core: $str") - - // Only update balances when we're told they changed - if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) { - balanceManager.loadAssets() - } - - if (payload.type in observabilityNotifications && payload.event != null) { - mObservabilityLog.getAndUpdate { logs -> - logs.takeLast(OBSERVABILITY_LIMIT) - .toMutableList().apply { - add(payload.event) - } - } - } - - if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) { - payload.transactionId?.let { id -> - // update currently selected transaction - transactionManager.updateTransactionIfSelected(id) - // update currently selected transaction list - if (payload.type == "transaction-state-transition") { - transactionManager.getTransactionById(id)?.let { tx -> - val v = viewMode.value - if (v is ViewMode.Transactions && v.selectedScope in tx.scopes) { - transactionManager.loadTransactions(v.selectedScope) - } - } - } - } - } - } - - @UiThread - fun lockWallet() { - mAuthenticated.value = false - } - - @UiThread - fun unlockWallet() { - mAuthenticated.value = true - } - - fun setViewMode(v: ViewMode?) = viewModelScope.launch { - mViewMode.value = when(v) { - null -> ViewMode.Assets - is ViewMode.Transactions -> v.copy( - // fill-in currency spec from DB - selectedSpec = exchangeManager.getCurrencySpecification(v.selectedScope), - ) - else -> v - } - } - - fun selectScope(scopeInfo: ScopeInfo?) { - if (scopeInfo != null) { - setViewMode(ViewMode.Transactions(scopeInfo)) - } else { - setViewMode(ViewMode.Assets) - } - } - - fun showAssets() { - if (viewMode.value != ViewMode.Assets) { - selectScope(null) - } - } - - /** - * Navigates to the given scope info's transaction list, when [MainFragment] is shown. - */ - @UiThread - fun showTransactions(scopeInfo: ScopeInfo, stateFilter: TransactionStateFilter? = null) { - mViewMode.value = ViewMode.Transactions(scopeInfo, stateFilter = stateFilter) - } - - @UiThread - fun createAmount(amountText: String, currency: String, incoming: Boolean = false): AmountResult { - val amount = try { - Amount.fromString(currency, amountText) - } catch (e: AmountParserException) { - return AmountResult.InvalidAmount - } - if (incoming || balanceManager.hasSufficientBalance(amount)) return AmountResult.Success(amount) - return AmountResult.InsufficientBalance(amount) - } - - @UiThread - fun dangerouslyReset() { - withdrawManager.resetTestWithdrawal() - balanceManager.resetBalances() - } - - @UiThread - fun scanCode(context: ScanQrContext = ScanQrContext.Unknown) { - scanQrContext = context - mScanCodeEvent.value = true.toEvent() - } - - fun getScanQrContext() = scanQrContext - - fun checkScanQrContext(uri: String): Boolean { - val parsed = uri.toUri() - val action = parsed.host - return when (scanQrContext) { - ScanQrContext.Send -> action in sendUriActions - ScanQrContext.Receive -> action in receiveUriActions - else -> true - } - } - - fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) { - mDevMode.postValue(enabled) - viewModelScope.launch { - val config = walletConfig.copy( - testing = walletConfig.testing?.copy( - devModeActive = enabled, - ) ?: Testing( - devModeActive = enabled, - ), - logLevel = if (enabled) "TRACE" else "INFO", - ) - - api.setWalletConfig(config) - .onSuccess { - walletConfig = config - }.onError(onError) - } - } - - fun hintNetworkAvailability(isAvailable: Boolean) { - viewModelScope.launch { - api.request<Unit>("hintNetworkAvailability") { - put("isNetworkAvailable", isAvailable) - } - } - } - - fun runIntegrationTest(onError: (error: TalerErrorInfo) -> Unit) { - viewModelScope.launch { - api.request<Unit>("runIntegrationTestV2") { - put("amountToWithdraw", "KUDOS:42") - put("amountToSpend", "KUDOS:23") - put("corebankApiBaseUrl", "https://bank.demo.taler.net/") - put("exchangeBaseUrl", "https://exchange.demo.taler.net/") - put("merchantBaseUrl", "https://backend.demo.taler.net/instances/sandbox/") - put("merchantAuthToken", "secret-token:sandbox") - }.onError(onError) - } - } - - fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) { - viewModelScope.launch { - api.request<Unit>("applyDevExperiment") { - put("devExperimentUri", uri) - }.onError(onError) - } - } -} - -enum class ScanQrContext { - Send, - Receive, - Unknown, -} - -sealed class AmountResult { - data class Success(val amount: Amount) : AmountResult() - data class InsufficientBalance(val amount: Amount) : AmountResult() - data object InvalidAmount : AmountResult() -} diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AccountManager.kt b/wallet/src/main/java/net/taler/wallet/accounts/AccountManager.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.accounts.ListBankAccountsResult.Error import net.taler.wallet.accounts.ListBankAccountsResult.None import net.taler.wallet.accounts.ListBankAccountsResult.Success diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AccountsFragment.kt b/wallet/src/main/java/net/taler/wallet/accounts/AccountsFragment.kt @@ -76,7 +76,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.accounts.ListBankAccountsResult.Error import net.taler.wallet.accounts.ListBankAccountsResult.None diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AddAccountFragment.kt b/wallet/src/main/java/net/taler/wallet/accounts/AddAccountFragment.kt @@ -31,7 +31,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface diff --git a/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt b/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt @@ -33,7 +33,7 @@ import kotlinx.serialization.SerializationException import net.taler.common.getDefaultHttpClient import net.taler.common.toHttpMethod import net.taler.qtart.Networking -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import java.io.IOException import java.util.concurrent.ConcurrentHashMap diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable import net.taler.common.Amount import net.taler.common.CurrencySpecification -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.donau.DonauSummaryItem diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt @@ -39,7 +39,7 @@ import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch import net.taler.common.Amount import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt @@ -16,7 +16,6 @@ package net.taler.wallet.deposit -import android.net.Uri import android.util.Log import androidx.annotation.UiThread import kotlinx.coroutines.CoroutineScope @@ -29,7 +28,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import net.taler.common.Amount -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.accounts.KnownBankAccountInfo import net.taler.wallet.accounts.PaytoUriBitcoin import net.taler.wallet.accounts.PaytoUriIban diff --git a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt @@ -49,9 +49,9 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import net.taler.common.Amount import net.taler.common.CurrencySpecification -import net.taler.wallet.AmountResult +import net.taler.wallet.main.AmountResult import net.taler.wallet.BottomInsetsSpacer -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.TalerSurface diff --git a/wallet/src/main/java/net/taler/wallet/donau/DonauManager.kt b/wallet/src/main/java/net/taler/wallet/donau/DonauManager.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.exchanges.ExchangeManager diff --git a/wallet/src/main/java/net/taler/wallet/donau/DonauStatementFragment.kt b/wallet/src/main/java/net/taler/wallet/donau/DonauStatementFragment.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.EmptyComposable import net.taler.wallet.compose.ErrorComposable diff --git a/wallet/src/main/java/net/taler/wallet/donau/SetDonauFragment.kt b/wallet/src/main/java/net/taler/wallet/donau/SetDonauFragment.kt @@ -57,7 +57,7 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import net.taler.common.showError import net.taler.wallet.BottomInsetsSpacer -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.LoadingScreen diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt @@ -50,9 +50,8 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.CopyToClipboardButton import net.taler.wallet.events.ObservabilityDialog.Companion.json diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/AddExchangeDialogFragment.kt @@ -23,7 +23,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt @@ -29,7 +29,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import net.taler.common.Amount import net.taler.common.toRelativeTime import net.taler.common.toShortDate -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentExchangeFeesBinding import net.taler.wallet.exchanges.CoinFeeAdapter.CoinFeeViewHolder diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListFragment.kt @@ -50,7 +50,7 @@ import net.taler.common.EventObserver import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentExchangeListBinding import net.taler.wallet.showError diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -27,11 +27,10 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import net.taler.common.CurrencySpecification import net.taler.common.Event import net.taler.common.toEvent -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi diff --git a/wallet/src/main/java/net/taler/wallet/main/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/main/MainActivity.kt @@ -0,0 +1,369 @@ +/* + * This file is part of GNU Taler + * (C) 2026 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.main + +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.nfc.NdefMessage +import android.nfc.NfcAdapter +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup.MarginLayoutParams +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS +import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.zxing.client.android.Intents.Scan.MIXED_SCAN +import com.google.zxing.client.android.Intents.Scan.SCAN_TYPE +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions +import com.journeyapps.barcodescanner.ScanOptions.QR_CODE +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import net.taler.common.EventObserver +import net.taler.lib.android.TalerNfcService +import net.taler.wallet.R +import net.taler.wallet.databinding.ActivityMainBinding +import net.taler.wallet.events.ObservabilityDialog +import net.taler.wallet.showError +import net.taler.wallet.transactions.TransactionPeerPullCredit +import net.taler.wallet.transactions.TransactionPeerPushDebit + +class MainActivity : AppCompatActivity(), OnPreferenceStartFragmentCallback { + private val model: MainViewModel by viewModels() + + private lateinit var ui: ActivityMainBinding + private lateinit var nav: NavController + private lateinit var biometricPrompt: BiometricPrompt + private lateinit var promptInfo: BiometricPrompt.PromptInfo + + private val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> + model.unlockWallet() // hack to prevent from locking after scanning QR + if (result == null || result.contents == null) return@registerForActivityResult + if (model.checkScanQrContext(result.contents)) { + handleTalerUri(result.contents, "QR code") + } else { + confirmTalerUri(result.contents, "QR code") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + ui = ActivityMainBinding.inflate(layoutInflater) + setContentView(ui.root) + setupInsets() + setupBiometrics() + + TalerNfcService.startService(this) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + nav = navHostFragment.navController + + setSupportActionBar(ui.toolbar) + setupActionBarWithNavController(nav) + ui.toolbar.setNavigationOnClickListener { + if (onBackPressedDispatcher.hasEnabledCallbacks()) { + onBackPressedDispatcher.onBackPressed() + } else { + nav.navigateUp() + } + } + + model.startWallet() + + // TODO: refactor and unify progress bar handling + // model.showProgressBar.observe(this) { show -> + // ui.content.progressBar.visibility = if (show) VISIBLE else INVISIBLE + // } + + handleIntents(intent) + + // Update devMode in model from Datastore API + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.settingsManager.getDevModeEnabled(this@MainActivity).collect { enabled -> + model.setDevMode(enabled) { error -> + showError(error) + } + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.transactionManager.selectedTransaction.collect { tx -> + TalerNfcService.clearUri(this@MainActivity) + + when (tx) { + is TransactionPeerPushDebit -> tx.talerUri + is TransactionPeerPullCredit -> tx.talerUri + else -> return@collect + }?.let { uri -> + Log.d(TAG, "Transaction ${tx.transactionId} selected with URI $uri") + TalerNfcService.setUri(this@MainActivity, uri) + } + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.viewMode.collect { tx -> + model.settingsManager.saveViewMode(this@MainActivity, tx) + } + } + } + + model.scanCodeEvent.observe(this, EventObserver { + val scanOptions = ScanOptions().apply { + setPrompt("") + setBeepEnabled(true) + setOrientationLocked(false) + setDesiredBarcodeFormats(QR_CODE) + addExtra(SCAN_TYPE, MIXED_SCAN) + } + if (it) barcodeLauncher.launch(scanOptions) + }) + + model.networkManager.networkStatus.observe(this) { online -> + ui.offlineBanner.visibility = if (online) GONE else VISIBLE + model.hintNetworkAvailability(online) + } + + model.devMode.observe(this) { + invalidateMenu() + } + } + + private fun setupInsets() { + // We really don't want to deal with cutouts! + ViewCompat.setOnApplyWindowInsetsListener(ui.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + v.updateLayoutParams<MarginLayoutParams> { + leftMargin = insets.left + rightMargin = insets.right + } + windowInsets + } + + ViewCompat.setOnApplyWindowInsetsListener(ui.toolbar) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updateLayoutParams<MarginLayoutParams> { + leftMargin = insets.left + rightMargin = insets.right + } + windowInsets + } + } + + private fun setupBiometrics() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + combine( + model.authenticated, + model.settingsManager.getBiometricLockEnabled(this@MainActivity) + ) { a, b -> a to b }.collect { c -> + val authenticated = c.first + val biometricEnabled = c.second + if (!authenticated && biometricEnabled) { + ui.biometricOverlay.visibility = VISIBLE + biometricPrompt.authenticate(promptInfo) + } else { + ui.biometricOverlay.visibility = GONE + } + } + } + } + + ui.unlockButton.setOnClickListener { + biometricPrompt.authenticate(promptInfo) + } + + biometricPrompt = BiometricPrompt( + this, + ContextCompat.getMainExecutor(this), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + if (errorCode == ERROR_NO_BIOMETRICS || errorCode == ERROR_NO_DEVICE_CREDENTIAL) { + model.unlockWallet() + } + Toast.makeText(this@MainActivity, getString(R.string.biometric_auth_error, errString), LENGTH_SHORT).show() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + model.unlockWallet() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Toast.makeText(this@MainActivity, getString(R.string.biometric_auth_failed), LENGTH_SHORT).show() + } + }, + ) + + promptInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.biometric_prompt_title)) + .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) + .setConfirmationRequired(true) + .build() + } else { + BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.biometric_prompt_title)) + .setDeviceCredentialAllowed(true) + .setConfirmationRequired(true) + .build() + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleIntents(intent) + } + + private fun handleIntents(intent: Intent?) { + if (intent == null) return + + if (intent.action == ACTION_VIEW) intent.dataString?.let { uri -> + handleTalerUri(uri, "intent") + } + + if (intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED) { + val messages: Array<NdefMessage> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, NdefMessage::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)?.let { rawMessages -> + rawMessages.map { it as NdefMessage } + }?.toTypedArray() + } ?: return + + messages.forEach { message -> + message.records?.forEach { record -> + record.toUri()?.let { uri -> + handleTalerUri(uri.toString(), "nfc") + } + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + if (model.devMode.value == true) { + menuInflater.inflate(R.menu.global_dev, menu) + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_show_logs -> { + ObservabilityDialog().show(supportFragmentManager, "OBSERVABILITY") + } + } + return super.onOptionsItemSelected(item) + } + + private fun confirmTalerUri(uri: String, from: String) { + MaterialAlertDialogBuilder(this).apply { + setTitle(R.string.qr_scan_context_title) + setMessage(when (model.getScanQrContext()) { + ScanQrContext.Send -> R.string.qr_scan_context_send_message + ScanQrContext.Receive -> R.string.qr_scan_context_receive_message + else -> error("invalid value") + }) + + setNegativeButton(R.string.ok) { _, _ -> + handleTalerUri(uri, from) + } + + setNeutralButton(R.string.cancel) { dialog, _ -> + dialog.dismiss() + } + }.show() + } + + private fun handleTalerUri(uri: String, from: String) { + val args = bundleOf("uri" to uri, "from" to from) + nav.navigate(R.id.action_global_handle_uri, args) + } + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat, + pref: Preference, + ): Boolean { + when (pref.key) { + "pref_exchanges" -> nav.navigate(R.id.nav_settings_exchanges) + "pref_accounts" -> nav.navigate(R.id.bankAccountsFragment) + "pref_donau" -> nav.navigate(R.id.nav_settings_donau) + } + return true + } + + override fun onResume() { + super.onResume() + TalerNfcService.setDefaultHandler(this) + } + + override fun onPause() { + super.onPause() + TalerNfcService.unsetDefaultHandler(this) + } + + override fun onStop() { + super.onStop() + model.lockWallet() + } + + override fun onDestroy() { + super.onDestroy() + TalerNfcService.stopService(this) + TalerNfcService.clearUri(this) + model.stopWallet() + } +} diff --git a/wallet/src/main/java/net/taler/wallet/main/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/main/MainFragment.kt @@ -0,0 +1,314 @@ +/* + * This file is part of GNU Taler + * (C) 2026 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.main + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.compose.AndroidFragment +import androidx.fragment.compose.FragmentState +import androidx.fragment.compose.rememberFragmentState +import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.flow.first +import net.taler.wallet.R +import net.taler.wallet.balances.BalanceState +import net.taler.wallet.compose.TalerSurface +import net.taler.wallet.compose.collectAsStateLifecycleAware +import net.taler.wallet.settings.SettingsFragment +import net.taler.wallet.transactions.Transaction +import net.taler.wallet.transactions.TransactionMajorState +import net.taler.wallet.transactions.TransactionPayment +import net.taler.wallet.transactions.TransactionState +import net.taler.wallet.transactions.TransactionStateFilter.Nonfinal + +class MainFragment: Fragment() { + + enum class Tab { ASSETS, SETTINGS } + + private val model: MainViewModel by activityViewModels() + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setContent { + TalerSurface { + var tab by rememberSaveable { mutableStateOf(Tab.ASSETS) } + var showSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() + + val settingsFragmentState = rememberFragmentState() + + val context = LocalContext.current + val online by model.networkManager.networkStatus.observeAsState(false) + val balanceState by model.balanceManager.state.observeAsState(BalanceState.None) + val viewMode by model.viewMode.collectAsStateLifecycleAware() + val devMode by model.devMode.observeAsState(false) + val txResult by remember(viewMode) { + val v = viewMode as? ViewMode.Transactions + model.transactionManager.transactionsFlow(v?.selectedScope, stateFilter = v?.stateFilter) + }.collectAsStateLifecycleAware() + val actionButtonUsed by remember { model.settingsManager.getActionButtonUsed(context) }.collectAsStateLifecycleAware(true) + + Scaffold( + bottomBar = { + NavigationBar { + NavigationBarItem( + icon = { Icon(Icons.Default.BarChart, contentDescription = null) }, + label = { Text(stringResource(R.string.assets_title)) }, + selected = tab == Tab.ASSETS, + onClick = { + tab = Tab.ASSETS + if (viewMode !is ViewMode.Assets) + model.showAssets() + } + ) + + TalerActionButton( + demandAttention = !actionButtonUsed, + onShowSheet = { + showSheet = true + model.settingsManager.saveActionButtonUsed(context) + }, + onScanQr = { + onScanQr() + model.settingsManager.saveActionButtonUsed(context) + }, + ) + + NavigationBarItem( + icon = { Icon(Icons.Default.Settings, contentDescription = null) }, + label = { Text(stringResource(R.string.menu_settings)) }, + selected = tab == Tab.SETTINGS, + onClick = { tab = Tab.SETTINGS }, + ) + } + }, + contentWindowInsets = WindowInsets.systemBars.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom + ) + ) { innerPadding -> + LaunchedEffect(Unit) { + val viewMode = model.settingsManager.getViewMode(context).first() + model.setViewMode(viewMode) + } + + LaunchedEffect(tab, viewMode) { + setTitle(tab, viewMode) + } + + BackHandler(viewMode !is ViewMode.Assets) { + model.showAssets() + } + + when (tab) { + Tab.ASSETS -> MainComposable( + innerPadding = innerPadding, + state = balanceState, + txResult = txResult, + viewMode = viewMode, + devMode = devMode, + onGetDemoMoneyClicked = { + model.withdrawManager.withdrawTestBalance() + Snackbar.make( + requireView(), + getString(R.string.settings_test_withdrawal), + LENGTH_LONG + ).show() + }, + onBalanceClicked = { + model.showTransactions(it.scopeInfo) + }, + onPendingClicked = { + model.showTransactions(it.scopeInfo, Nonfinal) + }, + onTransactionClicked = { tx -> + onTransactionClicked(tx) + }, + onTransactionsDelete = { txIds -> + model.transactionManager.deleteTransactions(txIds) { error -> + Toast.makeText(context, error.userFacingMsg, Toast.LENGTH_LONG) + .show() + } + }, + onShowBalancesClicked = { + model.showAssets() + }, + onStatementClicked = { + findNavController().navigate( + R.id.nav_donau_statement, + bundleOf("host" to it), + ) + } + ) + Tab.SETTINGS -> SettingsView( + innerPadding = innerPadding, + settingsFragmentState = settingsFragmentState, + ) + } + } + + val disableActions = remember(balanceState, online) { + !online || (balanceState as? BalanceState.Success)?.balances?.isEmpty() ?: true + } + + val disablePeer = remember(balanceState, viewMode) { + val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope + val balances = (balanceState as? BalanceState.Success)?.balances + ?.filter { !it.disablePeerPayments } + ?: emptyList() + selectedScope?.let { + balances.find { it.scopeInfo == selectedScope } == null + } ?: balances.isEmpty() + } + + TalerActionsModal( + showSheet = showSheet, + sheetState = sheetState, + onDismiss = { showSheet = false }, + disableActions = disableActions, + disablePeer = disablePeer, + onSend = this@MainFragment::onSend, + onReceive = this@MainFragment::onReceive, + onScanQr = this@MainFragment::onScanQr, + onDeposit = this@MainFragment::onDeposit, + onWithdraw = this@MainFragment::onWithdraw, + onEnterUri = this@MainFragment::onEnterUri, + ) + } + } + } + + private fun onTransactionClicked(tx: Transaction) { + val showTxDetails = { + if (tx.detailPageNav != 0) { + model.transactionManager.selectTransaction(tx) + findNavController().navigate(tx.detailPageNav) + } + } + + when (tx.txState) { + // unfinished transactions (dialog) + TransactionState(TransactionMajorState.Dialog) -> when (tx) { + is TransactionPayment -> { + model.paymentManager.preparePay(tx.transactionId) { + findNavController().navigate(R.id.action_global_promptPayment) + } + } + + else -> showTxDetails() + } + + else -> showTxDetails() + } + } + + override fun onStart() { + super.onStart() + model.balanceManager.loadAssets(model.viewMode.value is ViewMode.Assets) + } + + private fun setTitle(tab: Tab, viewMode: ViewMode?) { + (requireActivity() as AppCompatActivity).apply { + supportActionBar?.title = when (tab) { + Tab.ASSETS -> when(viewMode) { + is ViewMode.Assets -> getString(R.string.assets_title) + is ViewMode.Transactions -> getString(R.string.transactions_title) + null -> getString(R.string.loading) + } + + Tab.SETTINGS -> getString(R.string.menu_settings) + } + } + } + + private fun onSend() { + findNavController().navigate(R.id.nav_peer_push) + } + + private fun onReceive() { + findNavController().navigate(R.id.nav_peer_pull) + } + + private fun onDeposit() { + findNavController().navigate(R.id.nav_deposit) + } + + private fun onWithdraw() { + model.withdrawManager.resetWithdrawal() + findNavController().navigate(R.id.promptWithdraw) + } + + private fun onScanQr() { + model.scanCode() + } + + private fun onEnterUri() { + findNavController().navigate(R.id.nav_uri_input) + } +} + +@Composable +fun SettingsView( + innerPadding: PaddingValues, + settingsFragmentState: FragmentState, +) { + AndroidFragment( + SettingsFragment::class.java, + modifier = Modifier.padding(innerPadding), + fragmentState = settingsFragmentState, + ) +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt @@ -0,0 +1,339 @@ +/* + * This file is part of GNU Taler + * (C) 2026 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.main + +import android.app.Application +import android.util.Log +import androidx.annotation.UiThread +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.launch +import net.taler.common.Amount +import net.taler.common.AmountParserException +import net.taler.common.Event +import net.taler.common.toEvent +import net.taler.wallet.accounts.AccountManager +import net.taler.wallet.backend.BackendManager +import net.taler.wallet.backend.NotificationPayload +import net.taler.wallet.backend.NotificationReceiver +import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.backend.VersionReceiver +import net.taler.wallet.backend.WalletBackendApi +import net.taler.wallet.backend.WalletCoreVersion +import net.taler.wallet.backend.WalletRunConfig +import net.taler.wallet.backend.WalletRunConfig.Features +import net.taler.wallet.backend.WalletRunConfig.Testing +import net.taler.wallet.balances.BalanceManager +import net.taler.wallet.balances.ScopeInfo +import net.taler.wallet.deposit.DepositManager +import net.taler.wallet.events.ObservabilityEvent +import net.taler.wallet.exchanges.ExchangeManager +import net.taler.wallet.payment.PaymentManager +import net.taler.wallet.peer.PeerManager +import net.taler.wallet.refund.RefundManager +import net.taler.wallet.settings.SettingsManager +import net.taler.wallet.transactions.TransactionManager +import net.taler.wallet.transactions.TransactionStateFilter +import net.taler.wallet.withdraw.WithdrawManager +import androidx.core.net.toUri +import net.taler.wallet.BuildConfig +import net.taler.wallet.NetworkManager +import net.taler.wallet.donau.DonauManager + +const val TAG = "taler-wallet" +const val OBSERVABILITY_LIMIT = 100 + +private val transactionNotifications = listOf( + "transaction-state-transition", +) + +private val observabilityNotifications = listOf( + "task-observability-event", + "request-observability-event", +) + +private val sendUriActions = listOf( + "pay", + "tip", + "pay-pull", + "pay-template", +) + +private val receiveUriActions = listOf( + "withdraw", + "refund", + "pay-push", +) + +class MainViewModel( + app: Application, +) : AndroidViewModel(app), VersionReceiver, NotificationReceiver { + + private val mDevMode = MutableLiveData(BuildConfig.DEBUG) + val devMode: LiveData<Boolean> = mDevMode + + val showProgressBar = MutableLiveData<Boolean>() + var walletVersion: String? = null + private set + var walletVersionHash: String? = null + private set + var exchangeVersion: String? = null + private set + var merchantVersion: String? = null + private set + + @set:Synchronized + private var walletConfig = WalletRunConfig( + testing = Testing( + emitObservabilityEvents = true, + devModeActive = devMode.value == true, + ), + features = Features( + enableV1Contracts = true, + ), + logLevel = if (devMode.value == true) "TRACE" else "INFO", + ) + + private val api = WalletBackendApi(app, walletConfig, this, this) + + val networkManager = NetworkManager(app.applicationContext) + val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) + val balanceManager = BalanceManager(api, viewModelScope, exchangeManager) + val paymentManager = PaymentManager(api, viewModelScope, exchangeManager) + val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) + val refundManager = RefundManager(api, viewModelScope) + val withdrawManager = WithdrawManager(api, viewModelScope, exchangeManager, transactionManager) + val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope) + val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope, balanceManager) + val accountManager: AccountManager = AccountManager(api, viewModelScope) + val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager) + val donauManager: DonauManager = DonauManager(api, viewModelScope, exchangeManager) + + private val mAuthenticated = MutableStateFlow(false) + val authenticated: StateFlow<Boolean> = mAuthenticated + + private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>() + val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent + + private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList()) + val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog + + private val mScanCodeEvent = MutableLiveData<Event<Boolean>>() + val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent + + private val mViewMode = MutableStateFlow<ViewMode>(ViewMode.Assets) + val viewMode: StateFlow<ViewMode> = mViewMode + + @set:Synchronized + private var scanQrContext = ScanQrContext.Unknown + + fun startWallet() { + api.startWallet() + } + + fun stopWallet() { + api.stopWallet() + } + + override fun onVersionReceived(versionInfo: WalletCoreVersion) { + walletVersion = versionInfo.implementationSemver + walletVersionHash = versionInfo.implementationGitHash + exchangeVersion = versionInfo.exchange + merchantVersion = versionInfo.merchant + } + + override fun onNotificationReceived(payload: NotificationPayload) { + if (payload.type == "waiting-for-retry") return // ignore ping) + + val str = BackendManager.json.encodeToString(payload) + Log.i(TAG, "Received notification from wallet-core: $str") + + // Only update balances when we're told they changed + if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) { + balanceManager.loadAssets() + } + + if (payload.type in observabilityNotifications && payload.event != null) { + mObservabilityLog.getAndUpdate { logs -> + logs.takeLast(OBSERVABILITY_LIMIT) + .toMutableList().apply { + add(payload.event) + } + } + } + + if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) { + payload.transactionId?.let { id -> + // update currently selected transaction + transactionManager.updateTransactionIfSelected(id) + // update currently selected transaction list + if (payload.type == "transaction-state-transition") { + transactionManager.getTransactionById(id)?.let { tx -> + val v = viewMode.value + if (v is ViewMode.Transactions && v.selectedScope in tx.scopes) { + transactionManager.loadTransactions(v.selectedScope) + } + } + } + } + } + } + + @UiThread + fun lockWallet() { + mAuthenticated.value = false + } + + @UiThread + fun unlockWallet() { + mAuthenticated.value = true + } + + fun setViewMode(v: ViewMode?) = viewModelScope.launch { + mViewMode.value = when(v) { + null -> ViewMode.Assets + is ViewMode.Transactions -> v.copy( + // fill-in currency spec from DB + selectedSpec = exchangeManager.getCurrencySpecification(v.selectedScope), + ) + else -> v + } + } + + fun selectScope(scopeInfo: ScopeInfo?) { + if (scopeInfo != null) { + setViewMode(ViewMode.Transactions(scopeInfo)) + } else { + setViewMode(ViewMode.Assets) + } + } + + fun showAssets() { + if (viewMode.value != ViewMode.Assets) { + selectScope(null) + } + } + + /** + * Navigates to the given scope info's transaction list, when [MainFragment] is shown. + */ + @UiThread + fun showTransactions(scopeInfo: ScopeInfo, stateFilter: TransactionStateFilter? = null) { + mViewMode.value = ViewMode.Transactions(scopeInfo, stateFilter = stateFilter) + } + + @UiThread + fun createAmount(amountText: String, currency: String, incoming: Boolean = false): AmountResult { + val amount = try { + Amount.fromString(currency, amountText) + } catch (e: AmountParserException) { + return AmountResult.InvalidAmount + } + if (incoming || balanceManager.hasSufficientBalance(amount)) return AmountResult.Success(amount) + return AmountResult.InsufficientBalance(amount) + } + + @UiThread + fun dangerouslyReset() { + withdrawManager.resetTestWithdrawal() + balanceManager.resetBalances() + } + + @UiThread + fun scanCode(context: ScanQrContext = ScanQrContext.Unknown) { + scanQrContext = context + mScanCodeEvent.value = true.toEvent() + } + + fun getScanQrContext() = scanQrContext + + fun checkScanQrContext(uri: String): Boolean { + val parsed = uri.toUri() + val action = parsed.host + return when (scanQrContext) { + ScanQrContext.Send -> action in sendUriActions + ScanQrContext.Receive -> action in receiveUriActions + else -> true + } + } + + fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) { + mDevMode.postValue(enabled) + viewModelScope.launch { + val config = walletConfig.copy( + testing = walletConfig.testing?.copy( + devModeActive = enabled, + ) ?: Testing( + devModeActive = enabled, + ), + logLevel = if (enabled) "TRACE" else "INFO", + ) + + api.setWalletConfig(config) + .onSuccess { + walletConfig = config + }.onError(onError) + } + } + + fun hintNetworkAvailability(isAvailable: Boolean) { + viewModelScope.launch { + api.request<Unit>("hintNetworkAvailability") { + put("isNetworkAvailable", isAvailable) + } + } + } + + fun runIntegrationTest(onError: (error: TalerErrorInfo) -> Unit) { + viewModelScope.launch { + api.request<Unit>("runIntegrationTestV2") { + put("amountToWithdraw", "KUDOS:42") + put("amountToSpend", "KUDOS:23") + put("corebankApiBaseUrl", "https://bank.demo.taler.net/") + put("exchangeBaseUrl", "https://exchange.demo.taler.net/") + put("merchantBaseUrl", "https://backend.demo.taler.net/instances/sandbox/") + put("merchantAuthToken", "secret-token:sandbox") + }.onError(onError) + } + } + + fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) { + viewModelScope.launch { + api.request<Unit>("applyDevExperiment") { + put("devExperimentUri", uri) + }.onError(onError) + } + } +} + +enum class ScanQrContext { + Send, + Receive, + Unknown, +} + +sealed class AmountResult { + data class Success(val amount: Amount) : AmountResult() + data class InsufficientBalance(val amount: Amount) : AmountResult() + data object InvalidAmount : AmountResult() +} diff --git a/wallet/src/main/java/net/taler/wallet/main/TalerActionButton.kt b/wallet/src/main/java/net/taler/wallet/main/TalerActionButton.kt @@ -0,0 +1,196 @@ +/* + * This file is part of GNU Taler + * (C) 2026 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.main + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFloatingActionButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.runBlocking +import net.taler.wallet.R +import net.taler.wallet.compose.DemandAttention +import net.taler.wallet.compose.GridMenu +import net.taler.wallet.compose.GridMenuItem +import kotlin.math.roundToInt + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun TalerActionButton( + demandAttention: Boolean, + onShowSheet: () -> Unit, + onScanQr: () -> Unit, +) { + val tooltipState = rememberTooltipState() + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { PlainTooltip { Text(stringResource(R.string.actions)) } }, + state = tooltipState, + ) { + val offsetY = remember { Animatable(0f) } + var cancelled by remember { mutableStateOf(false) } + + DemandAttention(demandAttention = demandAttention) { + LargeFloatingActionButton( + modifier = Modifier + .requiredSize(86.dp) + .padding(8.dp) + .offset { IntOffset(0, offsetY.value.roundToInt() / 6) } + .draggable( + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + runBlocking { offsetY.snapTo(offsetY.value + delta) } + if (delta > 0) { + cancelled = true + } + }, + onDragStopped = { + offsetY.animateTo(0.0f) + if (!cancelled) { + onScanQr() + } + cancelled = false + }, + ), + shape = CircleShape, + onClick = { onShowSheet() }, + ) { + if (offsetY.value == 0.0f) { + Icon( + painterResource(R.drawable.ic_actions), + modifier = Modifier.size(38.dp), + contentDescription = stringResource(R.string.actions), + ) + } else { + Icon( + painterResource(R.drawable.ic_scan_qr), + contentDescription = stringResource(R.string.actions), + ) + } + } + } + } + + LaunchedEffect(demandAttention) { + if (demandAttention) { + tooltipState.show() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TalerActionsModal( + showSheet: Boolean, + sheetState: SheetState, + disableActions: Boolean, + disablePeer: Boolean, + onDismiss: () -> Unit, + onSend: () -> Unit, + onReceive: () -> Unit, + onScanQr: () -> Unit, + onDeposit: () -> Unit, + onWithdraw: () -> Unit, + onEnterUri: () -> Unit, +) { + if (showSheet) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + GridMenu( + contentPadding = PaddingValues( + start = 8.dp, + end = 8.dp, + bottom = 16.dp + WindowInsets + .systemBars + .asPaddingValues() + .calculateBottomPadding(), + ), + ) { + GridMenuItem( + icon = R.drawable.ic_link, + title = R.string.enter_uri, + onClick = { onEnterUri(); onDismiss() }, + ) + + GridMenuItem( + icon = R.drawable.transaction_deposit, + title = R.string.send_deposit_button_label, + onClick = { onDeposit(); onDismiss() }, + enabled = !disableActions + ) + + GridMenuItem( + icon = R.drawable.ic_scan_qr, + title = R.string.button_scan_qr_code_label, + onClick = { onScanQr(); onDismiss() }, + ) + + GridMenuItem( + icon = R.drawable.transaction_p2p_incoming, + title = R.string.transactions_receive_funds, + onClick = { onReceive(); onDismiss() }, + enabled = !disableActions && !disablePeer, + ) + + GridMenuItem( + icon = R.drawable.transaction_withdrawal, + title = R.string.withdraw_button_label, + onClick = { onWithdraw(); onDismiss() }, + enabled = !disableActions, + ) + + GridMenuItem( + icon = R.drawable.transaction_p2p_outgoing, + title = R.string.transactions_send_funds, + onClick = { onSend(); onDismiss() }, + enabled = !disableActions && !disablePeer, + ) + } + } + } +} +\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt @@ -32,7 +32,7 @@ import net.taler.common.Amount import net.taler.common.ContractTerms import net.taler.common.CurrencySpecification import net.taler.common.Merchant -import net.taler.wallet.AmountResult +import net.taler.wallet.main.AmountResult import net.taler.wallet.R import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt @@ -27,7 +27,7 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.asFlow import androidx.navigation.fragment.findNavController import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.unit.dp import net.taler.common.Amount import net.taler.common.CurrencySpecification import net.taler.common.RelativeTime -import net.taler.wallet.AmountResult +import net.taler.wallet.main.AmountResult import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.compose.AmountCurrencyField diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -28,7 +28,7 @@ import net.taler.common.ContractInput import net.taler.common.ContractOutput import net.taler.common.ContractTerms import net.taler.common.TalerUtils.getLocalizedString -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt @@ -34,9 +34,9 @@ import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface import net.taler.wallet.showError diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPullPaymentFragment.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingPushPaymentFragment.kt @@ -31,9 +31,9 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.showError diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt @@ -32,7 +32,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.main.ViewMode import net.taler.wallet.compose.AmountScope diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt @@ -32,7 +32,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.main.ViewMode import net.taler.wallet.compose.AmountScope diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -28,7 +28,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import net.taler.common.Amount import net.taler.common.Timestamp -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi @@ -36,7 +36,6 @@ import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.cleanExchange import net.taler.wallet.exchanges.ExchangeItem import net.taler.wallet.exchanges.ExchangeManager -import net.taler.wallet.exchanges.ExchangeTosStatus import net.taler.wallet.payment.InsufficientBalanceHint import org.json.JSONObject import java.util.concurrent.TimeUnit.HOURS diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsFragment.kt @@ -53,7 +53,7 @@ import net.taler.common.showError import net.taler.wallet.BuildConfig.FLAVOR import net.taler.wallet.BuildConfig.VERSION_CODE import net.taler.wallet.BuildConfig.VERSION_NAME -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.showError import net.taler.wallet.withdraw.TestWithdrawStatus diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt @@ -29,9 +29,9 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.launch import net.taler.common.showError -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.launchInAppBrowser import net.taler.wallet.showError import net.taler.wallet.transactions.TransactionAction.Abort diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -25,11 +25,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonPrimitive import net.taler.wallet.PrefsStateFilter -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -41,7 +41,7 @@ import net.taler.common.ContractProduct import net.taler.common.ContractTerms import net.taler.common.Timestamp import net.taler.wallet.R -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo import net.taler.common.CurrencySpecification diff --git a/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsFragment.kt b/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsFragment.kt @@ -30,22 +30,19 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch import net.taler.common.openUri import net.taler.common.shareText -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.transactions.Transaction import net.taler.wallet.transactions.TransactionDeposit import net.taler.wallet.transactions.TransactionMajorState.Done import net.taler.wallet.transactions.TransactionWithdrawal import net.taler.wallet.transactions.WithdrawalDetails import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails -import net.taler.wallet.transfer.ScreenTransfer import net.taler.wallet.withdraw.TransferData class WireTransferDetailsFragment : Fragment() { diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ErrorFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ErrorFragment.kt @@ -26,7 +26,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import net.taler.common.isOnline -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentErrorBinding diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -39,7 +39,7 @@ import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import kotlinx.coroutines.launch import net.taler.common.Amount import net.taler.common.EventObserver -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.main.ViewMode import net.taler.wallet.compose.AmountScope diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ReviewExchangeTosFragment.kt @@ -42,11 +42,10 @@ import io.noties.markwon.Markwon import kotlinx.coroutines.launch import net.taler.common.fadeIn import net.taler.common.fadeOut -import net.taler.wallet.MainViewModel +import net.taler.wallet.main.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentReviewExchangeTosBinding import net.taler.wallet.exchanges.ExchangeTosStatus -import net.taler.wallet.showError import java.text.ParseException import java.util.Locale diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -30,14 +30,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.taler.common.Amount import net.taler.common.Bech32 -import net.taler.wallet.TAG +import net.taler.wallet.main.TAG import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.exchanges.ExchangeFees import net.taler.wallet.exchanges.ExchangeItem import net.taler.wallet.exchanges.ExchangeManager -import net.taler.wallet.exchanges.ExchangeTosStatus import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails import net.taler.wallet.withdraw.WithdrawStatus.Status.* import androidx.core.net.toUri diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml @@ -22,7 +22,7 @@ <fragment android:id="@+id/nav_main" - android:name="net.taler.wallet.MainFragment" + android:name="net.taler.wallet.main.MainFragment" android:label="@string/assets_title"> <action android:id="@+id/action_nav_main_to_nav_uri_input"