diff --git a/README.md b/README.md index 57e8144e9..3ebc817cb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Features: * Displays and sends tokens and NFT * Your secrets are stored password-encrypted or authentication-protected * Show wallet balance, configurable comparison fiat currency +* Cold wallet capable ([more information](https://github.com/ergoplatform/ergo-wallet-app/wiki/Cold-wallet)) +* ErgoPay support You need at least Android 7 or iOS 13 to run Ergo Wallet. @@ -43,6 +45,18 @@ The APK file can be installed on your Android device. If you sideload for the fi * [Android](android/BUILD.md) * [iOS](ios/BUILD.md) +### Translations + +Every translation is welcome! There is a single +[strings file to translate](https://github.com/ergoplatform/ergo-wallet-app/blob/develop/android/src/main/res/values/strings.xml) +to your language. + +Either send me the translated file on Discord or Telegram, or open a PR here. For this, move the +file to a values-xx directory where xx is your language's ISO code. +([Spanish example](https://github.com/ergoplatform/ergo-wallet-app/tree/develop/android/src/main/res/values-es)) + +Thanks in advance! + ### Tip the developer If you want to tip the developer for making this app, thanks in advance! Send your tips to diff --git a/android/build.gradle b/android/build.gradle index 75834ad01..4e2b174e1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -13,8 +13,8 @@ android { applicationId "org.ergoplatform.android" minSdkVersion 24 targetSdkVersion 30 - versionCode 2203 - versionName "1.5.2203" + versionCode 2204 + versionName "1.6.2204" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index c2c53e2a1..b8909401c 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -33,7 +33,8 @@ - + + diff --git a/android/src/main/java/org/ergoplatform/android/MainActivity.kt b/android/src/main/java/org/ergoplatform/android/MainActivity.kt index 4c492f2ae..b3fa2e10c 100644 --- a/android/src/main/java/org/ergoplatform/android/MainActivity.kt +++ b/android/src/main/java/org/ergoplatform/android/MainActivity.kt @@ -10,8 +10,13 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.zxing.integration.android.IntentIntegrator import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent -import org.ergoplatform.isPaymentRequestUrl +import org.ergoplatform.android.transactions.ChooseSpendingWalletFragmentDialog +import org.ergoplatform.android.ui.AndroidStringProvider +import org.ergoplatform.android.ui.postDelayed +import org.ergoplatform.uilogic.MainAppUiLogic class MainActivity : AppCompatActivity() { @@ -41,11 +46,45 @@ class MainActivity : AppCompatActivity() { } } - private fun handleIntent(navController: NavController) { - intent.dataString?.let { - if (isPaymentRequestUrl(it)) { - navController.navigate(R.id.chooseSpendingWalletFragmentDialog) + fun scanQrCode() { + IntentIntegrator(this).initiateScan(setOf(IntentIntegrator.QR_CODE)) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + if (result != null) { + result.contents?.let { + // post on Main thread, otherwise navigate() not working + postDelayed(0) { handleRequests(it, true) } } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + private fun handleRequests( + data: String, + fromQrCode: Boolean, + optNavController: NavController? = null + ) { + val navController = optNavController ?: findNavController(R.id.nav_host_fragment) + + MainAppUiLogic.handleRequests(data, fromQrCode, AndroidStringProvider(this), { + navController.navigate( + R.id.chooseSpendingWalletFragmentDialog, + ChooseSpendingWalletFragmentDialog.buildArgs(it) + ) + }, { + MaterialAlertDialogBuilder(this) + .setMessage(it) + .setPositiveButton(R.string.zxing_button_ok, null) + .show() + }) + } + + private fun handleIntent(navController: NavController? = null) { + intent.dataString?.let { + handleRequests(it, false, navController) } } @@ -55,6 +94,6 @@ class MainActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - handleIntent(findNavController(R.id.nav_host_fragment)) + handleIntent() } } \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt b/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt index 499f19fd3..af9d9dcda 100644 --- a/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt @@ -5,7 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import org.ergoplatform.NodeConnector +import org.ergoplatform.ErgoApiService import org.ergoplatform.android.Preferences import org.ergoplatform.android.databinding.FragmentConnectionSettingsBinding import org.ergoplatform.getDefaultExplorerApiUrl @@ -58,7 +58,7 @@ class ConnectionSettingsDialogFragment : BottomSheetDialogFragment() { preferences.prefNodeUrl = nodeUrl // reset api service of NodeConnector to load new settings - NodeConnector.getInstance().resetApiService() + ErgoApiService.resetApiService() dismiss() } diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt b/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt index 59830f664..3ee68e43d 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt @@ -13,6 +13,7 @@ import org.ergoplatform.android.databinding.FragmentSendFundsWalletChooserBindin import org.ergoplatform.android.databinding.FragmentSendFundsWalletChooserItemBinding import org.ergoplatform.android.ui.FullScreenFragmentDialog import org.ergoplatform.android.ui.navigateSafe +import org.ergoplatform.transactions.isErgoPaySigningRequest import org.ergoplatform.parsePaymentRequest import org.ergoplatform.wallet.getBalanceForAllAddresses @@ -39,18 +40,25 @@ class ChooseSpendingWalletFragmentDialog : FullScreenFragmentDialog() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val query = requireActivity().intent.dataString + val query = arguments?.getString(ARG_QUERY) if (query == null) { dismiss() return } - val content = parsePaymentRequest(query) - binding.receiverAddress.text = content?.address - val amount = content?.amount ?: ErgoAmount.ZERO - binding.grossAmount.amount = amount.toDouble() - binding.grossAmount.visibility = if (amount.nanoErgs > 0) View.VISIBLE else View.GONE + if (isErgoPaySigningRequest(query)) { + binding.grossAmount.visibility = View.GONE + binding.textviewTo.visibility = View.GONE + binding.receiverAddress.visibility = View.GONE + binding.labelTitle.setText(R.string.title_ergo_pay_request) + } else { + val content = parsePaymentRequest(query) + binding.receiverAddress.text = content?.address + val amount = content?.amount ?: ErgoAmount.ZERO + binding.grossAmount.setAmount(amount.toBigDecimal()) + binding.grossAmount.visibility = if (amount.nanoErgs > 0) View.VISIBLE else View.GONE + } AppDatabase.getInstance(requireContext()).walletDao().getWalletsWithStates() .observe(viewLifecycleOwner, { @@ -59,34 +67,41 @@ class ChooseSpendingWalletFragmentDialog : FullScreenFragmentDialog() { if (wallets.size == 1) { // immediately switch to send funds screen - navigateToSendFundsScreen(wallets.first().walletConfig.id, query) + navigateToNextScreen(wallets.first().walletConfig.id, query) } wallets.sortedBy { it.walletConfig.displayName }.forEach { wallet -> val itemBinding = FragmentSendFundsWalletChooserItemBinding.inflate( layoutInflater, binding.listWallets, true ) - itemBinding.walletBalance.amount = - ErgoAmount(wallet.getBalanceForAllAddresses()).toDouble() + itemBinding.walletBalance.setAmount( + ErgoAmount(wallet.getBalanceForAllAddresses()).toBigDecimal() + ) itemBinding.walletName.text = wallet.walletConfig.displayName itemBinding.root.setOnClickListener { - navigateToSendFundsScreen(wallet.walletConfig.id, query) + navigateToNextScreen(wallet.walletConfig.id, query) } } }) } - private fun navigateToSendFundsScreen(walletId: Int, paymentRequest: String) { + private fun navigateToNextScreen(walletId: Int, request: String) { val navBuilder = NavOptions.Builder() val navOptions = navBuilder.setPopUpTo(R.id.chooseSpendingWalletFragmentDialog, true).build() NavHostFragment.findNavController(requireParentFragment()) .navigateSafe( - ChooseSpendingWalletFragmentDialogDirections.actionChooseSpendingWalletFragmentDialogToSendFundsFragment( - paymentRequest, walletId - ), navOptions + if (isErgoPaySigningRequest(request)) + ChooseSpendingWalletFragmentDialogDirections.actionChooseSpendingWalletFragmentDialogToErgoPaySigningFragment( + request, walletId + ) + else + ChooseSpendingWalletFragmentDialogDirections.actionChooseSpendingWalletFragmentDialogToSendFundsFragment( + request, walletId + ), + navOptions ) } @@ -102,4 +117,14 @@ class ChooseSpendingWalletFragmentDialog : FullScreenFragmentDialog() { super.onDestroyView() _binding = null } + + companion object { + private const val ARG_QUERY = "ARG_QUERY" + + fun buildArgs(query: String): Bundle { + val args = Bundle() + args.putString(ARG_QUERY, query) + return args + } + } } \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt index 5045f2589..599accb6c 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt @@ -11,13 +11,9 @@ import androidx.navigation.fragment.navArgs import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.snackbar.Snackbar import com.google.zxing.integration.android.IntentIntegrator -import org.ergoplatform.ErgoAmount import org.ergoplatform.android.R -import org.ergoplatform.android.databinding.EntryTransactionBoxBinding -import org.ergoplatform.android.databinding.EntryWalletTokenBinding import org.ergoplatform.android.databinding.FragmentColdWalletSigningBinding import org.ergoplatform.android.ui.* -import org.ergoplatform.explorer.client.model.AssetInstanceInfo import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES import org.ergoplatform.transactions.coldSigningResponseToQrChunks @@ -64,7 +60,7 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { viewModel.signingResult.observe(viewLifecycleOwner, { if (it?.success == true && viewModel.signedQrCode != null) { - binding.transactionInfo.visibility = View.GONE + binding.transactionInfo.root.visibility = View.GONE binding.cardSigningResult.visibility = View.VISIBLE binding.cardScanMore.visibility = View.GONE @@ -94,7 +90,7 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { }) // Button click listeners - binding.buttonSignTx.setOnClickListener { + binding.transactionInfo.buttonSignTx.setOnClickListener { viewModel.wallet?.let { startAuthFlow(it.walletConfig) } @@ -162,24 +158,10 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { } transactionInfo?.reduceBoxes()?.let { - binding.transactionInfo.visibility = View.VISIBLE + binding.transactionInfo.root.visibility = View.VISIBLE binding.cardScanMore.visibility = View.GONE - binding.layoutInboxes.apply { - removeAllViews() - - it.inputs.forEach { input -> - bindBoxView(this, input.value, input.address ?: input.boxId, input.assets) - } - } - - binding.layoutOutboxes.apply { - removeAllViews() - - it.outputs.forEach { output -> - bindBoxView(this, output.value, output.address, output.assets) - } - } + binding.transactionInfo.bindTransactionInfo(it, layoutInflater) } } @@ -194,40 +176,6 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { ) } - private fun bindBoxView( - container: ViewGroup, - value: Long?, - address: String, - assets: List? - ) { - val boxBinding = EntryTransactionBoxBinding.inflate(layoutInflater, container, true) - boxBinding.boxErgAmount.text = getString( - R.string.label_erg_amount, - ErgoAmount(value ?: 0).toStringTrimTrailingZeros() - ) - boxBinding.boxErgAmount.visibility = - if (value == null || value == 0L) View.GONE else View.VISIBLE - boxBinding.labelBoxAddress.text = address - boxBinding.labelBoxAddress.setOnClickListener { - boxBinding.labelBoxAddress.maxLines = - if (boxBinding.labelBoxAddress.maxLines == 1) 10 else 1 - } - - boxBinding.boxTokenEntries.apply { - removeAllViews() - visibility = View.GONE - - assets?.forEach { - visibility = View.VISIBLE - val tokenBinding = - EntryWalletTokenBinding.inflate(layoutInflater, this, true) - // we use the token id here, we don't have the name in the cold wallet context - tokenBinding.labelTokenName.text = it.tokenId - tokenBinding.labelTokenVal.text = it.amount.toString() - } - } - } - override fun proceedAuthFlowWithPassword(password: String): Boolean { return viewModel.signTxWithPassword(password, AndroidStringProvider(requireContext())) } diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt new file mode 100644 index 000000000..57fcc340d --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt @@ -0,0 +1,162 @@ +package org.ergoplatform.android.transactions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.ergoplatform.transactions.MessageSeverity +import org.ergoplatform.android.AppDatabase +import org.ergoplatform.android.Preferences +import org.ergoplatform.android.R +import org.ergoplatform.android.RoomWalletDbProvider +import org.ergoplatform.android.databinding.FragmentErgoPaySigningBinding +import org.ergoplatform.android.ui.AndroidStringProvider +import org.ergoplatform.transactions.reduceBoxes +import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic +import org.ergoplatform.wallet.addresses.getAddressLabel +import org.ergoplatform.wallet.getNumOfAddresses + +class ErgoPaySigningFragment : SubmitTransactionFragment() { + private var _binding: FragmentErgoPaySigningBinding? = null + private val binding get() = _binding!! + + private val args: ErgoPaySigningFragmentArgs by navArgs() + + override val viewModel: ErgoPaySigningViewModel + get() = ViewModelProvider(this).get(ErgoPaySigningViewModel::class.java) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentErgoPaySigningBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val viewModel = this.viewModel + val context = requireContext() + viewModel.uiLogic.init( + args.request, + args.walletId, + args.derivationIdx, + RoomWalletDbProvider(AppDatabase.getInstance(context)), + Preferences(context), + AndroidStringProvider(context) + ) + + viewModel.uiStateRefresh.observe(viewLifecycleOwner, { state -> + binding.layoutTransactionInfo.visibility = + visibleWhen(state, ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION) + binding.layoutProgress.visibility = + visibleWhen(state, ErgoPaySigningUiLogic.State.FETCH_DATA) + binding.layoutDoneInfo.visibility = + visibleWhen(state, ErgoPaySigningUiLogic.State.DONE) + binding.layoutChooseAddress.visibility = + visibleWhen(state, ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS) + + when (state) { + ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS -> { + // nothing to do + } + ErgoPaySigningUiLogic.State.FETCH_DATA -> showFetchData() + ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION -> showTransactionInfo() + ErgoPaySigningUiLogic.State.DONE -> showDoneInfo() + } + }) + + viewModel.addressChosen.observe(viewLifecycleOwner, { + val walletLabel = viewModel.uiLogic.wallet?.walletConfig?.displayName ?: "" + val addressLabel = + it?.getAddressLabel(AndroidStringProvider(requireContext())) + ?: getString( + R.string.label_all_addresses, + viewModel.uiLogic.wallet?.getNumOfAddresses() + ) + binding.addressLabel.text = + getString(R.string.label_sign_with, addressLabel, walletLabel) + }) + + // Click listeners + binding.transactionInfo.buttonSignTx.setOnClickListener { + startAuthFlow(viewModel.uiLogic.wallet!!.walletConfig) + } + binding.buttonDismiss.setOnClickListener { + findNavController().popBackStack() + } + binding.buttonChooseAddress.setOnClickListener { + showChooseAddressList(false) + } + } + + override fun onAddressChosen(addressDerivationIdx: Int?) { + super.onAddressChosen(addressDerivationIdx) + // redo the request - can't be done within uilogic because the context is needed + val uiLogic = viewModel.uiLogic + uiLogic.lastRequest?.let { + val context = requireContext() + uiLogic.hasNewRequest( + it, + Preferences(context), + AndroidStringProvider(context) + ) + } + } + + private fun visibleWhen( + state: ErgoPaySigningUiLogic.State?, + visibleWhen: ErgoPaySigningUiLogic.State + ) = + if (state == visibleWhen) View.VISIBLE else View.GONE + + private fun showFetchData() { + // nothing special to do yet + } + + private fun showDoneInfo() { + // use normal tx done message in case of success + val uiLogic = viewModel.uiLogic + binding.tvMessage.text = uiLogic.getDoneMessage(AndroidStringProvider(requireContext())) + binding.tvMessage.setCompoundDrawablesRelativeWithIntrinsicBounds( + 0, + getSeverityDrawableResId(uiLogic.getDoneSeverity()), + 0, 0 + ) + } + + private fun getSeverityDrawableResId(severity: MessageSeverity) = + when (severity) { + MessageSeverity.NONE -> 0 + MessageSeverity.INFORMATION -> R.drawable.ic_info_24 + MessageSeverity.WARNING -> R.drawable.ic_warning_amber_24 + MessageSeverity.ERROR -> R.drawable.ic_error_outline_24 + } + + private fun showTransactionInfo() { + val uiLogic = viewModel.uiLogic + binding.transactionInfo.bindTransactionInfo( + uiLogic.transactionInfo!!.reduceBoxes(), + layoutInflater + ) + binding.layoutTiMessage.visibility = uiLogic.epsr?.message?.let { + binding.tvTiMessage.text = getString(R.string.label_message_from_dapp, it) + val severityResId = getSeverityDrawableResId( + uiLogic.epsr?.messageSeverity ?: MessageSeverity.NONE + ) + binding.imageTiMessage.setImageResource(severityResId) + binding.imageTiMessage.visibility = if (severityResId == 0) View.GONE else View.VISIBLE + View.VISIBLE + } ?: View.GONE + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt new file mode 100644 index 000000000..c9130f44c --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt @@ -0,0 +1,50 @@ +package org.ergoplatform.android.transactions + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import org.ergoplatform.persistance.WalletAddress +import org.ergoplatform.transactions.TransactionResult +import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic + +class ErgoPaySigningViewModel: SubmitTransactionViewModel() { + override val uiLogic = AndroidErgoPaySigningUiLogic() + + private var _uiStateRefresh = MutableLiveData() + val uiStateRefresh: LiveData get() = _uiStateRefresh + + private var _addressChosen = MutableLiveData() + val addressChosen: LiveData get() = _addressChosen + + inner class AndroidErgoPaySigningUiLogic: ErgoPaySigningUiLogic() { + override val coroutineScope: CoroutineScope + get() = viewModelScope + + override fun notifyWalletStateLoaded() { + // not needed, notifyDerivedAddressChanged is always called right after this one + } + + override fun notifyDerivedAddressChanged() { + _addressChosen.postValue(uiLogic.derivedAddress) + } + + override fun notifyUiLocked(locked: Boolean) { + _lockInterface.postValue(locked) + } + + override fun notifyHasErgoTxResult(txResult: TransactionResult) { + _txWorkDoneLiveData.postValue(txResult) + } + + override fun notifyHasSigningPromptData(signingPrompt: String) { + _signingPromptData.postValue(signingPrompt) + } + + override fun notifyStateChanged(newState: State) { + _uiStateRefresh.postValue(newState) + } + + + } +} \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt index 31ee303de..de0053a58 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt @@ -3,35 +3,26 @@ package org.ergoplatform.android.transactions import android.animation.LayoutTransition import android.content.Intent import android.os.Bundle -import android.os.Handler import android.text.Editable import android.text.InputType import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.EditText import androidx.core.view.descendants import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout import com.google.zxing.integration.android.IntentIntegrator import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent import org.ergoplatform.* -import org.ergoplatform.android.Preferences import org.ergoplatform.android.R import org.ergoplatform.android.databinding.FragmentSendFundsBinding import org.ergoplatform.android.databinding.FragmentSendFundsTokenItemBinding import org.ergoplatform.android.ui.* -import org.ergoplatform.android.wallet.addresses.AddressChooserCallback -import org.ergoplatform.android.wallet.addresses.ChooseAddressListDialogFragment -import org.ergoplatform.persistance.WalletConfig import org.ergoplatform.persistance.WalletToken import org.ergoplatform.tokens.isSingularToken -import org.ergoplatform.transactions.PromptSigningResult import org.ergoplatform.utils.formatFiatToString import org.ergoplatform.wallet.addresses.getAddressLabel import org.ergoplatform.wallet.getNumOfAddresses @@ -40,13 +31,17 @@ import org.ergoplatform.wallet.getNumOfAddresses /** * Here's the place to send transactions */ -class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallback, - AddressChooserCallback { +class SendFundsFragment : SubmitTransactionFragment() { private var _binding: FragmentSendFundsBinding? = null private val binding get() = _binding!! - private lateinit var viewModel: SendFundsViewModel + override lateinit var viewModel: SendFundsViewModel private val args: SendFundsFragmentArgs by navArgs() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -96,7 +91,7 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba R.string.desc_fee, viewModel.uiLogic.feeAmount.toStringRoundToDecimals() ) - binding.grossAmount.amount = it.toDouble() + binding.grossAmount.setAmount(it.toBigDecimal()) val nodeConnector = NodeConnector.getInstance() binding.tvFiat.visibility = if (nodeConnector.fiatCurrency.isNotEmpty()) View.VISIBLE else View.GONE @@ -113,33 +108,11 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba viewModel.tokensChosenLiveData.observe(viewLifecycleOwner, { refreshTokensList() }) - viewModel.lockInterface.observe(viewLifecycleOwner, { - if (it) - ProgressBottomSheetDialogFragment.showProgressDialog(childFragmentManager) - else - ProgressBottomSheetDialogFragment.dismissProgressDialog(childFragmentManager) - }) - viewModel.txWorkDoneLiveData.observe(viewLifecycleOwner, { - if (!it.success) { - val snackbar = Snackbar.make( - requireView(), - if (it is PromptSigningResult) - R.string.error_prepare_transaction - else R.string.error_send_transaction, - Snackbar.LENGTH_LONG - ) - it.errorMsg?.let { errorMsg -> - snackbar.setAction( - R.string.label_details - ) { - showDialogWithCopyOption(requireContext(), errorMsg) - } - } - snackbar.setAnchorView(R.id.nav_view).show() - } else if (it is PromptSigningResult) { - // if this is a prompt signing result, switch to prompt signing dialog - SigningPromptDialogFragment().show(childFragmentManager, null) - } + viewModel.errorMessageLiveData.observe(viewLifecycleOwner, { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(it) + .setPositiveButton(R.string.zxing_button_ok, null) + .show() }) viewModel.txId.observe(viewLifecycleOwner, { it?.let { @@ -151,11 +124,7 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba // Add click listeners binding.addressLabel.setOnClickListener { - viewModel.uiLogic.wallet?.let { wallet -> - ChooseAddressListDialogFragment.newInstance( - wallet.walletConfig.id, true - ).show(childFragmentManager, null) - } + showChooseAddressList(true) } binding.buttonShareTx.setOnClickListener { val txUrl = getExplorerTxUrl(binding.labelTxId.text.toString()) @@ -180,9 +149,6 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba startPayment() } - binding.buttonScan.setOnClickListener { - IntentIntegrator.forSupportFragment(this).initiateScan(setOf(IntentIntegrator.QR_CODE)) - } binding.buttonAddToken.setOnClickListener { ChooseTokenListDialogFragment().show(childFragmentManager, null) } @@ -218,27 +184,34 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba }) } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.fragment_send_funds, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.menu_scan_qr) { + IntentIntegrator.forSupportFragment(this).initiateScan(setOf(IntentIntegrator.QR_CODE)) + return true + } else { + return super.onOptionsItemSelected(item) + } + } + private fun enableLayoutChangeAnimations() { // set layout change animations. they are not set in the xml to avoid animations for the first // time the layout is displayed, and enabling them is delayed due to the same reason - Handler().postDelayed({ + postDelayed(200) { _binding?.let { binding -> binding.container.layoutTransition = LayoutTransition() binding.container.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) } - }, 200) + } } private fun ensureAmountVisibleDelayed() { // delay 200 to make sure that smart keyboard is already open - Handler().postDelayed( - { _binding?.let { it.scrollView.smoothScrollTo(0, it.amount.top) } }, - 200 - ) - } - - override fun onAddressChosen(addressDerivationIdx: Int?) { - viewModel.uiLogic.derivedAddressIdx = addressDerivationIdx + postDelayed(200) { _binding?.let { it.scrollView.smoothScrollTo(0, it.amount.top) } } } private fun refreshTokensList() { @@ -337,32 +310,11 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba } } - override fun startAuthFlow(walletConfig: WalletConfig) { - if (walletConfig.secretStorage == null) { - // we have a read only wallet here, let's go to cold wallet support mode - val context = requireContext() - viewModel.uiLogic.startColdWalletPayment( - Preferences(context), - AndroidStringProvider(context) - ) - } else { - super.startAuthFlow(walletConfig) - } - } - - override fun proceedAuthFlowWithPassword(password: String): Boolean { - return viewModel.startPaymentWithPassword(password, requireContext()) - } - override fun showBiometricPrompt() { hideForcedSoftKeyboard(requireContext(), binding.amount.editText!!) super.showBiometricPrompt() } - override fun proceedAuthFlowFromBiometrics() { - context?.let { viewModel.startPaymentUserAuth(it) } - } - private fun inputChangesToViewModel() { viewModel.uiLogic.receiverAddress = binding.tvReceiver.editText?.text?.toString() ?: "" @@ -378,25 +330,38 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { - result.contents?.let { - viewModel.uiLogic.qrCodeScanned(it, { data, walletId -> - findNavController().navigate( - SendFundsFragmentDirections - .actionSendFundsFragmentToColdWalletSigningFragment( - data, - walletId - ) - ) - }, { address, amount -> - binding.tvReceiver.editText?.setText(address) - amount?.let { setAmountEdittext(amount) } - }) - } + result.contents?.let { qrCodeScanned(it) } } else { super.onActivityResult(requestCode, resultCode, data) } } + private fun qrCodeScanned(qrCode: String) { + viewModel.uiLogic.qrCodeScanned( + qrCode, + AndroidStringProvider(requireContext()), + { data, walletId -> + findNavController().navigate( + SendFundsFragmentDirections + .actionSendFundsFragmentToColdWalletSigningFragment( + data, + walletId + ) + ) + }, + { ergoPayRequest -> + findNavController().navigateSafe( + SendFundsFragmentDirections.actionSendFundsFragmentToErgoPaySigningFragment( + ergoPayRequest, args.walletId, viewModel.uiLogic.derivedAddressIdx ?: -1 + ) + ) + }, + { address, amount -> + binding.tvReceiver.editText?.setText(address) + amount?.let { setAmountEdittext(amount) } + }) + } + private fun showPaymentRequestWarnings() { viewModel.uiLogic.getPaymentRequestWarnings(AndroidStringProvider(requireContext()))?.let { MaterialAlertDialogBuilder(requireContext()) diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt index 5842d6ce1..f9078a9a5 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt @@ -3,51 +3,39 @@ package org.ergoplatform.android.transactions import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import org.ergoplatform.ErgoAmount import org.ergoplatform.android.AppDatabase -import org.ergoplatform.android.Preferences import org.ergoplatform.android.RoomWalletDbProvider -import org.ergoplatform.android.ui.AndroidStringProvider import org.ergoplatform.android.ui.SingleLiveEvent -import org.ergoplatform.api.AesEncryptionManager -import org.ergoplatform.api.AndroidEncryptionManager -import org.ergoplatform.deserializeSecrets -import org.ergoplatform.persistance.WalletAddress import org.ergoplatform.transactions.TransactionResult import org.ergoplatform.uilogic.transactions.SendFundsUiLogic /** * Holding state of the send funds screen (thus to be expected to get complicated) */ -class SendFundsViewModel : ViewModel() { - val uiLogic = AndroidSendFundsUiLogic() +class SendFundsViewModel : SubmitTransactionViewModel() { + override val uiLogic = AndroidSendFundsUiLogic() - private val _lockInterface = MutableLiveData() - val lockInterface: LiveData = _lockInterface private val _walletName = MutableLiveData() val walletName: LiveData = _walletName - private val _address = MutableLiveData() - val address: LiveData = _address private val _walletBalance = MutableLiveData() val walletBalance: LiveData = _walletBalance private val _grossAmount = MutableLiveData().apply { value = ErgoAmount.ZERO } val grossAmount: LiveData = _grossAmount - private val _txWorkDoneLiveData = SingleLiveEvent() - val txWorkDoneLiveData: LiveData = _txWorkDoneLiveData private val _txId = MutableLiveData() val txId: LiveData = _txId - private val _signingPromptData = MutableLiveData() - val signingPromptData: LiveData = _signingPromptData // the live data gets data posted on adding or removing tokens, not on every amount change private val _tokensChosenLiveData = MutableLiveData>() val tokensChosenLiveData: LiveData> = _tokensChosenLiveData + private val _errorMessageLiveData = SingleLiveEvent() + val errorMessageLiveData: LiveData = _errorMessageLiveData + fun initWallet(ctx: Context, walletId: Int, derivationIdx: Int, paymentRequest: String?) { uiLogic.initWallet( RoomWalletDbProvider(AppDatabase.getInstance(ctx)), @@ -57,52 +45,6 @@ class SendFundsViewModel : ViewModel() { ) } - fun startPaymentWithPassword(password: String, context: Context): Boolean { - uiLogic.wallet?.walletConfig?.secretStorage?.let { - val mnemonic: String? - try { - val decryptData = AesEncryptionManager.decryptData(password, it) - mnemonic = deserializeSecrets(String(decryptData!!)) - } catch (t: Throwable) { - // Password wrong - return false - } - - if (mnemonic == null) { - // deserialization error, corrupted db data - return false - } - - uiLogic.startPaymentWithMnemonicAsync( - mnemonic, - Preferences(context), - AndroidStringProvider(context) - ) - - return true - } - - return false - } - - fun startPaymentUserAuth(context: Context) { - // we don't handle exceptions here by intention: we throw them back to the fragment which - // will show a snackbar to give the user a hint what went wrong - uiLogic.wallet?.walletConfig?.secretStorage?.let { - val mnemonic: String? - - val decryptData = AndroidEncryptionManager.decryptDataWithDeviceKey(it) - mnemonic = deserializeSecrets(String(decryptData!!)) - - uiLogic.startPaymentWithMnemonicAsync( - mnemonic!!, - Preferences(context), - AndroidStringProvider(context) - ) - - } - } - inner class AndroidSendFundsUiLogic : SendFundsUiLogic() { override val coroutineScope: CoroutineScope get() = viewModelScope @@ -144,5 +86,9 @@ class SendFundsViewModel : ViewModel() { override fun notifyHasSigningPromptData(signingPrompt: String) { _signingPromptData.postValue(signingPrompt) } + + override fun showErrorMessage(message: String) { + _errorMessageLiveData.postValue(message) + } } } \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt index b44556feb..4e2dae22a 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -104,8 +104,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { binding.tvDesc.setText(if (lastPage) R.string.desc_prompt_signing else R.string.desc_prompt_signing_multiple) } - private fun getViewModel() = ViewModelProvider(parentFragment as ViewModelStoreOwner) - .get(SendFundsViewModel::class.java) + private fun getViewModel() = (parentFragment as SubmitTransactionFragment).viewModel override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionFragment.kt new file mode 100644 index 000000000..27c4a1c91 --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionFragment.kt @@ -0,0 +1,89 @@ +package org.ergoplatform.android.transactions + +import android.os.Bundle +import android.view.View +import com.google.android.material.snackbar.Snackbar +import org.ergoplatform.android.Preferences +import org.ergoplatform.android.R +import org.ergoplatform.android.ui.AbstractAuthenticationFragment +import org.ergoplatform.android.ui.AndroidStringProvider +import org.ergoplatform.android.ui.ProgressBottomSheetDialogFragment +import org.ergoplatform.android.ui.showDialogWithCopyOption +import org.ergoplatform.android.wallet.addresses.AddressChooserCallback +import org.ergoplatform.android.wallet.addresses.ChooseAddressListDialogFragment +import org.ergoplatform.persistance.WalletConfig +import org.ergoplatform.transactions.PromptSigningResult + +abstract class SubmitTransactionFragment : AbstractAuthenticationFragment(), + AddressChooserCallback { + + abstract val viewModel: SubmitTransactionViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val viewModel = this.viewModel + viewModel.lockInterface.observe(viewLifecycleOwner, { + if (it) + ProgressBottomSheetDialogFragment.showProgressDialog(childFragmentManager) + else + ProgressBottomSheetDialogFragment.dismissProgressDialog(childFragmentManager) + }) + viewModel.txWorkDoneLiveData.observe(viewLifecycleOwner, { + if (!it.success) { + val snackbar = Snackbar.make( + requireView(), + if (it is PromptSigningResult) + R.string.error_prepare_transaction + else R.string.error_send_transaction, + Snackbar.LENGTH_LONG + ) + it.errorMsg?.let { errorMsg -> + snackbar.setAction( + R.string.label_details + ) { + showDialogWithCopyOption(requireContext(), errorMsg) + } + } + snackbar.setAnchorView(R.id.nav_view).show() + } else if (it is PromptSigningResult) { + // if this is a prompt signing result, switch to prompt signing dialog + SigningPromptDialogFragment().show(childFragmentManager, null) + } + }) + + } + + fun showChooseAddressList(addShowAllEntry: Boolean) { + viewModel.uiLogic.wallet?.let { wallet -> + ChooseAddressListDialogFragment.newInstance( + wallet.walletConfig.id, addShowAllEntry + ).show(childFragmentManager, null) + } + } + + override fun onAddressChosen(addressDerivationIdx: Int?) { + viewModel.uiLogic.derivedAddressIdx = addressDerivationIdx + } + + override fun startAuthFlow(walletConfig: WalletConfig) { + if (walletConfig.secretStorage == null) { + // we have a read only wallet here, let's go to cold wallet support mode + val context = requireContext() + viewModel.uiLogic.startColdWalletPayment( + Preferences(context), + AndroidStringProvider(context) + ) + } else { + super.startAuthFlow(walletConfig) + } + } + + override fun proceedAuthFlowWithPassword(password: String): Boolean { + return viewModel.startPaymentWithPassword(password, requireContext()) + } + + override fun proceedAuthFlowFromBiometrics() { + context?.let { viewModel.startPaymentUserAuth(it) } + } +} \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionViewModel.kt new file mode 100644 index 000000000..46bcb1cdc --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionViewModel.kt @@ -0,0 +1,74 @@ +package org.ergoplatform.android.transactions + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.ergoplatform.android.Preferences +import org.ergoplatform.android.ui.AndroidStringProvider +import org.ergoplatform.android.ui.SingleLiveEvent +import org.ergoplatform.api.AesEncryptionManager +import org.ergoplatform.api.AndroidEncryptionManager +import org.ergoplatform.deserializeSecrets +import org.ergoplatform.persistance.WalletAddress +import org.ergoplatform.transactions.TransactionResult +import org.ergoplatform.uilogic.transactions.SubmitTransactionUiLogic + +abstract class SubmitTransactionViewModel : ViewModel() { + abstract val uiLogic: SubmitTransactionUiLogic + + protected val _lockInterface = MutableLiveData() + val lockInterface: LiveData = _lockInterface + protected val _address = MutableLiveData() + val address: LiveData = _address + protected val _txWorkDoneLiveData = SingleLiveEvent() + val txWorkDoneLiveData: LiveData = _txWorkDoneLiveData + protected val _signingPromptData = MutableLiveData() + val signingPromptData: LiveData = _signingPromptData + + fun startPaymentWithPassword(password: String, context: Context): Boolean { + uiLogic.wallet?.walletConfig?.secretStorage?.let { + val mnemonic: String? + try { + val decryptData = AesEncryptionManager.decryptData(password, it) + mnemonic = deserializeSecrets(String(decryptData!!)) + } catch (t: Throwable) { + // Password wrong + return false + } + + if (mnemonic == null) { + // deserialization error, corrupted db data + return false + } + + uiLogic.startPaymentWithMnemonicAsync( + mnemonic, + Preferences(context), + AndroidStringProvider(context) + ) + + return true + } + + return false + } + + fun startPaymentUserAuth(context: Context) { + // we don't handle exceptions here by intention: we throw them back to the fragment which + // will show a snackbar to give the user a hint what went wrong + uiLogic.wallet?.walletConfig?.secretStorage?.let { + val mnemonic: String? + + val decryptData = AndroidEncryptionManager.decryptDataWithDeviceKey(it) + mnemonic = deserializeSecrets(String(decryptData!!)) + + uiLogic.startPaymentWithMnemonicAsync( + mnemonic!!, + Preferences(context), + AndroidStringProvider(context) + ) + + } + } +} \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt b/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt new file mode 100644 index 000000000..9551208fe --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt @@ -0,0 +1,79 @@ +package org.ergoplatform.android.transactions + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.ergoplatform.ErgoAmount +import org.ergoplatform.TokenAmount +import org.ergoplatform.android.R +import org.ergoplatform.android.databinding.CardTransactionInfoBinding +import org.ergoplatform.android.databinding.EntryTransactionBoxBinding +import org.ergoplatform.android.databinding.EntryWalletTokenBinding +import org.ergoplatform.explorer.client.model.AssetInstanceInfo +import org.ergoplatform.transactions.TransactionInfo + +fun CardTransactionInfoBinding.bindTransactionInfo( + ti: TransactionInfo, + layoutInflater: LayoutInflater +) { + layoutInboxes.apply { + removeAllViews() + + ti.inputs.forEach { input -> + bindBoxView( + this, + input.value, + input.address ?: input.boxId, + input.assets, + layoutInflater + ) + } + } + + layoutOutboxes.apply { + removeAllViews() + + ti.outputs.forEach { output -> + bindBoxView( + this, output.value, output.address, output.assets, layoutInflater + ) + } + } +} + +private fun bindBoxView( + container: ViewGroup, + value: Long?, + address: String, + assets: List?, + layoutInflater: LayoutInflater +) { + val boxBinding = EntryTransactionBoxBinding.inflate(layoutInflater, container, true) + boxBinding.boxErgAmount.text = container.context.getString( + R.string.label_erg_amount, + ErgoAmount(value ?: 0).toStringTrimTrailingZeros() + ) + boxBinding.boxErgAmount.visibility = + if (value == null || value == 0L) View.GONE else View.VISIBLE + boxBinding.labelBoxAddress.text = address + boxBinding.labelBoxAddress.setOnClickListener { + boxBinding.labelBoxAddress.maxLines = + if (boxBinding.labelBoxAddress.maxLines == 1) 10 else 1 + } + + boxBinding.boxTokenEntries.apply { + removeAllViews() + visibility = View.GONE + + assets?.forEach { + visibility = View.VISIBLE + val tokenBinding = + EntryWalletTokenBinding.inflate(layoutInflater, this, true) + // we use the token id here, we don't have the name in the cold wallet context + tokenBinding.labelTokenName.text = it.name ?: it.tokenId + tokenBinding.labelTokenVal.text = + TokenAmount(it.amount, it.decimals ?: 0).toStringTrimTrailingZeros() + } + } +} + diff --git a/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt b/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt index d45d97275..dd9bc0fe3 100644 --- a/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt +++ b/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt @@ -6,6 +6,8 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.method.LinkMovementMethod import android.util.Log import android.view.View @@ -133,4 +135,8 @@ fun BottomSheetDialogFragment.expandBottomSheetOnShow() { BottomSheetBehavior.STATE_EXPANDED } } +} + +fun postDelayed(delayMs: Long, r: Runnable) { + Handler(Looper.getMainLooper()).postDelayed(r, delayMs) } \ No newline at end of file diff --git a/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt b/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt index 822ba4614..f2f12d808 100644 --- a/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt @@ -3,8 +3,6 @@ package org.ergoplatform.android.wallet import android.animation.LayoutTransition import android.content.Context import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.* import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle @@ -27,6 +25,7 @@ import org.ergoplatform.android.tokens.inflateAndBindDetailedTokenEntryView import org.ergoplatform.android.ui.AndroidStringProvider import org.ergoplatform.android.ui.navigateSafe import org.ergoplatform.android.ui.openUrlWithBrowser +import org.ergoplatform.android.ui.postDelayed import org.ergoplatform.android.wallet.addresses.AddressChooserCallback import org.ergoplatform.android.wallet.addresses.ChooseAddressListDialogFragment import org.ergoplatform.getExplorerWebUrl @@ -128,7 +127,7 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback { } // enable layout change animations after a short wait time - Handler(Looper.getMainLooper()).postDelayed({ enableLayoutChangeAnimations() }, 500) + postDelayed(500) { enableLayoutChangeAnimations() } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -175,8 +174,8 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback { val ergoAmount = walletDetailsViewModel.uiLogic.getErgoBalance() val unconfirmed = walletDetailsViewModel.uiLogic.getUnconfirmedErgoBalance() - binding.walletBalance.amount = ergoAmount.toDouble() - binding.walletUnconfirmed.amount = unconfirmed.toDouble() + binding.walletBalance.setAmount(ergoAmount.toBigDecimal()) + binding.walletUnconfirmed.setAmount(unconfirmed.toBigDecimal()) binding.walletUnconfirmed.visibility = if (unconfirmed.isZero()) View.GONE else View.VISIBLE binding.labelWalletUnconfirmed.visibility = binding.walletUnconfirmed.visibility @@ -187,7 +186,7 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback { binding.walletFiat.visibility = View.GONE } else { binding.walletFiat.visibility = View.VISIBLE - binding.walletFiat.amount = ergoPrice * binding.walletBalance.amount + binding.walletFiat.amount = ergoPrice * ergoAmount.toDouble() binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.uppercase()) } diff --git a/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt b/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt index 47785477a..24fc4d3d7 100644 --- a/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt @@ -19,10 +19,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.ergoplatform.ErgoAmount import org.ergoplatform.NodeConnector -import org.ergoplatform.android.AppDatabase -import org.ergoplatform.android.Preferences -import org.ergoplatform.android.R -import org.ergoplatform.android.RoomWalletDbProvider +import org.ergoplatform.android.* import org.ergoplatform.android.databinding.CardWalletBinding import org.ergoplatform.android.databinding.EntryWalletTokenBinding import org.ergoplatform.android.databinding.FragmentWalletBinding @@ -99,6 +96,7 @@ class WalletFragment : Fragment() { .navigate(R.id.createWalletDialog) } + binding.buttonScan.setOnClickListener { (requireActivity() as? MainActivity)?.scanQrCode() } val nodeConnector = NodeConnector.getInstance() val rotateAnimation = @@ -153,9 +151,7 @@ class WalletFragment : Fragment() { binding.ergoPrice.visibility = View.VISIBLE binding.ergoPrice.amount = value.toDouble() binding.ergoPrice.setSymbol( - nodeConnector.fiatCurrency.toUpperCase( - Locale.getDefault() - ) + nodeConnector.fiatCurrency.uppercase() ) } binding.labelErgoPrice.visibility = binding.ergoPrice.visibility @@ -241,7 +237,8 @@ class WalletAdapter(initWalletList: List) : class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(wallet: Wallet) { binding.walletName.text = wallet.walletConfig.displayName - binding.walletBalance.amount = ErgoAmount(wallet.getBalanceForAllAddresses()).toDouble() + val walletBalance = ErgoAmount(wallet.getBalanceForAllAddresses()) + binding.walletBalance.setAmount(walletBalance.toBigDecimal()) // Fill token headline val tokens = wallet.getTokensForAllAddresses() @@ -259,7 +256,7 @@ class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder // Fill unconfirmed fields val unconfirmed = wallet.getUnconfirmedBalanceForAllAddresses() - binding.walletUnconfirmed.amount = ErgoAmount(unconfirmed).toDouble() + binding.walletUnconfirmed.setAmount(ErgoAmount(unconfirmed).toBigDecimal()) binding.walletUnconfirmed.visibility = if (unconfirmed == 0L) View.GONE else View.VISIBLE binding.labelWalletUnconfirmed.visibility = binding.walletUnconfirmed.visibility @@ -322,8 +319,8 @@ class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder binding.walletFiat.visibility = View.GONE } else { binding.walletFiat.visibility = View.VISIBLE - binding.walletFiat.amount = ergoPrice * binding.walletBalance.amount - binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.toUpperCase()) + binding.walletFiat.amount = ergoPrice * walletBalance.toDouble() + binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.uppercase()) } // Fill token entries diff --git a/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt b/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt index 83905ba7e..5c516c5e0 100644 --- a/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt +++ b/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt @@ -26,7 +26,7 @@ fun IncludeWalletAddressInfoBinding.fillAddressInformation( val state = wallet.getStateForAddress(walletAddress.publicAddress) val tokens = wallet.getTokensForAddress(walletAddress.publicAddress) - addressBalance.amount = ErgoAmount(state?.balance ?: 0).toDouble() + addressBalance.setAmount(ErgoAmount(state?.balance ?: 0).toBigDecimal()) labelTokenNum.visibility = if (tokens.isNullOrEmpty()) View.GONE else View.VISIBLE labelTokenNum.text = @@ -47,6 +47,7 @@ fun IncludeWalletAddressInfoBinding.fillWalletAddressesInformation( labelTokenNum.text = ctx.getString(R.string.label_wallet_token_balance, tokenNum.toString()) - addressBalance.amount = - ErgoAmount(wallet.getBalanceForAllAddresses()).toDouble() + addressBalance.setAmount( + ErgoAmount(wallet.getBalanceForAllAddresses()).toBigDecimal() + ) } \ No newline at end of file diff --git a/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java b/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java index b9c96a304..08a6dfa3f 100644 --- a/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java +++ b/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java @@ -17,6 +17,7 @@ import org.ergoplatform.android.R; +import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; @@ -42,6 +43,7 @@ public class MoneyTextView extends View { private Section mDecimalSection; private char mDecimalSeparator; private double mAmount; + private BigDecimal bigDecimalAmount; private int mGravity; private int mSymbolGravity; private int mDecimalGravity; @@ -153,11 +155,13 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { public void setAmount(double amount) { mAmount = amount; + bigDecimalAmount = null; requestLayout(); } private void createTextFromAmount() { - String formattedAmount = mDecimalFormat.format(mAmount); + String formattedAmount = bigDecimalAmount != null ? mDecimalFormat.format(bigDecimalAmount) + : mDecimalFormat.format(mAmount); int separatorIndex = formattedAmount.lastIndexOf(mDecimalSeparator); @@ -284,12 +288,6 @@ private void drawSection(Canvas canvas, Section section) { /// SETTERS /// - public void setAmount(float amount, String symbol) { - mAmount = amount; - mSymbolSection.text = symbol; - requestLayout(); - } - public void setDecimalFormat(DecimalFormat decimalFormat) { mDecimalFormat = decimalFormat; requestLayout(); @@ -354,6 +352,15 @@ public double getAmount() { return mAmount; } + public BigDecimal getBigDecimalAmount() { + return bigDecimalAmount; + } + + public void setAmount(BigDecimal bigDecimalAmount) { + this.bigDecimalAmount = bigDecimalAmount; + requestLayout(); + } + private int getMinPadding(int padding) { if(padding == 0){ return (int) (MIN_PADDING * Resources.getSystem().getDisplayMetrics().density); diff --git a/android/src/main/res/drawable/ic_error_outline_24.xml b/android/src/main/res/drawable/ic_error_outline_24.xml new file mode 100644 index 000000000..5a3f71bf7 --- /dev/null +++ b/android/src/main/res/drawable/ic_error_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/src/main/res/drawable/ic_info_24.xml b/android/src/main/res/drawable/ic_info_24.xml new file mode 100644 index 000000000..322b8a9e8 --- /dev/null +++ b/android/src/main/res/drawable/ic_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/src/main/res/drawable/ic_qr_code_scanner_24.xml b/android/src/main/res/drawable/ic_qr_code_scanner_24.xml new file mode 100644 index 000000000..597e8d7bc --- /dev/null +++ b/android/src/main/res/drawable/ic_qr_code_scanner_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/src/main/res/drawable/ic_warning_amber_24.xml b/android/src/main/res/drawable/ic_warning_amber_24.xml new file mode 100644 index 000000000..a88d71898 --- /dev/null +++ b/android/src/main/res/drawable/ic_warning_amber_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/android/src/main/res/layout/card_transaction_info.xml b/android/src/main/res/layout/card_transaction_info.xml new file mode 100644 index 000000000..8515e9ba0 --- /dev/null +++ b/android/src/main/res/layout/card_transaction_info.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +