From 5782a210970a30ef619d23e5c775b5767a87dae1 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 6 Jan 2022 20:27:40 +0100 Subject: [PATCH 01/25] Year of build 2022, better maintainability --- .../ergoplatform/android/settings/SettingsFragment.kt | 1 + android/src/main/res/layout/fragment_settings.xml | 5 +++-- android/src/main/res/values-es/strings.xml | 2 +- android/src/main/res/values-zh/strings.xml | 2 +- android/src/main/res/values/strings.xml | 11 ++++++----- .../java/org/ergoplatform/uilogic/StringResources.kt | 1 + ios/resources/i18n/strings.properties | 5 +++-- ios/resources/i18n/strings_es.properties | 2 +- ios/resources/i18n/strings_zh.properties | 2 +- .../ios/settings/SettingsViewController.kt | 2 +- 10 files changed, 19 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/org/ergoplatform/android/settings/SettingsFragment.kt b/android/src/main/java/org/ergoplatform/android/settings/SettingsFragment.kt index c95b4335c..915f449cd 100644 --- a/android/src/main/java/org/ergoplatform/android/settings/SettingsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/settings/SettingsFragment.kt @@ -38,6 +38,7 @@ class SettingsFragment : Fragment() { _binding = FragmentSettingsBinding.inflate(inflater, container, false) binding.labelVersion.text = BuildConfig.VERSION_NAME + binding.labelBuildBy.text = getString(R.string.desc_about, getString(R.string.about_year)) return binding.root } diff --git a/android/src/main/res/layout/fragment_settings.xml b/android/src/main/res/layout/fragment_settings.xml index 581f922df..059152565 100644 --- a/android/src/main/res/layout/fragment_settings.xml +++ b/android/src/main/res/layout/fragment_settings.xml @@ -46,13 +46,14 @@ tools:text="0.1.2" /> + app:layout_constraintTop_toBottomOf="@id/screen_header" + tools:text="@string/desc_about" /> - Creado por Benjamin Schulte, 2021 + Creado por Benjamin Schulte, %1s Licenciado bajo Licencia de Apache 2\n Este proyecto es software de código abierto\n Encuentra ayuda en Discord o Telegram diff --git a/android/src/main/res/values-zh/strings.xml b/android/src/main/res/values-zh/strings.xml index 741a5af13..4938bd7fa 100644 --- a/android/src/main/res/values-zh/strings.xml +++ b/android/src/main/res/values-zh/strings.xml @@ -170,7 +170,7 @@ 扫描这些二维码以继续。 - Benjamin Schulte于 2021 年为您带来 + Benjamin Schulte于 %1s 年为您带来 许可依据Apache 2 License\n 本项目是一个开源软件\n 请到Discord电报寻求帮助 diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index acdef4612..1745a9d6d 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -1,11 +1,11 @@ - Ergo Wallet + Ergo Wallet Transactions Wallets Settings - ###,##0.0000 - ###,##0.000000 - ###,##0.00 + ###,##0.0000 + ###,##0.000000 + ###,##0.00 Please enter your spending password Forgot your password? @@ -178,7 +178,8 @@ Scan these QR codes to proceed. - Brought to you by Benjamin Schulte, 2021 + Brought to you by Benjamin Schulte, %1s + 2022 Licensed under Apache 2 License\n This project is open source software\n Find help on Discord or Telegram diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt index 645362f25..7aa6ef2e6 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt @@ -27,6 +27,7 @@ const val STRING_ABC_SEARCHVIEW_DESCRIPTION_VOICE = "abc_searchview_description_ const val STRING_ABC_SHAREACTIONPROVIDER_SHARE_WITH = "abc_shareactionprovider_share_with" const val STRING_ABC_SHAREACTIONPROVIDER_SHARE_WITH_APPLICATION = "abc_shareactionprovider_share_with_application" const val STRING_ABC_TOOLBAR_COLLAPSE_DESCRIPTION = "abc_toolbar_collapse_description" +const val STRING_ABOUT_YEAR = "about_year" const val STRING_APP_NAME = "app_name" const val STRING_APP_VERSION = "app_version" const val STRING_APPBAR_SCROLLING_VIEW_BEHAVIOR = "appbar_scrolling_view_behavior" diff --git a/ios/resources/i18n/strings.properties b/ios/resources/i18n/strings.properties index debcf25f2..00e87a237 100644 --- a/ios/resources/i18n/strings.properties +++ b/ios/resources/i18n/strings.properties @@ -25,6 +25,7 @@ abc_searchview_description_voice=Voice search abc_shareactionprovider_share_with=Share with abc_shareactionprovider_share_with_application=Share with %s abc_toolbar_collapse_description=Collapse +about_year=2022 app_name=Ergo Wallet app_version=null appbar_scrolling_view_behavior=com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior @@ -61,7 +62,7 @@ chip_text=Chip text clear_text_end_icon_content_description=Clear text confirm_device_credential_password=Use password default_error_msg=Unknown error -desc_about=Brought to you by Benjamin Schulte, 2021 +desc_about=Brought to you by Benjamin Schulte, {0} desc_about_moreinfo=Licensed under Apache 2 License\n\ This project is open source software\n\ Find help on Discord or Telegram @@ -137,7 +138,7 @@ generic_error_no_keyguard=This device does not support PIN, pattern, or password generic_error_user_canceled=Authentication canceled by user. hide_bottom_view_on_scroll_behavior=com.google.android.material.behavior.HideBottomViewOnScrollBehavior hint_password=Please enter your spending password -hint_read_only=This is a read only wallet. For sending funds, you need to restore it with your mnemonic. Cold wallet capabilities coming soon. +hint_read_only=This is a read only wallet. For sending funds, you need a device to sign your transaction. Learn more. hint_wallet_addr_label=Descriptive label (optional) intro_add_readonly=Enter your public wallet address to add the wallet in read-only mode. intro_confirm_create_wallet=To confirm that your handwritten mnemonic is correct, enter two words of the phrase now: diff --git a/ios/resources/i18n/strings_es.properties b/ios/resources/i18n/strings_es.properties index e48ce58a2..df3925f16 100644 --- a/ios/resources/i18n/strings_es.properties +++ b/ios/resources/i18n/strings_es.properties @@ -27,7 +27,7 @@ character_counter_overflowed_content_description=Límite de caracteres superado check_confirm_create_wallet=Entiendo que soy responsable de guardar mi frase semilla en un lugar seguro. \ Es la única forma de acceder a mi billetera si las claves almacenadas en este dispositivo se pierden o son inaccesibles. clear_text_end_icon_content_description=Borrar texto -desc_about=Creado por Benjamin Schulte, 2021 +desc_about=Creado por Benjamin Schulte, {0} desc_about_moreinfo=Licenciado bajo Licencia de Apache 2\n\ Este proyecto es software de código abierto\n\ Encuentra ayuda en Discord o Telegram diff --git a/ios/resources/i18n/strings_zh.properties b/ios/resources/i18n/strings_zh.properties index 2f814ad2e..1c3a32c2e 100644 --- a/ios/resources/i18n/strings_zh.properties +++ b/ios/resources/i18n/strings_zh.properties @@ -24,7 +24,7 @@ button_show_debug_info=显示调试信息 button_yes=是 check_confirm_create_wallet=我明白我有责任将我的助记词存放在安全的地方。 \ 如果存储在此设备上的密钥丢失或无法访问,则助记词是访问我的钱包的唯一方式。 -desc_about= Benjamin Schulte于 2021 年为您带来 +desc_about= Benjamin Schulte于 {0} 年为您带来 desc_about_moreinfo=许可依据Apache 2 License\n\ 本项目是一个开源软件\n\ 请到Discord电报 diff --git a/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt index bf9b21bb7..2a0af2256 100644 --- a/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt @@ -36,7 +36,7 @@ class SettingsViewController : CoroutineViewController() { version.text = CrashHandler.getAppVersion() version.textAlignment = NSTextAlignment.Center val compiledBy = Body1Label() - compiledBy.text = texts.get(STRING_DESC_ABOUT) + compiledBy.text = texts.format(STRING_DESC_ABOUT, texts.get(STRING_ABOUT_YEAR)) compiledBy.textAlignment = NSTextAlignment.Center val moreInfo = UITextView(CGRect.Zero()).apply { From 93175eb2573f520514f2a866df425ed971cbf6f3 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 6 Jan 2022 20:58:53 +0100 Subject: [PATCH 02/25] iOS restore wallet make wordlist hint disappear on iPhone SE #54 --- .../org/ergoplatform/ios/wallet/RestoreWalletViewController.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/RestoreWalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/RestoreWalletViewController.kt index ea92b1113..cf96d1cf5 100644 --- a/ios/src/main/java/org/ergoplatform/ios/wallet/RestoreWalletViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/wallet/RestoreWalletViewController.kt @@ -70,6 +70,8 @@ class RestoreWalletViewController : ViewControllerWithKeyboardLayoutGuide() { descLabel.widthMatchesSuperview(false, DEFAULT_MARGIN).topToSuperview(false, DEFAULT_MARGIN) wordListHint.widthMatchesSuperview(false, DEFAULT_MARGIN) .topToBottomOf(descLabel, DEFAULT_MARGIN) + // make the word list get compressed on iPhone SE + wordListHint.setContentCompressionResistancePriority(500f, UILayoutConstraintAxis.Vertical) tvMnemonic.widthMatchesSuperview(false, DEFAULT_MARGIN) .topToBottomOf(wordListHint, DEFAULT_MARGIN * 2) errorLabel.widthMatchesSuperview().topToBottomOf(tvMnemonic) From 14e02208a09916b3fc8547816ae85bf0cf83e286 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 6 Jan 2022 21:47:39 +0100 Subject: [PATCH 03/25] Bring back ergo-wallet 4.0.15-snapshot for Java 7 compatibility #54 --- ios/build.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ios/build.gradle b/ios/build.gradle index b87be0f20..40d7942f8 100644 --- a/ios/build.gradle +++ b/ios/build.gradle @@ -103,9 +103,16 @@ dependencies { // normal compile no change iOS: 90 sec // exclude kiama, sigma-state, ergo-wallet: 20 sec //exclude group: 'org.scorexfoundation', module: 'sigma-state_2.11' - //exclude group: 'org.ergoplatform', module: 'ergo-wallet_2.11' + exclude group: 'org.ergoplatform', module: 'ergo-wallet_2.11' } implementation project(path: ':sqldelight') + // 4.0.15 snapshot for ergo-wallet with java 7 compatibility, + // TODO can be removed when appkit is on ergo-wallet 4.0.17+ + implementation ('org.ergoplatform:ergo-wallet_2.11:4.0.15-158-d0584eae-SNAPSHOT') { + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.bitbucket.inkytonik.kiama', module: 'kiama_2.11' + exclude group: 'com.google.guava', module: 'guava' + } implementation "com.squareup.sqldelight:sqlite-driver:$sqldelight_version" implementation "com.mobidevelop.robovm:robovm-rt:${robovm_version}" From e21031127ae9f2c90c0660e2855590397a4d1dc1 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 6 Jan 2022 21:58:06 +0100 Subject: [PATCH 04/25] iOS Compiled by label was missing #54 --- .../java/org/ergoplatform/ios/settings/SettingsViewController.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt index 2a0af2256..8e45b3458 100644 --- a/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/settings/SettingsViewController.kt @@ -73,6 +73,7 @@ class SettingsViewController : CoroutineViewController() { ergoLogo, title, version, + compiledBy, moreInfo, createHorizontalSeparator(), fiatCurrencyContainer, From cfe8b2bd488451f44ba885db560ff9335deb6bc6 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 7 Jan 2022 15:16:24 +0100 Subject: [PATCH 05/25] Introduce ColdWalletSigningUiLogic #58 --- .../ColdWalletSigningViewModel.kt | 105 ++++------------ .../transactions/ColdWalletSigningUiLogic.kt | 113 ++++++++++++++++++ 2 files changed, 137 insertions(+), 81 deletions(-) create mode 100644 common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt index 212d5b3c7..078730d27 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt @@ -5,82 +5,39 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.CoroutineScope import org.ergoplatform.android.AppDatabase -import org.ergoplatform.deserializeSecrets -import org.ergoplatform.signSerializedErgoTx +import org.ergoplatform.android.RoomWalletDbProvider import org.ergoplatform.api.AesEncryptionManager import org.ergoplatform.api.AndroidEncryptionManager +import org.ergoplatform.deserializeSecrets import org.ergoplatform.explorer.client.model.TransactionInfo -import org.ergoplatform.persistance.Wallet -import org.ergoplatform.transactions.PromptSigningResult import org.ergoplatform.transactions.SigningResult import org.ergoplatform.uilogic.StringProvider -import org.ergoplatform.wallet.getSortedDerivedAddressesList +import org.ergoplatform.uilogic.transactions.ColdWalletSigningUiLogic class ColdWalletSigningViewModel : ViewModel() { - var pagesQrCode = 0 - val pagesAdded get() = qrCodeChunks.size - val qrCodeChunks = HashMap() - var signedQrCode: List? = null - private set - private var signingRequest: PromptSigningResult? = null private val _transactionInfo = MutableLiveData() val transactionInfo: LiveData = _transactionInfo private val _lockInterface = MutableLiveData() val lockInterface: LiveData = _lockInterface private val _signingResult = MutableLiveData() val signingResult: LiveData = _signingResult - var wallet: Wallet? = null - - fun addQrCodeChunk(qrCodeChunk: String): Boolean { - // qr code not fitting, no qr code chunk, or we are already done? => don't add - if (transactionInfo.value != null || !isColdSigningRequestChunk(qrCodeChunk)) { - return false - } - - val page = getQrChunkIndex(qrCodeChunk) - val count = getQrChunkPagesCount(qrCodeChunk) + private val uiLogic = AndroidColdWalletSigningUiLogic() + val wallet get() = uiLogic.wallet + val signedQrCode get() = uiLogic.signedQrCode + val pagesQrCode get() = uiLogic.pagesQrCode + val pagesAdded get() = uiLogic.pagesAdded - if (pagesQrCode != 0 && count != pagesQrCode) { - return false - } - - qrCodeChunks.put(page, qrCodeChunk) - pagesQrCode = count - - val ti = buildRequestWhenApplicable() + fun addQrCodeChunk(qrCodeChunk: String) { + val ti = uiLogic.addQrCodeChunk(qrCodeChunk) _transactionInfo.postValue(ti) - - return true - } - - private fun buildRequestWhenApplicable(): TransactionInfo? { - if (pagesAdded == pagesQrCode) { - try { - val sr = coldSigningRequestFromQrChunks(qrCodeChunks.values) - signingRequest = sr - return buildTransactionInfoFromReduced( - sr.serializedTx!!, - sr.serializedInputs - ) - } catch (t: Throwable) { - - } - } - - return null } fun setWalletId(walletId: Int, ctx: Context) { - viewModelScope.launch { - wallet = - AppDatabase.getInstance(ctx).walletDao().loadWalletWithStateById(walletId)?.toModel() - } + uiLogic.setWalletId(walletId, RoomWalletDbProvider(AppDatabase.getInstance(ctx))) } fun signTxWithPassword(password: String, texts: StringProvider): Boolean { @@ -99,7 +56,7 @@ class ColdWalletSigningViewModel : ViewModel() { return false } - signTxWithMnemonicAsync(mnemonic, texts) + uiLogic.signTxWithMnemonicAsync(mnemonic, texts) return true } @@ -116,36 +73,22 @@ class ColdWalletSigningViewModel : ViewModel() { val decryptData = AndroidEncryptionManager.decryptDataWithDeviceKey(it) mnemonic = deserializeSecrets(String(decryptData!!)) - signTxWithMnemonicAsync(mnemonic!!, texts) + uiLogic.signTxWithMnemonicAsync(mnemonic!!, texts) } } - private fun signTxWithMnemonicAsync(mnemonic: String, texts: StringProvider) { - signingRequest?.let { signingRequest -> - val derivedAddresses = - wallet!!.getSortedDerivedAddressesList().map { it.derivationIndex } - - viewModelScope.launch { - val ergoTxResult: SigningResult - withContext(Dispatchers.IO) { - ergoTxResult = signSerializedErgoTx( - signingRequest.serializedTx!!, mnemonic, "", - derivedAddresses, texts - ) - signedQrCode = buildColdSigningResponse(ergoTxResult)?.let { - coldSigningResponseToQrChunks( - it, - QR_SIZE_LIMIT - ) - } - } - _lockInterface.postValue(false) - - _signingResult.postValue(ergoTxResult) - } + inner class AndroidColdWalletSigningUiLogic : ColdWalletSigningUiLogic() { + override val coroutineScope: CoroutineScope + get() = viewModelScope - _lockInterface.postValue(true) + override fun notifyUiLocked(locked: Boolean) { + _lockInterface.postValue(locked) } + + override fun notifySigningResult(ergoTxResult: SigningResult) { + _signingResult.postValue(ergoTxResult) + } + } } \ No newline at end of file diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt new file mode 100644 index 000000000..2c4ea81d3 --- /dev/null +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -0,0 +1,113 @@ +package org.ergoplatform.uilogic.transactions + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.ergoplatform.android.transactions.* +import org.ergoplatform.explorer.client.model.TransactionInfo +import org.ergoplatform.persistance.Wallet +import org.ergoplatform.persistance.WalletDbProvider +import org.ergoplatform.signSerializedErgoTx +import org.ergoplatform.transactions.PromptSigningResult +import org.ergoplatform.transactions.SigningResult +import org.ergoplatform.uilogic.StringProvider +import org.ergoplatform.wallet.getSortedDerivedAddressesList + +abstract class ColdWalletSigningUiLogic { + abstract val coroutineScope: CoroutineScope + + var pagesQrCode = 0 + val pagesAdded get() = qrCodeChunks.size + private val qrCodeChunks = HashMap() + var signedQrCode: List? = null + private set + private var signingRequest: PromptSigningResult? = null + + var wallet: Wallet? = null + private set + + private var transactionInfo: TransactionInfo? = null + + /** + * Adds QR code chunk when applicable + * + * @return TransactionInfo when it could be built, null otherwise + */ + fun addQrCodeChunk(qrCodeChunk: String): TransactionInfo? { + + // qr code not fitting, no qr code chunk, or we are already done? => don't add + if (transactionInfo != null || !isColdSigningRequestChunk(qrCodeChunk)) { + return transactionInfo + } + + val page = getQrChunkIndex(qrCodeChunk) + val count = getQrChunkPagesCount(qrCodeChunk) + + if (pagesQrCode != 0 && count != pagesQrCode) { + return transactionInfo + } + + qrCodeChunks.put(page, qrCodeChunk) + pagesQrCode = count + + transactionInfo = buildRequestWhenApplicable() + + return transactionInfo + } + + private fun buildRequestWhenApplicable(): TransactionInfo? { + if (pagesAdded == pagesQrCode) { + try { + val sr = coldSigningRequestFromQrChunks(qrCodeChunks.values) + signingRequest = sr + return buildTransactionInfoFromReduced( + sr.serializedTx!!, + sr.serializedInputs + ) + } catch (t: Throwable) { + + } + } + + return null + } + + fun setWalletId(walletId: Int, db: WalletDbProvider) { + coroutineScope.launch { + wallet = db.loadWalletWithStateById(walletId) + } + } + + fun signTxWithMnemonicAsync(mnemonic: String, texts: StringProvider) { + signingRequest?.let { signingRequest -> + val derivedAddresses = + wallet!!.getSortedDerivedAddressesList().map { it.derivationIndex } + + coroutineScope.launch { + val ergoTxResult: SigningResult + withContext(Dispatchers.IO) { + ergoTxResult = signSerializedErgoTx( + signingRequest.serializedTx!!, mnemonic, "", + derivedAddresses, texts + ) + signedQrCode = buildColdSigningResponse(ergoTxResult)?.let { + coldSigningResponseToQrChunks( + it, + QR_SIZE_LIMIT + ) + } + } + notifyUiLocked(false) + + notifySigningResult(ergoTxResult) + } + + notifyUiLocked(true) + } + } + + abstract fun notifyUiLocked(locked: Boolean) + abstract fun notifySigningResult(ergoTxResult: SigningResult) + +} \ No newline at end of file From 442fa2a3d4e6f7e74b1e1c78dc1623717c0f3f13 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 7 Jan 2022 16:36:47 +0100 Subject: [PATCH 06/25] Cold wallet present QR code scan errors to user #58 --- .../transactions/ColdWalletSigningFragment.kt | 86 ++++++++++--------- .../ColdWalletSigningViewModel.kt | 12 +-- .../layout/fragment_cold_wallet_signing.xml | 14 +++ .../transactions/ColdWalletSigningUiLogic.kt | 25 ++++-- 4 files changed, 80 insertions(+), 57 deletions(-) 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 b8aeb047c..f29646ef9 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt @@ -40,54 +40,16 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { private val viewModel: ColdWalletSigningViewModel get() { - val viewModel = ViewModelProvider(this).get(ColdWalletSigningViewModel::class.java) - return viewModel + return ViewModelProvider(this).get(ColdWalletSigningViewModel::class.java) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val viewModel = viewModel - args.qrCode?.let { viewModel.addQrCodeChunk(it) } + args.qrCode?.let { addQrCodeChunk(it) } viewModel.setWalletId(args.walletId, requireContext()) - viewModel.transactionInfo.observe(viewLifecycleOwner, { - // don't show transaction info when we already have a signing result - if (viewModel.signedQrCode != null) - return@observe - - if (it == null) { - // refresh information on scanned codes - binding.labelScannedPages.text = getString( - R.string.label_qr_pages_info, - viewModel.pagesAdded.toString(), - viewModel.pagesQrCode.toString() - ) - binding.cardScanMore.visibility = View.VISIBLE - } - - it?.reduceBoxes()?.let { - binding.transactionInfo.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) - } - } - } - }) - viewModel.lockInterface.observe(viewLifecycleOwner, { if (it) ProgressBottomSheetDialogFragment.showProgressDialog(childFragmentManager) @@ -153,6 +115,48 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { }) } + private fun addQrCodeChunk(qrCode: String) { + val transactionInfo = viewModel.uiLogic.addQrCodeChunk(qrCode) + + // don't show transaction info when we already have a signing result + if (viewModel.signedQrCode != null) + return + + if (transactionInfo == null) { + // refresh information on scanned codes + binding.labelScannedPages.text = getString( + R.string.label_qr_pages_info, + viewModel.uiLogic.pagesAdded.toString(), + viewModel.uiLogic.pagesQrCode.toString() + ) + binding.cardScanMore.visibility = View.VISIBLE + val errorMessage = viewModel.uiLogic.lastErrorMessage + binding.labelErrorMessage.visibility = if (errorMessage.isNullOrBlank()) View.GONE else View.VISIBLE + binding.labelErrorMessage.text = errorMessage + } + + transactionInfo?.reduceBoxes()?.let { + binding.transactionInfo.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) + } + } + } + } + private fun refreshButtonState() { val lastPage = binding.qrCodePager.currentItem + 1 == binding.qrCodePager.adapter!!.itemCount @@ -210,7 +214,7 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { result.contents?.let { - viewModel.addQrCodeChunk(it) + addQrCodeChunk(it) } } else { super.onActivityResult(requestCode, resultCode, data) diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt index 078730d27..3d4e29d7e 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningViewModel.kt @@ -11,30 +11,20 @@ import org.ergoplatform.android.RoomWalletDbProvider import org.ergoplatform.api.AesEncryptionManager import org.ergoplatform.api.AndroidEncryptionManager import org.ergoplatform.deserializeSecrets -import org.ergoplatform.explorer.client.model.TransactionInfo import org.ergoplatform.transactions.SigningResult import org.ergoplatform.uilogic.StringProvider import org.ergoplatform.uilogic.transactions.ColdWalletSigningUiLogic class ColdWalletSigningViewModel : ViewModel() { - private val _transactionInfo = MutableLiveData() - val transactionInfo: LiveData = _transactionInfo private val _lockInterface = MutableLiveData() val lockInterface: LiveData = _lockInterface private val _signingResult = MutableLiveData() val signingResult: LiveData = _signingResult - private val uiLogic = AndroidColdWalletSigningUiLogic() + val uiLogic = AndroidColdWalletSigningUiLogic() val wallet get() = uiLogic.wallet val signedQrCode get() = uiLogic.signedQrCode - val pagesQrCode get() = uiLogic.pagesQrCode - val pagesAdded get() = uiLogic.pagesAdded - - fun addQrCodeChunk(qrCodeChunk: String) { - val ti = uiLogic.addQrCodeChunk(qrCodeChunk) - _transactionInfo.postValue(ti) - } fun setWalletId(walletId: Int, ctx: Context) { uiLogic.setWalletId(walletId, RoomWalletDbProvider(AppDatabase.getInstance(ctx))) diff --git a/android/src/main/res/layout/fragment_cold_wallet_signing.xml b/android/src/main/res/layout/fragment_cold_wallet_signing.xml index e2cc656f5..16993a09d 100644 --- a/android/src/main/res/layout/fragment_cold_wallet_signing.xml +++ b/android/src/main/res/layout/fragment_cold_wallet_signing.xml @@ -213,6 +213,20 @@ android:gravity="center" tools:text="1 of 2" /> + + don't add - if (transactionInfo != null || !isColdSigningRequestChunk(qrCodeChunk)) { + // are we are already done? => don't add + if (transactionInfo != null) { return transactionInfo } + lastErrorMessage = null + + // qr code not fitting or no qr code chunk + if (!isColdSigningRequestChunk(qrCodeChunk)) { + lastErrorMessage = "Not a cold signing QR code" + return null + } + val page = getQrChunkIndex(qrCodeChunk) val count = getQrChunkPagesCount(qrCodeChunk) if (pagesQrCode != 0 && count != pagesQrCode) { - return transactionInfo + lastErrorMessage = "QR code does not belong to the formerly scanned codes" + return null } qrCodeChunks.put(page, qrCodeChunk) @@ -66,7 +79,9 @@ abstract class ColdWalletSigningUiLogic { sr.serializedInputs ) } catch (t: Throwable) { - + LogUtils.logDebug("ColdWalletSigning", "Error thrown on signing", t) + val message = t.message ?: t.javaClass.name + lastErrorMessage = "Error: $message" } } From f42cb44bb50f8093c1870583df65dce168efa277 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 7 Jan 2022 19:59:16 +0100 Subject: [PATCH 07/25] ColdWalletRequest and Response: disable HTML escaping, throw exception when input boxes are missing #58 --- .../android/transactions/SendFundsFragment.kt | 1 + .../SigningPromptDialogFragment.kt | 4 ++ .../transactions/ColdWalletUtils.kt | 39 +++++++++---------- .../transactions/ColdWalletSigningUiLogic.kt | 4 +- .../uilogic/transactions/SendFundsUiLogic.kt | 4 +- .../transactions/ColdWalletUtilsKtTest.kt | 20 +++++----- 6 files changed, 37 insertions(+), 35 deletions(-) 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 3957f35d7..1196b3173 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt @@ -32,6 +32,7 @@ import org.ergoplatform.persistance.WalletConfig import org.ergoplatform.persistance.WalletToken import org.ergoplatform.tokens.isSingularToken import org.ergoplatform.transactions.PromptSigningResult +import org.ergoplatform.transactions.isColdSigningRequestChunk import org.ergoplatform.utils.formatFiatToString import org.ergoplatform.wallet.addresses.getAddressLabel import org.ergoplatform.wallet.getNumOfAddresses 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 ccc9a2e86..396f19388 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -16,6 +16,10 @@ import org.ergoplatform.android.R import org.ergoplatform.android.databinding.FragmentPromptSigningDialogBinding import org.ergoplatform.android.ui.AndroidStringProvider import org.ergoplatform.android.ui.QrPagerAdapter +import org.ergoplatform.transactions.QR_SIZE_LIMIT +import org.ergoplatform.transactions.coldSigninRequestToQrChunks +import org.ergoplatform.transactions.getQrChunkPagesCount +import org.ergoplatform.transactions.isColdSignedTxChunk class SigningPromptDialogFragment : BottomSheetDialogFragment() { private var _binding: FragmentPromptSigningDialogBinding? = null diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt index 6da5df943..a563d9805 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -1,9 +1,6 @@ -package org.ergoplatform.android.transactions +package org.ergoplatform.transactions -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.google.gson.JsonParser +import com.google.gson.* import org.ergoplatform.ErgoBox import org.ergoplatform.deserializeErgobox import org.ergoplatform.deserializeUnsignedTx @@ -15,8 +12,6 @@ import org.ergoplatform.explorer.client.model.AssetInstanceInfo import org.ergoplatform.explorer.client.model.InputInfo import org.ergoplatform.explorer.client.model.OutputInfo import org.ergoplatform.explorer.client.model.TransactionInfo -import org.ergoplatform.transactions.PromptSigningResult -import org.ergoplatform.transactions.SigningResult import org.ergoplatform.utils.Base64Coder import scala.Tuple2 import scala.collection.JavaConversions @@ -32,7 +27,7 @@ private const val JSON_FIELD_INPUTS = "inputs" */ fun buildColdSigningRequest(data: PromptSigningResult): String? { if (data.success) { - val gson = Gson() + val gson = GsonBuilder().disableHtmlEscaping().create() val root = JsonObject() root.addProperty(JSON_FIELD_REDUCED_TX, String(Base64Coder.encode(data.serializedTx!!))) root.addProperty(JSON_FIELD_SENDER, data.address) @@ -49,7 +44,7 @@ fun buildColdSigningRequest(data: PromptSigningResult): String? { fun buildColdSigningResponse(data: SigningResult): String? { if (data.success) { - val gson = Gson() + val gson = GsonBuilder().disableHtmlEscaping().create() val root = JsonObject() root.addProperty(JSON_FIELD_SIGNED_TX, String(Base64Coder.encode(data.serializedTx!!))) return gson.toJson(root) @@ -237,25 +232,27 @@ fun buildTransactionInfoFromReduced( val boxid = ErgoId(it.boxId()).toString() val inputInfo = InputInfo() inputInfo.boxId = boxid - inputBoxes.get(boxid)?.let { - inputInfo.address = Address.fromErgoTree(it.ergoTree(), getErgoNetworkType()).toString() - inputInfo.value = it.value() - getAssetInstanceInfos(it.additionalTokens()).forEach { - inputInfo.addAssetsItem(it) + inputBoxes.get(boxid)?.let { ergoBox -> + inputInfo.address = + Address.fromErgoTree(ergoBox.ergoTree(), getErgoNetworkType()).toString() + inputInfo.value = ergoBox.value() + getAssetInstanceInfos(ergoBox.additionalTokens()).forEach { assetsItem -> + inputInfo.addAssetsItem(assetsItem) } - } + } ?: throw java.lang.IllegalArgumentException("No information for input box $boxid") retVal.addInputsItem(inputInfo) } - JavaConversions.seqAsJavaList(unsignedTx.outputCandidates())!!.forEach { + JavaConversions.seqAsJavaList(unsignedTx.outputCandidates())!!.forEach { ergoBoxCandidate -> val outputInfo = OutputInfo() - outputInfo.address = Address.fromErgoTree(it.ergoTree(), getErgoNetworkType()).toString() - outputInfo.value = it.value() + outputInfo.address = + Address.fromErgoTree(ergoBoxCandidate.ergoTree(), getErgoNetworkType()).toString() + outputInfo.value = ergoBoxCandidate.value() retVal.addOutputsItem(outputInfo) - getAssetInstanceInfos(it.additionalTokens()).forEach { + getAssetInstanceInfos(ergoBoxCandidate.additionalTokens()).forEach { outputInfo.addAssetsItem(it) } } @@ -264,8 +261,8 @@ fun buildTransactionInfoFromReduced( } -private fun getAssetInstanceInfos(tokens: Coll>): List { - val tokens = Iso.isoTokensListToPairsColl().from(tokens) +private fun getAssetInstanceInfos(tokensColl: Coll>): List { + val tokens = Iso.isoTokensListToPairsColl().from(tokensColl) return tokens.map { val tokenInfo = AssetInstanceInfo() tokenInfo.amount = it.value diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt index b01fe0195..1847b97ce 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -4,13 +4,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.ergoplatform.android.transactions.* import org.ergoplatform.explorer.client.model.TransactionInfo import org.ergoplatform.persistance.Wallet import org.ergoplatform.persistance.WalletDbProvider import org.ergoplatform.signSerializedErgoTx -import org.ergoplatform.transactions.PromptSigningResult -import org.ergoplatform.transactions.SigningResult +import org.ergoplatform.transactions.* import org.ergoplatform.uilogic.StringProvider import org.ergoplatform.utils.LogUtils import org.ergoplatform.wallet.getSortedDerivedAddressesList diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt index 82fcf06a5..a10d96e62 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt @@ -5,8 +5,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.ergoplatform.* -import org.ergoplatform.android.transactions.buildColdSigningRequest -import org.ergoplatform.android.transactions.coldSigningResponseFromQrChunks +import org.ergoplatform.transactions.buildColdSigningRequest +import org.ergoplatform.transactions.coldSigningResponseFromQrChunks import org.ergoplatform.appkit.Address import org.ergoplatform.appkit.ErgoToken import org.ergoplatform.appkit.Parameters diff --git a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt index 539c54d5c..b7445d6cf 100644 --- a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt +++ b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt @@ -1,7 +1,6 @@ package org.ergoplatform.transactions import org.ergoplatform.isErgoMainNet -import org.ergoplatform.android.transactions.* import org.junit.Assert import org.junit.Test @@ -62,7 +61,9 @@ class ColdWalletUtilsKtTest { Assert.assertEquals(2, ti.reduceBoxes().outputs.size) val csr2 = - parseColdSigningRequest("{\"reducedTx\":\"0QMDCp28G2Gct69t0D+IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk+W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE/q8m9DAACuLv3AeVnq+cluQls7Kz/8+YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j/RElAYGZUMXvVFoZRIO1wKVKrG/n/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dzaVi/TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7/HRyQIACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw/QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/QjAE\\u003d\",\"sender\":\"3WvxRdGA2Ce3otzqtc7jUb61H67NiugArk9mTCxKwMQjrKgsjwwj\",\"inputs\":[\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITqlywQAAGRGq63RnvOuuPeM07c0y1ZzwTcilEjMaNkS/Ws2WuLVAA\\u003d\\u003d\",\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITrk5wQAAPRQZ9K80uxdbpd1Zwx4DA1zakyULHJh/yfOl8ocdz6uAA\\u003d\\u003d\",\"gKr548cCAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE67OcEBPn/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dAc2lYv06TcqRy8IHWpEaOlsRt+2BAJexrFpUt2Kod7BigNDbw/QCUUCDoXD8c0BxwHdI/0RJQGBmVDF71RaGUSDtcClSqxvju9vD9AKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgNDYlq0DANXS4I5Ac3ygmxQ6/8SYHikzLoKK0T4jDBvcoZVjWvIvAg\\u003d\\u003d\"]}") + parseColdSigningRequest( + "{\"reducedTx\":\"0QMDCp28G2Gct69t0D+IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk+W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE/q8m9DAACuLv3AeVnq+cluQls7Kz/8+YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j/RElAYGZUMXvVFoZRIO1wKVKrG/n/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dzaVi/TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7/HRyQIACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw/QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/QjAE\\u003d\",\"sender\":\"3WvxRdGA2Ce3otzqtc7jUb61H67NiugArk9mTCxKwMQjrKgsjwwj\",\"inputs\":[\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITqlywQAAGRGq63RnvOuuPeM07c0y1ZzwTcilEjMaNkS/Ws2WuLVAA\\u003d\\u003d\",\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITrk5wQAAPRQZ9K80uxdbpd1Zwx4DA1zakyULHJh/yfOl8ocdz6uAA\\u003d\\u003d\",\"gKr548cCAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE67OcEBPn/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dAc2lYv06TcqRy8IHWpEaOlsRt+2BAJexrFpUt2Kod7BigNDbw/QCUUCDoXD8c0BxwHdI/0RJQGBmVDF71RaGUSDtcClSqxvju9vD9AKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgNDYlq0DANXS4I5Ac3ygmxQ6/8SYHikzLoKK0T4jDBvcoZVjWvIvAg\\u003d\\u003d\"]}" + ) val ti2 = buildTransactionInfoFromReduced(csr2.serializedTx!!, csr2.serializedInputs) Assert.assertNotNull(ti2.outputs) @@ -80,15 +81,16 @@ class ColdWalletUtilsKtTest { Assert.assertEquals(4, ti2.outputs.last().assets.size) Assert.assertEquals(tokenAmountBeforeReduce, ti2.outputs.last().assets.last().amount) - // same without input boxes + // exception raised without input boxes val csr3 = parseColdSigningRequest("{\"reducedTx\":\"0QMDCp28G2Gct69t0D+IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk+W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE/q8m9DAACuLv3AeVnq+cluQls7Kz/8+YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j/RElAYGZUMXvVFoZRIO1wKVKrG/n/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dzaVi/TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7/HRyQIACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw/QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/QjAE\\u003d\"}") - val ti3 = buildTransactionInfoFromReduced(csr3.serializedTx!!, csr3.serializedInputs) - val ti3b = ti3.reduceBoxes() - Assert.assertEquals(3, ti3.inputs.size) - Assert.assertEquals(3, ti3b.inputs.size) - Assert.assertEquals(3, ti3.outputs.size) - Assert.assertEquals(3, ti3b.outputs.size) + var exceptionThrown = false + try { + buildTransactionInfoFromReduced(csr3.serializedTx!!, csr3.serializedInputs) + } catch (t: Throwable) { + exceptionThrown = true + } + Assert.assertTrue(exceptionThrown) } } \ No newline at end of file From 37b2d4dde9cb133793b4b516831c3eb03ec1085a Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 7 Jan 2022 21:18:38 +0100 Subject: [PATCH 08/25] ColdWalletRequest and Response: change QR chunk format to json #58 --- .../SigningPromptDialogFragment.kt | 16 +-- .../transactions/ColdWalletUtils.kt | 111 +++++++++--------- .../transactions/ColdWalletSigningUiLogic.kt | 8 +- .../transactions/ColdWalletUtilsKtTest.kt | 15 +-- 4 files changed, 75 insertions(+), 75 deletions(-) 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 396f19388..242086231 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -17,9 +17,8 @@ import org.ergoplatform.android.databinding.FragmentPromptSigningDialogBinding import org.ergoplatform.android.ui.AndroidStringProvider import org.ergoplatform.android.ui.QrPagerAdapter import org.ergoplatform.transactions.QR_SIZE_LIMIT -import org.ergoplatform.transactions.coldSigninRequestToQrChunks -import org.ergoplatform.transactions.getQrChunkPagesCount -import org.ergoplatform.transactions.isColdSignedTxChunk +import org.ergoplatform.transactions.coldSigningRequestToQrChunks +import org.ergoplatform.transactions.getColdSignedTxChunk class SigningPromptDialogFragment : BottomSheetDialogFragment() { private var _binding: FragmentPromptSigningDialogBinding? = null @@ -39,7 +38,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { val viewModel = getViewModel() viewModel.signingPromptData.observe(viewLifecycleOwner, { it?.let { - val qrPages = coldSigninRequestToQrChunks(it, QR_SIZE_LIMIT) + val qrPages = coldSigningRequestToQrChunks(it, QR_SIZE_LIMIT) binding.qrCodePager.adapter = QrPagerAdapter(qrPages) refreshButtonState() @@ -74,9 +73,10 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { - result.contents?.let { - if (isColdSignedTxChunk(it)) { - if (getQrChunkPagesCount(it) > 1) { + result.contents?.let { qrCode -> + val qrChunk = getColdSignedTxChunk(qrCode) + qrChunk?.let { + if (it.pages > 1) { // TODO handle paged QR codes Snackbar.make( requireView(), @@ -86,7 +86,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { } else { val context = requireContext() getViewModel().uiLogic.sendColdWalletSignedTx( - listOf(it), + listOf(qrCode), Preferences(context), AndroidStringProvider(context) ) diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt index a563d9805..59d3e598f 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -1,17 +1,20 @@ package org.ergoplatform.transactions -import com.google.gson.* +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser import org.ergoplatform.ErgoBox -import org.ergoplatform.deserializeErgobox -import org.ergoplatform.deserializeUnsignedTx -import org.ergoplatform.getErgoNetworkType import org.ergoplatform.appkit.Address import org.ergoplatform.appkit.ErgoId import org.ergoplatform.appkit.Iso +import org.ergoplatform.deserializeErgobox +import org.ergoplatform.deserializeUnsignedTx import org.ergoplatform.explorer.client.model.AssetInstanceInfo import org.ergoplatform.explorer.client.model.InputInfo import org.ergoplatform.explorer.client.model.OutputInfo import org.ergoplatform.explorer.client.model.TransactionInfo +import org.ergoplatform.getErgoNetworkType import org.ergoplatform.utils.Base64Coder import scala.Tuple2 import scala.collection.JavaConversions @@ -94,17 +97,19 @@ fun parseColdSigningResponse(qrData: String): SigningResult { } const val QR_SIZE_LIMIT = 2900 -private const val QR_PREFIX_COLD_SIGNING_REQUEST = "CSR" -private const val QR_PREFIX_COLD_SIGNED_TX = "CSTX" -private const val QR_PREFIX_DATA_DELIMITER = '-' -private const val QR_PREFIX_HEADER_DELIMITER = '/' +private const val QR_PROPERTY_COLD_SIGNING_REQUEST = "CSR" +private const val QR_PROPERTY_COLD_SIGNED_TX = "CSTX" +private const val QR_PROPERTY_INDEX = "p" +private const val QR_PROPERTY_PAGES = "n" + +data class QrChunk(val index: Int, val pages: Int, val data: String) -fun coldSigninRequestToQrChunks(serializedSigningRequest: String, sizeLimit: Int): List { - return buildQrChunks(QR_PREFIX_COLD_SIGNING_REQUEST, sizeLimit, serializedSigningRequest) +fun coldSigningRequestToQrChunks(serializedSigningRequest: String, sizeLimit: Int): List { + return buildQrChunks(QR_PROPERTY_COLD_SIGNING_REQUEST, sizeLimit, serializedSigningRequest) } fun coldSigningResponseToQrChunks(serializedSigningRequest: String, sizeLimit: Int): List { - return buildQrChunks(QR_PREFIX_COLD_SIGNED_TX, sizeLimit, serializedSigningRequest) + return buildQrChunks(QR_PROPERTY_COLD_SIGNED_TX, sizeLimit, serializedSigningRequest) } private fun buildQrChunks( @@ -112,28 +117,31 @@ private fun buildQrChunks( sizeLimit: Int, serializedSigningRequest: String ): List { - val actualSizeLimit = sizeLimit - 20 // reserve some space for our prefix + val actualSizeLimit = sizeLimit - 20 - prefix.length // reserve some space for our prefix + + val gson = GsonBuilder().disableHtmlEscaping().create() if (serializedSigningRequest.length <= actualSizeLimit) { - return listOf(prefix + QR_PREFIX_DATA_DELIMITER + serializedSigningRequest) + val root = JsonObject() + root.addProperty(prefix, serializedSigningRequest) + return listOf(gson.toJson(root)) } else { val chunks = serializedSigningRequest.chunked(sizeLimit) var slice = 0 return chunks.map { slice++ - prefix + QR_PREFIX_HEADER_DELIMITER + slice.toString() + QR_PREFIX_HEADER_DELIMITER + chunks.size + QR_PREFIX_DATA_DELIMITER + it + val root = JsonObject() + root.addProperty(prefix, it) + root.addProperty(QR_PROPERTY_PAGES, chunks.size) + root.addProperty(QR_PROPERTY_INDEX, slice) + gson.toJson(root) } } } fun coldSigningRequestFromQrChunks(qrChunks: Collection): PromptSigningResult { try { - // check the list - qrChunks.forEach { - if (!isColdSigningRequestChunk(it)) - throw IllegalArgumentException("Not a cold signing request chunk") - } - val qrdata = joinQrCodeChunks(qrChunks) + val qrdata = joinQrCodeChunks(qrChunks, QR_PROPERTY_COLD_SIGNING_REQUEST) return parseColdSigningRequest(qrdata) @@ -144,11 +152,7 @@ fun coldSigningRequestFromQrChunks(qrChunks: Collection): PromptSigningR fun coldSigningResponseFromQrChunks(qrChunks: Collection): SigningResult { try { - qrChunks.forEach { - if (!isColdSignedTxChunk(it)) - throw IllegalArgumentException("Not a cold signing request chunk") - } - val qrdata = joinQrCodeChunks(qrChunks) + val qrdata = joinQrCodeChunks(qrChunks, QR_PROPERTY_COLD_SIGNED_TX) return parseColdSigningResponse(qrdata) @@ -157,48 +161,45 @@ fun coldSigningResponseFromQrChunks(qrChunks: Collection): SigningResult } } -private fun joinQrCodeChunks(qrChunks: Collection): String { - val chunksSorted = qrChunks.sortedBy { getQrChunkIndex(it) }.map { - if (getQrChunkPagesCount(it) != qrChunks.size) +private fun joinQrCodeChunks(qrChunks: Collection, property: String): String { + val qrList = qrChunks.map { parseQrChunk(it, property) } + val chunksSorted = qrList.sortedBy { it.index }.map { + if (it.pages != qrChunks.size) throw IllegalArgumentException("QR code chunk sizes differ") - it.substringAfter( - QR_PREFIX_DATA_DELIMITER - ) + it.data } - val qrdata = chunksSorted.joinToString("") { it } - return qrdata + return chunksSorted.joinToString("") { it } } fun isColdSigningRequestChunk(chunk: String): Boolean { - return chunk.startsWith(QR_PREFIX_COLD_SIGNING_REQUEST) && - chunk.contains(QR_PREFIX_DATA_DELIMITER) + return getColdSigningRequestChunk(chunk) != null } -fun isColdSignedTxChunk(chunk: String): Boolean { - return chunk.startsWith(QR_PREFIX_COLD_SIGNED_TX) && - chunk.contains(QR_PREFIX_DATA_DELIMITER) +fun getColdSigningRequestChunk(chunk: String): QrChunk? { + return try { + parseQrChunk(chunk, QR_PROPERTY_COLD_SIGNING_REQUEST) + } catch (t: Throwable) { + null + } } -fun getQrChunkIndex(chunk: String): Int { - val chunkWithoutData = chunk.substringBefore(QR_PREFIX_DATA_DELIMITER) - if (!chunkWithoutData.contains(QR_PREFIX_HEADER_DELIMITER)) - return 0 - - val chunkWithoutHeaderPrefix = chunk.substringAfter(QR_PREFIX_HEADER_DELIMITER) - val pages = chunkWithoutHeaderPrefix.substringBefore(QR_PREFIX_DATA_DELIMITER) - .split(QR_PREFIX_HEADER_DELIMITER) - return pages.firstOrNull()?.toIntOrNull() ?: 0 +fun getColdSignedTxChunk(chunk: String): QrChunk? { + return try { + parseQrChunk(chunk, QR_PROPERTY_COLD_SIGNED_TX) + } catch (t: Throwable) { + null + } } -fun getQrChunkPagesCount(chunk: String): Int { - val chunkWithoutData = chunk.substringBefore(QR_PREFIX_DATA_DELIMITER) - if (!chunkWithoutData.contains(QR_PREFIX_HEADER_DELIMITER)) - return 1 - - val pages = chunkWithoutData.substringBefore(QR_PREFIX_DATA_DELIMITER) - .split(QR_PREFIX_HEADER_DELIMITER) - return pages.lastOrNull()?.toIntOrNull() ?: 1 +private fun parseQrChunk(chunk: String, property: String): QrChunk { + val jsonTree = JsonParser().parse(chunk) as JsonObject + if (!jsonTree.has(property)) { + throw java.lang.IllegalArgumentException("QR code does not contain element $property") + } + val index = if (jsonTree.has(QR_PROPERTY_INDEX)) jsonTree.get(QR_PROPERTY_INDEX).asInt else 1 + val pages = if (jsonTree.has(QR_PROPERTY_PAGES)) jsonTree.get(QR_PROPERTY_PAGES).asInt else 1 + return QrChunk(index, pages, jsonTree.get(property).asString) } /* diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt index 1847b97ce..a044599da 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -45,14 +45,16 @@ abstract class ColdWalletSigningUiLogic { lastErrorMessage = null + val qrChunk = getColdSigningRequestChunk(qrCodeChunk) + // qr code not fitting or no qr code chunk - if (!isColdSigningRequestChunk(qrCodeChunk)) { + if (qrChunk == null) { lastErrorMessage = "Not a cold signing QR code" return null } - val page = getQrChunkIndex(qrCodeChunk) - val count = getQrChunkPagesCount(qrCodeChunk) + val page = qrChunk.index + val count = qrChunk.pages if (pagesQrCode != 0 && count != pagesQrCode) { lastErrorMessage = "QR code does not belong to the formerly scanned codes" diff --git a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt index b7445d6cf..e9b4fa9a4 100644 --- a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt +++ b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt @@ -18,16 +18,13 @@ class ColdWalletUtilsKtTest { Assert.assertNotNull(csr) - val manyChunks = coldSigninRequestToQrChunks(csr!!, 30) - val oneChunk = coldSigninRequestToQrChunks(csr, 50000000) + val manyChunks = coldSigningRequestToQrChunks(csr!!, 30) + val oneChunk = coldSigningRequestToQrChunks(csr, 50000000) Assert.assertEquals(1, oneChunk.size) - Assert.assertEquals(0, getQrChunkIndex(oneChunk.first())) - Assert.assertEquals(1, getQrChunkIndex(manyChunks.first())) - Assert.assertEquals(1, getQrChunkPagesCount(oneChunk.first())) - Assert.assertEquals( - manyChunks.size, - getQrChunkPagesCount(manyChunks.first()) - ) + Assert.assertEquals(1, getColdSigningRequestChunk(oneChunk.first())?.index) + Assert.assertEquals(1, getColdSigningRequestChunk(manyChunks.first())?.index) + Assert.assertEquals(1, getColdSigningRequestChunk(oneChunk.first())?.pages) + Assert.assertEquals(manyChunks.size, getColdSigningRequestChunk(manyChunks.first())?.pages) val prompt1 = coldSigningRequestFromQrChunks(manyChunks) val prompt2 = coldSigningRequestFromQrChunks(oneChunk) From 67050a32089429f4d85f0b0b268f7922aeffea06 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 8 Jan 2022 15:59:06 +0100 Subject: [PATCH 09/25] ColdWalletSigningFragment Make sure transaction info is shown after configuration change #58 --- .../android/transactions/ColdWalletSigningFragment.kt | 11 +++++++---- .../uilogic/transactions/ColdWalletSigningUiLogic.kt | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) 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 f29646ef9..be9759a03 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt @@ -47,7 +47,7 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { super.onViewCreated(view, savedInstanceState) val viewModel = viewModel - args.qrCode?.let { addQrCodeChunk(it) } + addQrCodeChunk(args.qrCode) viewModel.setWalletId(args.walletId, requireContext()) viewModel.lockInterface.observe(viewLifecycleOwner, { @@ -115,8 +115,10 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { }) } - private fun addQrCodeChunk(qrCode: String) { - val transactionInfo = viewModel.uiLogic.addQrCodeChunk(qrCode) + private fun addQrCodeChunk(qrCode: String?) { + qrCode?.let { viewModel.uiLogic.addQrCodeChunk(qrCode) } + + val transactionInfo = viewModel.uiLogic.transactionInfo // don't show transaction info when we already have a signing result if (viewModel.signedQrCode != null) @@ -131,7 +133,8 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { ) binding.cardScanMore.visibility = View.VISIBLE val errorMessage = viewModel.uiLogic.lastErrorMessage - binding.labelErrorMessage.visibility = if (errorMessage.isNullOrBlank()) View.GONE else View.VISIBLE + binding.labelErrorMessage.visibility = + if (errorMessage.isNullOrBlank()) View.GONE else View.VISIBLE binding.labelErrorMessage.text = errorMessage } diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt index a044599da..ea6fc70f6 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -26,7 +26,8 @@ abstract class ColdWalletSigningUiLogic { var wallet: Wallet? = null private set - private var transactionInfo: TransactionInfo? = null + var transactionInfo: TransactionInfo? = null + private set var lastErrorMessage: String? = null private set From e3b96e7eef139e0f12e1e87b315e43fb3480868c Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 8 Jan 2022 19:32:46 +0100 Subject: [PATCH 10/25] RoboVM updated to 2.3.15 --- build.gradle | 2 +- gradle/verification-metadata.xml | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index a899be474..0124e3b8e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = "1.5.31" - ext.robovm_version = "2.3.14" + ext.robovm_version = "2.3.15" ext.sqldelight_version = "1.5.1" ext.coroutines_version = "1.4.1" ext.mockito_version = "4.0.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 720d18122..c70395fa4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -864,29 +864,29 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 661cf71cb0f811f1a55fbb3605bf5e810d5c7847 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 8 Jan 2022 22:32:27 +0100 Subject: [PATCH 11/25] iOS Cold wallet SigningPromptViewController introduced #54 --- .../transactions/SendFundsViewModel.kt | 2 +- .../SigningPromptDialogFragment.kt | 2 +- .../uilogic/transactions/SendFundsUiLogic.kt | 6 +- .../transactions/SendFundsUiLogicTest.kt | 2 +- .../ReceiveToWalletViewController.kt | 9 +- .../transactions/SendFundsViewController.kt | 37 ++++-- .../SigningPromptViewController.kt | 114 ++++++++++++++++++ .../ergoplatform/ios/ui/TinyConstraints.kt | 19 +++ .../java/org/ergoplatform/ios/ui/UIUtils.kt | 14 ++- 9 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt 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 e8f2a95bc..5842d6ce1 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt @@ -141,7 +141,7 @@ class SendFundsViewModel : ViewModel() { _txWorkDoneLiveData.postValue(txResult) } - override fun notifyHasSigningPromptData(signingPrompt: String?) { + override fun notifyHasSigningPromptData(signingPrompt: String) { _signingPromptData.postValue(signingPrompt) } } 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 242086231..bb94b1dbf 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -77,7 +77,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { val qrChunk = getColdSignedTxChunk(qrCode) qrChunk?.let { if (it.pages > 1) { - // TODO handle paged QR codes + // TODO cold wallet handle paged QR codes Snackbar.make( requireView(), R.string.error_qr_pages_num, diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt index a10d96e62..b31d422f6 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt @@ -224,7 +224,9 @@ abstract class SendFundsUiLogic { } notifyUiLocked(false) if (serializedTx.success) { - notifyHasSigningPromptData(buildColdSigningRequest(serializedTx)) + buildColdSigningRequest(serializedTx)?.let { + notifyHasSigningPromptData(it) + } } notifyHasErgoTxResult(serializedTx) } @@ -366,7 +368,7 @@ abstract class SendFundsUiLogic { abstract fun notifyUiLocked(locked: Boolean) abstract fun notifyHasTxId(txId: String) abstract fun notifyHasErgoTxResult(txResult: TransactionResult) - abstract fun notifyHasSigningPromptData(signingPrompt: String?) + abstract fun notifyHasSigningPromptData(signingPrompt: String) data class CheckCanPayResponse( val canPay: Boolean, diff --git a/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt b/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt index bae395794..cd76fea2f 100644 --- a/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt +++ b/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt @@ -146,7 +146,7 @@ class SendFundsUiLogicTest { } - override fun notifyHasSigningPromptData(signingPrompt: String?) { + override fun notifyHasSigningPromptData(signingPrompt: String) { } diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ReceiveToWalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ReceiveToWalletViewController.kt index 0d4e5a470..778317e7b 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/ReceiveToWalletViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ReceiveToWalletViewController.kt @@ -63,7 +63,9 @@ class ReceiveToWalletViewController(private val walletId: Int, derivationIdx: In } navigationController.topViewController.navigationItem.rightBarButtonItem = uiBarButtonItem - qrCode = UIImageView(CGRect.Zero()) + qrCode = UIImageView(CGRect.Zero()).apply { + contentMode = UIViewContentMode.ScaleAspectFit + } val qrCodeContainer = UIView() qrCodeContainer.addSubview(qrCode) addressLabel = Headline2Label() @@ -102,7 +104,8 @@ class ReceiveToWalletViewController(private val walletId: Int, derivationIdx: In stackView.setCustomSpacing(DEFAULT_MARGIN * 2, qrCodeContainer) stackView.setCustomSpacing(DEFAULT_MARGIN * 2, addressLabel) val scrollView = container.wrapInVerticalScrollView() - qrCode.fixedWidth(300.0).fixedHeight(300.0).centerHorizontal().topToSuperview().bottomToSuperview() + qrCode.fixedWidth(DEFAULT_QR_CODE_SIZE).fixedHeight(DEFAULT_QR_CODE_SIZE).centerHorizontal().topToSuperview() + .bottomToSuperview() container.addSubview(stackView) stackView.topToSuperview(topInset = DEFAULT_MARGIN) .widthMatchesSuperview(inset = DEFAULT_MARGIN, maxWidth = MAX_WIDTH) @@ -136,7 +139,7 @@ class ReceiveToWalletViewController(private val walletId: Int, derivationIdx: In private fun refreshQrCode() { uiLogic.getTextToShare(getInputAmount(), getInputPurpose())?.let { - qrCode.setQrCode(it, 300) + qrCode.setQrCode(it, DEFAULT_QR_CODE_SIZE) } } diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 2a300060a..67916d4bc 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -3,10 +3,14 @@ package org.ergoplatform.ios.transactions import com.badlogic.gdx.utils.I18NBundle import kotlinx.coroutines.CoroutineScope import org.ergoplatform.* +import org.ergoplatform.ios.IOSPreferences import org.ergoplatform.ios.tokens.SendTokenEntryView import org.ergoplatform.ios.ui.* import org.ergoplatform.ios.wallet.addresses.ChooseAddressListDialogViewController +import org.ergoplatform.transactions.PromptSigningResult +import org.ergoplatform.transactions.QR_SIZE_LIMIT import org.ergoplatform.transactions.TransactionResult +import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.uilogic.* import org.ergoplatform.uilogic.transactions.SendFundsUiLogic import org.ergoplatform.utils.LogUtils @@ -276,14 +280,14 @@ class SendFundsViewController( } if (checkResponse.canPay) { - startAuthFlow(uiLogic.wallet!!.walletConfig) { mnemonic -> - val appDelegate = getAppDelegate() - uiLogic.startPaymentWithMnemonicAsync( - mnemonic, appDelegate.prefs, IosStringProvider( - appDelegate.texts - ) - ) - } + val walletConfig = uiLogic.wallet!!.walletConfig + val appDelegate = getAppDelegate() + val stringProvider = IosStringProvider(appDelegate.texts) + walletConfig.secretStorage?.let { + startAuthFlow(walletConfig) { mnemonic -> + uiLogic.startPaymentWithMnemonicAsync(mnemonic, appDelegate.prefs, stringProvider) + } + } ?: uiLogic.startColdWalletPayment(appDelegate.prefs, stringProvider) } } @@ -304,7 +308,6 @@ class SendFundsViewController( if (txDoneView == null) { walletTitle.text = texts.format(STRING_LABEL_SEND_FROM, wallet!!.walletConfig.displayName) readOnlyHint.isHidden = uiLogic.wallet!!.walletConfig.secretStorage != null - sendButton.isEnabled = readOnlyHint.isHidden // TODO cold wallet scrollView.isHidden = false } } @@ -442,8 +445,20 @@ class SendFundsViewController( } } - override fun notifyHasSigningPromptData(signingPrompt: String?) { - TODO("Cold wallet not yet implemented") + override fun notifyHasSigningPromptData(signingPrompt: String) { + runOnMainThread { + // TODO cold wallet open signing prompt dialog + presentViewController( + SigningPromptViewController( + coldSigningRequestToQrChunks( + signingPrompt, + QR_SIZE_LIMIT + ), + uiLogic + ), true + ) {} + } + } } } \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt new file mode 100644 index 000000000..06456c0f7 --- /dev/null +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -0,0 +1,114 @@ +package org.ergoplatform.ios.transactions + +import org.ergoplatform.ios.ui.* +import org.ergoplatform.transactions.getColdSignedTxChunk +import org.ergoplatform.uilogic.* +import org.ergoplatform.uilogic.transactions.SendFundsUiLogic +import org.robovm.apple.coregraphics.CGRect +import org.robovm.apple.uikit.* + +class SigningPromptViewController( + private val qrPages: List, + private val uiLogic: SendFundsUiLogic +) : UIViewController() { + private lateinit var description: UILabel + private lateinit var pager: UIScrollView + private lateinit var nextButton: PrimaryButton + private lateinit var scanButton: PrimaryButton + private lateinit var descContainer: UIView + + override fun viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.systemBackground() + val closeButton = addCloseButton() + + val qrContainer = UIStackView(CGRect.Zero()).apply { + axis = UILayoutConstraintAxis.Horizontal + } + + qrPages.forEach { + val qrCode = UIImageView(CGRect.Zero()) + qrCode.fixedWidth(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2).fixedHeight(DEFAULT_QR_CODE_SIZE) + qrCode.setQrCode(it, DEFAULT_QR_CODE_SIZE) + qrCode.contentMode = UIViewContentMode.Center + qrContainer.addArrangedSubview(qrCode) + } + + pager = qrContainer.wrapInHorizontalPager(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2) + pager.delegate = object : UIScrollViewDelegateAdapter() { + override fun didEndDecelerating(scrollView: UIScrollView?) { + super.didEndDecelerating(scrollView) + pageChanged() + } + + override fun didEndScrollingAnimation(scrollView: UIScrollView?) { + super.didEndScrollingAnimation(scrollView) + pageChanged() + } + } + + description = Body1Label() + + val texts = getAppDelegate().texts + nextButton = PrimaryButton(texts.get(STRING_BUTTON_NEXT)) + nextButton.addOnTouchUpInsideListener { _, _ -> + pager.page = pager.page + 1 + } + + scanButton = PrimaryButton(texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) + scanButton.addOnTouchUpInsideListener { _, _ -> + presentViewController(QrScannerViewController { qrCode -> + getColdSignedTxChunk(qrCode)?.let { + if (it.pages > 1) { + // TODO cold wallet handle paged QR codes + val uac = + buildSimpleAlertController("", texts.get(STRING_ERROR_QR_PAGES_NUM), getAppDelegate().texts) + presentViewController(uac, false) {} + } else { + dismissViewController(false) { + val delegate = getAppDelegate() + uiLogic.sendColdWalletSignedTx( + listOf(qrCode), + delegate.prefs, + IosStringProvider(delegate.texts) + ) + } + } + } + }, true) {} + } + + descContainer = UIView(CGRect.Zero()).apply { + layoutMargins = UIEdgeInsets.Zero() + } + + descContainer.addSubview(description) + descContainer.addSubview(nextButton) + descContainer.addSubview(scanButton) + view.addSubview(pager) + view.addSubview(descContainer) + + pager.topToBottomOf(closeButton, DEFAULT_MARGIN).centerHorizontal() + descContainer.topToBottomOf(pager, DEFAULT_MARGIN * 3).widthMatchesSuperview(maxWidth = MAX_WIDTH) + description.topToSuperview().widthMatchesSuperview(inset = DEFAULT_MARGIN) + nextButton.topToBottomOf(description, DEFAULT_MARGIN * 3).centerHorizontal().fixedWidth(120.0) + scanButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0).bottomToSuperview() + + pageChanged() + } + + private fun pageChanged() { + val texts = getAppDelegate().texts + val lastPage = pager.page == qrPages.size - 1 + + descContainer.animateLayoutChanges { + description.text = texts.get( + if (lastPage) STRING_DESC_PROMPT_SIGNING + else STRING_DESC_PROMPT_SIGNING_MULTIPLE + ) + nextButton.isHidden = lastPage + scanButton.isHidden = !lastPage + } + } +} \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt b/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt index 8a96491be..0580dfe5d 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt @@ -389,6 +389,25 @@ fun UIView.wrapInVerticalScrollView(): UIScrollView { return scrollView } +fun UIView.wrapInHorizontalPager(width: Double): UIScrollView { + val scrollView = UIScrollView(CGRect.Zero()) + scrollView.addSubview(this) + scrollView.isPagingEnabled = true + scrollView.fixedWidth(width) + this.setTranslatesAutoresizingMaskIntoConstraints(false) + NSLayoutConstraint.activateConstraints( + NSArray( + this.heightAnchor.equalTo(scrollView.heightAnchor), + this.trailingAnchor.equalTo(scrollView.trailingAnchor), + this.leadingAnchor.equalTo(scrollView.leadingAnchor), + this.topAnchor.equalTo(scrollView.topAnchor), + this.bottomAnchor.equalTo(scrollView.bottomAnchor), + ) + ) + + return scrollView +} + fun createHorizontalSeparator(): UIView { val separator = UIView(CGRect.Zero()) separator.backgroundColor = UIColor.systemGray() diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt index 2ab54b64e..2ce2ac394 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.utils.I18NBundle import org.ergoplatform.ios.Main import org.ergoplatform.uilogic.STRING_ZXING_BUTTON_OK import org.robovm.apple.coregraphics.CGAffineTransform +import org.robovm.apple.coregraphics.CGPoint import org.robovm.apple.coregraphics.CGRect import org.robovm.apple.coreimage.CIFilter import org.robovm.apple.foundation.* @@ -14,6 +15,7 @@ import org.robovm.objc.Selector const val MAX_WIDTH = 500.0 const val DEFAULT_MARGIN = 6.0 const val DEFAULT_TEXT_FIELD_HEIGHT = 40.0 +const val DEFAULT_QR_CODE_SIZE = 280.0 const val IMAGE_WALLET = "rectangle.on.rectangle.angled" val IMAGE_SETTINGS = if (Foundation.getMajorSystemVersion() >= 14) "gearshape" else "gear" @@ -64,7 +66,7 @@ fun UIViewController.shareText(text: String, sourceView: UIView) { presentViewController(share, true, null) } -fun UIImageView.setQrCode(data: String, size: Int) { +fun UIImageView.setQrCode(data: String, size: Double) { val nsString = NSString(data).toData(NSStringEncoding.ASCII) val filter = CIFilter("CIQRCodeGenerator") filter.keyValueCoder.setValue("inputMessage", nsString) @@ -78,8 +80,6 @@ fun UIImageView.setQrCode(data: String, size: Int) { val output = unscaledOutput.newImageByApplyingTransform(transform) val image = UIImage(output) setImage(image) - contentMode = UIViewContentMode.ScaleAspectFit - backgroundColor = UIColor.systemRed() } } @@ -261,4 +261,10 @@ fun buildSimpleAlertController(title: String, message: String, texts: I18NBundle UIAlertActionStyle.Default ) {}) return uac -} \ No newline at end of file +} + +var UIScrollView.page + get() = (contentOffset.x / frame.size.width).toInt() + set(value) { + setContentOffset(CGPoint(frame.size.width * value.toDouble(), contentOffset.y), true) + } \ No newline at end of file From 0d761e9bbed878a524025a02716900248715632d Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 9 Jan 2022 14:39:46 +0100 Subject: [PATCH 12/25] iOS Cold wallet SigningPromptViewController add label for current page when necessary #54 --- .../SigningPromptDialogFragment.kt | 8 +++-- .../transactions/SendFundsViewController.kt | 3 -- .../SigningPromptViewController.kt | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 16 deletions(-) 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 bb94b1dbf..1acda46a5 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -20,6 +20,10 @@ import org.ergoplatform.transactions.QR_SIZE_LIMIT import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.transactions.getColdSignedTxChunk +/** + * SigningPromptDialogFragment is shown when user makes a transaction on a read-only address, presenting QR code(s) + * to scan with a cold wallet device. + */ class SigningPromptDialogFragment : BottomSheetDialogFragment() { private var _binding: FragmentPromptSigningDialogBinding? = null private val binding get() = _binding!! @@ -36,14 +40,14 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val viewModel = getViewModel() - viewModel.signingPromptData.observe(viewLifecycleOwner, { + viewModel.signingPromptData.observe(viewLifecycleOwner) { it?.let { val qrPages = coldSigningRequestToQrChunks(it, QR_SIZE_LIMIT) binding.qrCodePager.adapter = QrPagerAdapter(qrPages) refreshButtonState() } - }) + } binding.qrCodePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 67916d4bc..5cc6001ca 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -3,11 +3,9 @@ package org.ergoplatform.ios.transactions import com.badlogic.gdx.utils.I18NBundle import kotlinx.coroutines.CoroutineScope import org.ergoplatform.* -import org.ergoplatform.ios.IOSPreferences import org.ergoplatform.ios.tokens.SendTokenEntryView import org.ergoplatform.ios.ui.* import org.ergoplatform.ios.wallet.addresses.ChooseAddressListDialogViewController -import org.ergoplatform.transactions.PromptSigningResult import org.ergoplatform.transactions.QR_SIZE_LIMIT import org.ergoplatform.transactions.TransactionResult import org.ergoplatform.transactions.coldSigningRequestToQrChunks @@ -447,7 +445,6 @@ class SendFundsViewController( override fun notifyHasSigningPromptData(signingPrompt: String) { runOnMainThread { - // TODO cold wallet open signing prompt dialog presentViewController( SigningPromptViewController( coldSigningRequestToQrChunks( diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index 06456c0f7..c7e1b359b 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -7,12 +7,17 @@ import org.ergoplatform.uilogic.transactions.SendFundsUiLogic import org.robovm.apple.coregraphics.CGRect import org.robovm.apple.uikit.* +/** + * SigningPromptViewController is shown when user makes a transaction on a read-only address, presenting QR code(s) + * to scan with a cold wallet device. + */ class SigningPromptViewController( private val qrPages: List, private val uiLogic: SendFundsUiLogic ) : UIViewController() { private lateinit var description: UILabel private lateinit var pager: UIScrollView + private lateinit var currPageLabel: Headline2Label private lateinit var nextButton: PrimaryButton private lateinit var scanButton: PrimaryButton private lateinit var descContainer: UIView @@ -39,21 +44,19 @@ class SigningPromptViewController( pager.delegate = object : UIScrollViewDelegateAdapter() { override fun didEndDecelerating(scrollView: UIScrollView?) { super.didEndDecelerating(scrollView) - pageChanged() - } - - override fun didEndScrollingAnimation(scrollView: UIScrollView?) { - super.didEndScrollingAnimation(scrollView) - pageChanged() + pageChanged(pager.page) } } + currPageLabel = Headline2Label() + description = Body1Label() val texts = getAppDelegate().texts nextButton = PrimaryButton(texts.get(STRING_BUTTON_NEXT)) nextButton.addOnTouchUpInsideListener { _, _ -> - pager.page = pager.page + 1 + val nextPage = pager.page + 1 + scrollToQrCode(nextPage) } scanButton = PrimaryButton(texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) @@ -86,21 +89,31 @@ class SigningPromptViewController( descContainer.addSubview(description) descContainer.addSubview(nextButton) descContainer.addSubview(scanButton) + view.addSubview(currPageLabel) view.addSubview(pager) view.addSubview(descContainer) pager.topToBottomOf(closeButton, DEFAULT_MARGIN).centerHorizontal() - descContainer.topToBottomOf(pager, DEFAULT_MARGIN * 3).widthMatchesSuperview(maxWidth = MAX_WIDTH) + currPageLabel.topToBottomOf(pager, DEFAULT_MARGIN).centerHorizontal() + descContainer.topToBottomOf(currPageLabel, DEFAULT_MARGIN * 3).widthMatchesSuperview(maxWidth = MAX_WIDTH) description.topToSuperview().widthMatchesSuperview(inset = DEFAULT_MARGIN) nextButton.topToBottomOf(description, DEFAULT_MARGIN * 3).centerHorizontal().fixedWidth(120.0) scanButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0).bottomToSuperview() - pageChanged() + pageChanged(0) + } + + private fun scrollToQrCode(nextPage: Int) { + pager.page = nextPage + pageChanged(nextPage) } - private fun pageChanged() { + private fun pageChanged(newPage: Int) { val texts = getAppDelegate().texts - val lastPage = pager.page == qrPages.size - 1 + val lastPage = newPage == qrPages.size - 1 + currPageLabel.text = + if (qrPages.size == 1) "" + else texts.format(STRING_LABEL_QR_PAGES_INFO, newPage + 1, qrPages.size) descContainer.animateLayoutChanges { description.text = texts.get( From 77c15d824a168e3909611e16f416fe5638e93195 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 9 Jan 2022 15:55:08 +0100 Subject: [PATCH 13/25] iOS QrScannerViewController option to invoke callback after dismissal, needed to present following viewcontrollers #54 --- .../SigningPromptViewController.kt | 40 +++++++++++-------- .../ios/ui/QrScannerViewController.kt | 32 ++++++++++++--- .../wallet/AddReadOnlyWalletViewController.kt | 2 +- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index c7e1b359b..002cacd77 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -61,25 +61,31 @@ class SigningPromptViewController( scanButton = PrimaryButton(texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) scanButton.addOnTouchUpInsideListener { _, _ -> - presentViewController(QrScannerViewController { qrCode -> - getColdSignedTxChunk(qrCode)?.let { - if (it.pages > 1) { - // TODO cold wallet handle paged QR codes - val uac = - buildSimpleAlertController("", texts.get(STRING_ERROR_QR_PAGES_NUM), getAppDelegate().texts) - presentViewController(uac, false) {} - } else { - dismissViewController(false) { - val delegate = getAppDelegate() - uiLogic.sendColdWalletSignedTx( - listOf(qrCode), - delegate.prefs, - IosStringProvider(delegate.texts) - ) + presentViewController( + QrScannerViewController(dismissAnimated = false) { qrCode -> + getColdSignedTxChunk(qrCode)?.let { + if (it.pages > 1) { + // TODO cold wallet handle paged QR codes + val uac = + buildSimpleAlertController( + "", + texts.get(STRING_ERROR_QR_PAGES_NUM), + getAppDelegate().texts + ) + presentViewController(uac, false) {} + } else { + dismissViewController(false) { + val delegate = getAppDelegate() + uiLogic.sendColdWalletSignedTx( + listOf(qrCode), + delegate.prefs, + IosStringProvider(delegate.texts) + ) + } } } - } - }, true) {} + }, true + ) {} } descContainer = UIView(CGRect.Zero()).apply { diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/QrScannerViewController.kt b/ios/src/main/java/org/ergoplatform/ios/ui/QrScannerViewController.kt index 2ec682324..bec178c73 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/QrScannerViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/QrScannerViewController.kt @@ -11,7 +11,19 @@ import org.robovm.apple.uikit.UIColor import org.robovm.apple.uikit.UIInterfaceOrientationMask import org.robovm.apple.uikit.UIViewController -class QrScannerViewController(val qrCodeScannedListener: (String) -> Unit) : UIViewController() { +/** + * Shows a view for scanning QR codes + * + * @param qrCodeScannedListener invoked when QR code is scanned + * @param dismissAnimated whether to dismiss this ViewController with animation after scan + * @param invokeAfterDismissal if true, qrCodeScannedListener is invoked after this ViewController was dismissed + * otherwise, it is invoked before dismissal + */ +class QrScannerViewController( + private val dismissAnimated: Boolean = true, + private val invokeAfterDismissal: Boolean = true, + private val qrCodeScannedListener: (String) -> Unit +) : UIViewController() { private lateinit var captureSession: AVCaptureSession private var previewLayer: AVCaptureVideoPreviewLayer? = null @@ -72,6 +84,17 @@ class QrScannerViewController(val qrCodeScannedListener: (String) -> Unit) : UIV label.centerVertical().widthMatchesSuperview(inset = DEFAULT_MARGIN) } + private fun didScanQrCode(stringValue: String?) { + if (!invokeAfterDismissal) { + stringValue?.let { qrCodeScannedListener.invoke(stringValue) } + } + dismissViewController(dismissAnimated) { + if (invokeAfterDismissal) { + stringValue?.let { qrCodeScannedListener.invoke(stringValue) } + } + } + } + override fun viewWillAppear(animated: Boolean) { super.viewWillAppear(animated) @@ -104,13 +127,12 @@ class QrScannerViewController(val qrCodeScannedListener: (String) -> Unit) : UIV ) { captureSession.stopRunning() - (metadataObjects?.firstOrNull() as? AVMetadataMachineReadableCodeObject)?.let { - val stringValue = it.stringValue - qrCodeScannedListener.invoke(stringValue) + val stringValue = (metadataObjects?.firstOrNull() as? AVMetadataMachineReadableCodeObject)?.let { AudioServices.playSystemSound(AudioServices.SystemSoundVibrate) + it.stringValue } - this@QrScannerViewController.dismissViewController(true) {} + didScanQrCode(stringValue) } } } \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/AddReadOnlyWalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/AddReadOnlyWalletViewController.kt index 661a94ba6..0a97eff7c 100644 --- a/ios/src/main/java/org/ergoplatform/ios/wallet/AddReadOnlyWalletViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/wallet/AddReadOnlyWalletViewController.kt @@ -45,7 +45,7 @@ class AddReadOnlyWalletViewController : ViewControllerWithKeyboardLayoutGuide() } setCustomActionField(getIosSystemImage(IMAGE_QR_SCAN, UIImageSymbolScale.Small)!!) { - presentViewController(QrScannerViewController { + presentViewController(QrScannerViewController(invokeAfterDismissal = false) { val content = parsePaymentRequest(it) content?.let { text = content.address } }, true) {} From 6fbe23abdc1ad6ed96c5976b9fba730b84666e6e Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 9 Jan 2022 18:58:42 +0100 Subject: [PATCH 14/25] iOS Cold wallet SigningPromptViewController improvements on small screens #54 --- .../transactions/ColdWalletUtils.kt | 1 + .../SigningPromptViewController.kt | 10 ++++++++-- .../org/ergoplatform/ios/ui/TinyConstraints.kt | 18 +++++++++++++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt index 59d3e598f..d3b6d2e77 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -96,6 +96,7 @@ fun parseColdSigningResponse(qrData: String): SigningResult { } } +// TODO cold wallet make this configurable for QR code scan problems const val QR_SIZE_LIMIT = 2900 private const val QR_PROPERTY_COLD_SIGNING_REQUEST = "CSR" private const val QR_PROPERTY_COLD_SIGNED_TX = "CSTX" diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index 002cacd77..a78ccafbf 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -50,7 +50,10 @@ class SigningPromptViewController( currPageLabel = Headline2Label() - description = Body1Label() + description = Body1Label().apply { + // make this compress as first (iPhone SE) + setContentCompressionResistancePriority(500f, UILayoutConstraintAxis.Vertical) + } val texts = getAppDelegate().texts nextButton = PrimaryButton(texts.get(STRING_BUTTON_NEXT)) @@ -102,9 +105,12 @@ class SigningPromptViewController( pager.topToBottomOf(closeButton, DEFAULT_MARGIN).centerHorizontal() currPageLabel.topToBottomOf(pager, DEFAULT_MARGIN).centerHorizontal() descContainer.topToBottomOf(currPageLabel, DEFAULT_MARGIN * 3).widthMatchesSuperview(maxWidth = MAX_WIDTH) + .bottomToSuperview(bottomInset = DEFAULT_MARGIN) description.topToSuperview().widthMatchesSuperview(inset = DEFAULT_MARGIN) nextButton.topToBottomOf(description, DEFAULT_MARGIN * 3).centerHorizontal().fixedWidth(120.0) - scanButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0).bottomToSuperview() + .bottomToSuperview(canBeLess = true) + scanButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0) + .bottomToSuperview(canBeLess = true) pageChanged(0) } diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt b/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt index 0580dfe5d..a3d307c3d 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/TinyConstraints.kt @@ -209,14 +209,22 @@ fun UIView.rightToRightOf( fun UIView.bottomToSuperview( useSafeArea: Boolean = false, - bottomInset: Double = 0.0 + bottomInset: Double = 0.0, + canBeLess: Boolean = false ): UIView { setTranslatesAutoresizingMaskIntoConstraints(false) - val topConstraint = this.bottomAnchor.equalTo( - getSuperviewLayoutGuide(useSafeArea).bottomAnchor, - bottomInset * -1.0 - ) + val topConstraint = + if (canBeLess) + this.bottomAnchor.lessThanOrEqualTo( + getSuperviewLayoutGuide(useSafeArea).bottomAnchor, + bottomInset * -1.0 + ) + else + this.bottomAnchor.equalTo( + getSuperviewLayoutGuide(useSafeArea).bottomAnchor, + bottomInset * -1.0 + ) NSLayoutConstraint.activateConstraints(NSArray(topConstraint)) return this From 351cb302355f9444be17271b25517fc0618ce76c Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 9 Jan 2022 19:31:36 +0100 Subject: [PATCH 15/25] SendFundsUiLogic missing token warnings not issued when scanning QR code #25 --- .../org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt index a10d96e62..579e304d6 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt @@ -338,6 +338,8 @@ abstract class SendFundsUiLogic { addPaymentRequestWarning(STRING_ERROR_REQUEST_TOKEN_BUT_NO_ERG) } + notifyTokensChosenChanged() + } else if (paymentRequestWarnings.isNotEmpty()) { notifyTokensChosenChanged() } } From a192d958061704ad64c69251e4663a0a1f5365a6 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 9 Jan 2022 22:33:15 +0100 Subject: [PATCH 16/25] ColdWalletRequest and Response: user can switch to low-res when experiencing scanning problems #58 --- .../transactions/ColdWalletSigningFragment.kt | 27 ++++++++++++++++-- .../SigningPromptDialogFragment.kt | 28 +++++++++++++++---- .../main/res/drawable/ic_burst_mode_24.xml | 10 +++++++ .../layout/fragment_cold_wallet_signing.xml | 10 +++++++ .../layout/fragment_prompt_signing_dialog.xml | 10 +++++++ .../transactions/ColdWalletUtils.kt | 4 +-- .../transactions/ColdWalletSigningUiLogic.kt | 9 ++---- .../transactions/SendFundsViewController.kt | 5 ++-- 8 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 android/src/main/res/drawable/ic_burst_mode_24.xml 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 be9759a03..f346c99a3 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt @@ -18,6 +18,9 @@ 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 import org.ergoplatform.transactions.reduceBoxes /** @@ -30,6 +33,8 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { private val args: ColdWalletSigningFragmentArgs by navArgs() + private var scaleDown = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -63,8 +68,9 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { binding.cardSigningResult.visibility = View.VISIBLE binding.cardScanMore.visibility = View.GONE - binding.qrCodePager.adapter = QrPagerAdapter(viewModel.signedQrCode!!) - refreshButtonState() + binding.switchResolution.visibility = + if (viewModel.signedQrCode!!.length > QR_DATA_LENGTH_LOW_RES) View.VISIBLE else View.GONE + setQrData() } else { binding.cardSigningResult.visibility = View.GONE @@ -98,6 +104,11 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { binding.qrCodePager.currentItem = binding.qrCodePager.currentItem + 1 } + binding.switchResolution.setOnClickListener { + scaleDown = !scaleDown + setQrData() + } + binding.buttonDismiss.setOnClickListener { findNavController().popBackStack() } @@ -115,6 +126,18 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { }) } + private fun setQrData() { + viewModel.signedQrCode?.let { + binding.qrCodePager.adapter = QrPagerAdapter( + coldSigningResponseToQrChunks( + it, + if (scaleDown) QR_DATA_LENGTH_LOW_RES else QR_DATA_LENGTH_LIMIT + ) + ) + refreshButtonState() + } + } + private fun addQrCodeChunk(qrCode: String?) { qrCode?.let { viewModel.uiLogic.addQrCodeChunk(qrCode) } 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 1acda46a5..0a34d7f16 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -16,7 +16,8 @@ import org.ergoplatform.android.R import org.ergoplatform.android.databinding.FragmentPromptSigningDialogBinding import org.ergoplatform.android.ui.AndroidStringProvider import org.ergoplatform.android.ui.QrPagerAdapter -import org.ergoplatform.transactions.QR_SIZE_LIMIT +import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT +import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.transactions.getColdSignedTxChunk @@ -28,6 +29,8 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { private var _binding: FragmentPromptSigningDialogBinding? = null private val binding get() = _binding!! + private var scaleDown = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -42,10 +45,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { val viewModel = getViewModel() viewModel.signingPromptData.observe(viewLifecycleOwner) { it?.let { - val qrPages = coldSigningRequestToQrChunks(it, QR_SIZE_LIMIT) - binding.qrCodePager.adapter = QrPagerAdapter(qrPages) - - refreshButtonState() + setQrCodePagerData(it) } } binding.qrCodePager.registerOnPageChangeCallback(object : @@ -61,6 +61,24 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { binding.buttonScanNextQr.setOnClickListener { binding.qrCodePager.currentItem = binding.qrCodePager.currentItem + 1 } + binding.switchResolution.setOnClickListener { + viewModel.signingPromptData.value?.let { + scaleDown = !scaleDown + setQrCodePagerData(it) + } + } + } + + private fun setQrCodePagerData(data: String) { + binding.switchResolution.visibility = + if (data.length > QR_DATA_LENGTH_LOW_RES) View.VISIBLE else View.GONE + val qrPages = coldSigningRequestToQrChunks( + data, + if (scaleDown) QR_DATA_LENGTH_LOW_RES else QR_DATA_LENGTH_LIMIT + ) + binding.qrCodePager.adapter = QrPagerAdapter(qrPages) + + refreshButtonState() } private fun refreshButtonState() { diff --git a/android/src/main/res/drawable/ic_burst_mode_24.xml b/android/src/main/res/drawable/ic_burst_mode_24.xml new file mode 100644 index 000000000..e2bbd9d62 --- /dev/null +++ b/android/src/main/res/drawable/ic_burst_mode_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/src/main/res/layout/fragment_cold_wallet_signing.xml b/android/src/main/res/layout/fragment_cold_wallet_signing.xml index 16993a09d..38bf1bc2c 100644 --- a/android/src/main/res/layout/fragment_cold_wallet_signing.xml +++ b/android/src/main/res/layout/fragment_cold_wallet_signing.xml @@ -144,6 +144,16 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + () - var signedQrCode: List? = null + var signedQrCode: String? = null private set private var signingRequest: PromptSigningResult? = null @@ -107,12 +107,7 @@ abstract class ColdWalletSigningUiLogic { signingRequest.serializedTx!!, mnemonic, "", derivedAddresses, texts ) - signedQrCode = buildColdSigningResponse(ergoTxResult)?.let { - coldSigningResponseToQrChunks( - it, - QR_SIZE_LIMIT - ) - } + signedQrCode = buildColdSigningResponse(ergoTxResult) } notifyUiLocked(false) diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 5cc6001ca..86efa933a 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -6,7 +6,7 @@ import org.ergoplatform.* import org.ergoplatform.ios.tokens.SendTokenEntryView import org.ergoplatform.ios.ui.* import org.ergoplatform.ios.wallet.addresses.ChooseAddressListDialogViewController -import org.ergoplatform.transactions.QR_SIZE_LIMIT +import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT import org.ergoplatform.transactions.TransactionResult import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.uilogic.* @@ -444,12 +444,13 @@ class SendFundsViewController( } override fun notifyHasSigningPromptData(signingPrompt: String) { + // TODO cold wallet make size limit switchable for QR code scan problems runOnMainThread { presentViewController( SigningPromptViewController( coldSigningRequestToQrChunks( signingPrompt, - QR_SIZE_LIMIT + QR_DATA_LENGTH_LIMIT ), uiLogic ), true From 4e9b32c8152d3fb64f7fd4708c199f5d982003f5 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 11 Jan 2022 11:32:25 +0100 Subject: [PATCH 17/25] iOS ColdWalletRequest: user can switch to low-res when experiencing scanning problems #54 --- .../transactions/ColdWalletUtils.kt | 2 +- .../transactions/SendFundsViewController.kt | 11 +--- .../SigningPromptViewController.kt | 54 +++++++++++++++---- .../java/org/ergoplatform/ios/ui/UIUtils.kt | 2 + 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt index bdec6d34e..e0fd0aabb 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -118,7 +118,7 @@ private fun buildQrChunks( sizeLimit: Int, serializedSigningRequest: String ): List { - val actualSizeLimit = sizeLimit - 20 - prefix.length // reserve some space for our prefix + val actualSizeLimit = sizeLimit - 50 - prefix.length // reserve some space for our prefix val gson = GsonBuilder().disableHtmlEscaping().create() diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 86efa933a..11d18b226 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -6,9 +6,7 @@ import org.ergoplatform.* import org.ergoplatform.ios.tokens.SendTokenEntryView import org.ergoplatform.ios.ui.* import org.ergoplatform.ios.wallet.addresses.ChooseAddressListDialogViewController -import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT import org.ergoplatform.transactions.TransactionResult -import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.uilogic.* import org.ergoplatform.uilogic.transactions.SendFundsUiLogic import org.ergoplatform.utils.LogUtils @@ -444,16 +442,9 @@ class SendFundsViewController( } override fun notifyHasSigningPromptData(signingPrompt: String) { - // TODO cold wallet make size limit switchable for QR code scan problems runOnMainThread { presentViewController( - SigningPromptViewController( - coldSigningRequestToQrChunks( - signingPrompt, - QR_DATA_LENGTH_LIMIT - ), - uiLogic - ), true + SigningPromptViewController(signingPrompt, uiLogic), true ) {} } diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index a78ccafbf..bdecf5c71 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -1,6 +1,9 @@ package org.ergoplatform.ios.transactions import org.ergoplatform.ios.ui.* +import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT +import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES +import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.transactions.getColdSignedTxChunk import org.ergoplatform.uilogic.* import org.ergoplatform.uilogic.transactions.SendFundsUiLogic @@ -12,7 +15,7 @@ import org.robovm.apple.uikit.* * to scan with a cold wallet device. */ class SigningPromptViewController( - private val qrPages: List, + private val signingPrompt: String, private val uiLogic: SendFundsUiLogic ) : UIViewController() { private lateinit var description: UILabel @@ -21,6 +24,11 @@ class SigningPromptViewController( private lateinit var nextButton: PrimaryButton private lateinit var scanButton: PrimaryButton private lateinit var descContainer: UIView + private lateinit var switchResButton: UIImageView + private lateinit var qrContainer: UIStackView + + private lateinit var qrPages: List + private var showLowRes = false override fun viewDidLoad() { super.viewDidLoad() @@ -28,18 +36,10 @@ class SigningPromptViewController( view.backgroundColor = UIColor.systemBackground() val closeButton = addCloseButton() - val qrContainer = UIStackView(CGRect.Zero()).apply { + qrContainer = UIStackView(CGRect.Zero()).apply { axis = UILayoutConstraintAxis.Horizontal } - qrPages.forEach { - val qrCode = UIImageView(CGRect.Zero()) - qrCode.fixedWidth(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2).fixedHeight(DEFAULT_QR_CODE_SIZE) - qrCode.setQrCode(it, DEFAULT_QR_CODE_SIZE) - qrCode.contentMode = UIViewContentMode.Center - qrContainer.addArrangedSubview(qrCode) - } - pager = qrContainer.wrapInHorizontalPager(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2) pager.delegate = object : UIScrollViewDelegateAdapter() { override fun didEndDecelerating(scrollView: UIScrollView?) { @@ -90,6 +90,15 @@ class SigningPromptViewController( }, true ) {} } + switchResButton = UIImageView(getIosSystemImage(IMAGE_SWITCH_RESOLUTION, UIImageSymbolScale.Small)).apply { + tintColor = UIColor.label() + isUserInteractionEnabled = true + addGestureRecognizer(UITapGestureRecognizer { + showLowRes = !showLowRes + setQrCodesToPager() + }) + } + switchResButton.isHidden = signingPrompt.length <= QR_DATA_LENGTH_LOW_RES descContainer = UIView(CGRect.Zero()).apply { layoutMargins = UIEdgeInsets.Zero() @@ -101,6 +110,7 @@ class SigningPromptViewController( view.addSubview(currPageLabel) view.addSubview(pager) view.addSubview(descContainer) + view.addSubview(switchResButton) pager.topToBottomOf(closeButton, DEFAULT_MARGIN).centerHorizontal() currPageLabel.topToBottomOf(pager, DEFAULT_MARGIN).centerHorizontal() @@ -111,8 +121,30 @@ class SigningPromptViewController( .bottomToSuperview(canBeLess = true) scanButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0) .bottomToSuperview(canBeLess = true) + switchResButton.topToSuperview(topInset = DEFAULT_MARGIN).rightToSuperview() - pageChanged(0) + } + + override fun viewWillAppear(animated: Boolean) { + super.viewWillAppear(animated) + setQrCodesToPager() + } + + private fun setQrCodesToPager() { + qrPages = coldSigningRequestToQrChunks( + signingPrompt, + if (showLowRes) QR_DATA_LENGTH_LOW_RES else QR_DATA_LENGTH_LIMIT + ) + qrContainer.clearArrangedSubviews() + qrPages.forEach { + val qrCode = UIImageView(CGRect.Zero()) + qrCode.fixedWidth(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2).fixedHeight(DEFAULT_QR_CODE_SIZE) + qrCode.setQrCode(it, DEFAULT_QR_CODE_SIZE) + qrCode.contentMode = UIViewContentMode.Center + qrContainer.addArrangedSubview(qrCode) + } + pager.layoutIfNeeded() + scrollToQrCode(0) } private fun scrollToQrCode(nextPage: Int) { diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt index 2ce2ac394..9ac656a0b 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt @@ -40,6 +40,8 @@ const val IMAGE_TRANSACTIONS = "arrow.right.arrow.left" const val IMAGE_ADDRESS_LIST = "list.number" const val IMAGE_CHEVRON_DOWN = "chevron.down" const val IMAGE_CHEVRON_UP = "chevron.up" +val IMAGE_SWITCH_RESOLUTION = if (Foundation.getMajorSystemVersion() >= 14) + "arrow.up.left.and.down.right.magnifyingglass" else "1.magnifyingglass" const val FONT_SIZE_BODY1 = 18.0 const val FONT_SIZE_HEADLINE1 = 30.0 From 3e655d0036d9d283fbfda68e1474b4a6d92dd404 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 11 Jan 2022 12:11:14 +0100 Subject: [PATCH 18/25] SendFundsUiLogic add logic when qr code was scanned #54 --- .../android/transactions/SendFundsFragment.kt | 91 ++++++++----------- .../android/wallet/WalletDetailsFragment.kt | 2 +- .../main/java/org/ergoplatform/ErgoAmount.kt | 4 + .../uilogic/transactions/SendFundsUiLogic.kt | 32 +++++-- .../ChooseSpendingWalletViewController.kt | 2 +- .../transactions/SendFundsViewController.kt | 15 ++- .../ios/wallet/WalletDetailsViewController.kt | 2 +- 7 files changed, 76 insertions(+), 72 deletions(-) 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 1196b3173..31ee303de 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt @@ -32,7 +32,6 @@ import org.ergoplatform.persistance.WalletConfig import org.ergoplatform.persistance.WalletToken import org.ergoplatform.tokens.isSingularToken import org.ergoplatform.transactions.PromptSigningResult -import org.ergoplatform.transactions.isColdSigningRequestChunk import org.ergoplatform.utils.formatFiatToString import org.ergoplatform.wallet.addresses.getAddressLabel import org.ergoplatform.wallet.getNumOfAddresses @@ -256,49 +255,49 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba tokensChosen.forEach { val ergoId = it.key tokensAvail.firstOrNull { it.tokenId.equals(ergoId) }?.let { tokenDbEntity -> - val itemBinding = - FragmentSendFundsTokenItemBinding.inflate(layoutInflater, this, true) - itemBinding.tvTokenName.text = tokenDbEntity.name - - val amountChosen = it.value.value - val isSingular = tokenDbEntity.isSingularToken() && amountChosen == 1L - - if (isSingular) { - itemBinding.inputTokenAmount.visibility = View.GONE - itemBinding.buttonTokenAll.visibility = View.INVISIBLE - } else { - itemBinding.inputTokenAmount.inputType = - if (tokenDbEntity.decimals > 0) InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL - else InputType.TYPE_CLASS_NUMBER - itemBinding.inputTokenAmount.addTextChangedListener( - TokenAmountWatcher(tokenDbEntity, itemBinding) + val itemBinding = + FragmentSendFundsTokenItemBinding.inflate(layoutInflater, this, true) + itemBinding.tvTokenName.text = tokenDbEntity.name + + val amountChosen = it.value.value + val isSingular = tokenDbEntity.isSingularToken() && amountChosen == 1L + + if (isSingular) { + itemBinding.inputTokenAmount.visibility = View.GONE + itemBinding.buttonTokenAll.visibility = View.INVISIBLE + } else { + itemBinding.inputTokenAmount.inputType = + if (tokenDbEntity.decimals > 0) InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + else InputType.TYPE_CLASS_NUMBER + itemBinding.inputTokenAmount.addTextChangedListener( + TokenAmountWatcher(tokenDbEntity, itemBinding) + ) + itemBinding.inputTokenAmount.setText( + viewModel.uiLogic.tokenAmountToText( + amountChosen, + tokenDbEntity.decimals ) + ) + itemBinding.buttonTokenAll.setOnClickListener { itemBinding.inputTokenAmount.setText( viewModel.uiLogic.tokenAmountToText( - amountChosen, + tokenDbEntity.amount!!, tokenDbEntity.decimals ) ) - itemBinding.buttonTokenAll.setOnClickListener { - itemBinding.inputTokenAmount.setText( - viewModel.uiLogic.tokenAmountToText( - tokenDbEntity.amount!!, - tokenDbEntity.decimals - ) - ) - itemBinding.buttonTokenAll.visibility = View.INVISIBLE - } + itemBinding.buttonTokenAll.visibility = View.INVISIBLE } + } - itemBinding.buttonTokenRemove.setOnClickListener { - if (isSingular || itemBinding.inputTokenAmount.text.isEmpty()) { - viewModel.uiLogic.removeToken(ergoId) - } else { - itemBinding.inputTokenAmount.text = null - itemBinding.inputTokenAmount.requestFocus() - } + itemBinding.buttonTokenRemove.setOnClickListener { + if (isSingular || itemBinding.inputTokenAmount.text.isEmpty()) { + viewModel.uiLogic.removeToken(ergoId) + } else { + itemBinding.inputTokenAmount.text = null + itemBinding.inputTokenAmount.requestFocus() } } + } } setFocusToEmptyTokenAmountInput() @@ -380,28 +379,18 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { result.contents?.let { - if (viewModel.uiLogic.wallet?.walletConfig?.secretStorage != null - && isColdSigningRequestChunk(it) - ) { + viewModel.uiLogic.qrCodeScanned(it, { data, walletId -> findNavController().navigate( SendFundsFragmentDirections .actionSendFundsFragmentToColdWalletSigningFragment( - it, - viewModel.uiLogic.wallet!!.walletConfig.id + data, + walletId ) ) - } else { - val content = parsePaymentRequest(it) - content?.let { - binding.tvReceiver.editText?.setText(content.address) - content.amount.let { amount -> - if (amount.nanoErgs > 0) setAmountEdittext( - amount - ) - } - viewModel.uiLogic.addTokensFromPaymentRequest(content.tokens) - } - } + }, { address, amount -> + binding.tvReceiver.editText?.setText(address) + amount?.let { setAmountEdittext(amount) } + }) } } else { super.onActivityResult(requestCode, resultCode, data) 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 de01db1c5..822ba4614 100644 --- a/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt @@ -177,7 +177,7 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback { binding.walletBalance.amount = ergoAmount.toDouble() binding.walletUnconfirmed.amount = unconfirmed.toDouble() - binding.walletUnconfirmed.visibility = if (unconfirmed.nanoErgs == 0L) View.GONE else View.VISIBLE + binding.walletUnconfirmed.visibility = if (unconfirmed.isZero()) View.GONE else View.VISIBLE binding.labelWalletUnconfirmed.visibility = binding.walletUnconfirmed.visibility // Fill fiat value diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt index 9a95a8782..bd719cb00 100644 --- a/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt +++ b/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt @@ -48,6 +48,10 @@ class ErgoAmount(val nanoErgs: Long) { operator fun minus(other: ErgoAmount): ErgoAmount { return ErgoAmount(this.nanoErgs - other.nanoErgs) } + + fun isZero(): Boolean { + return nanoErgs == 0L + } } fun String.toErgoAmount(): ErgoAmount? { diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt index 17d00ca87..86507081f 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt @@ -5,16 +5,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.ergoplatform.* -import org.ergoplatform.transactions.buildColdSigningRequest -import org.ergoplatform.transactions.coldSigningResponseFromQrChunks import org.ergoplatform.appkit.Address import org.ergoplatform.appkit.ErgoToken import org.ergoplatform.appkit.Parameters import org.ergoplatform.persistance.* import org.ergoplatform.tokens.isSingularToken -import org.ergoplatform.transactions.PromptSigningResult -import org.ergoplatform.transactions.SendTransactionResult -import org.ergoplatform.transactions.TransactionResult +import org.ergoplatform.transactions.* import org.ergoplatform.uilogic.STRING_ERROR_REQUEST_TOKEN_AMOUNT import org.ergoplatform.uilogic.STRING_ERROR_REQUEST_TOKEN_BUT_NO_ERG import org.ergoplatform.uilogic.STRING_ERROR_REQUEST_TOKEN_NOT_FOUND @@ -154,11 +150,10 @@ abstract class SendFundsUiLogic { * min possible amount in case no amount was entered and token should be sent */ private fun getActualAmountToSendNanoErgs(): Long { - val userEnteredNanoErgs = amountToSend.nanoErgs - return if (tokensChosen.isNotEmpty() && userEnteredNanoErgs == 0L) + return if (tokensChosen.isNotEmpty() && amountToSend.isZero()) Parameters.MinChangeValue else - userEnteredNanoErgs + amountToSend.nanoErgs } fun checkCanMakePayment(): CheckCanPayResponse { @@ -336,7 +331,7 @@ abstract class SendFundsUiLogic { } ?: addPaymentRequestWarning(STRING_ERROR_REQUEST_TOKEN_NOT_FOUND, tokenId) } if (changed) { - if (amountToSend.nanoErgs == 0L) { + if (amountToSend.isZero()) { addPaymentRequestWarning(STRING_ERROR_REQUEST_TOKEN_BUT_NO_ERG) } @@ -362,6 +357,25 @@ abstract class SendFundsUiLogic { else stringWarnings.joinToString("\n")) } + fun qrCodeScanned( + qrCodeData: String, + navigateToColdWalletSigning: ((signingData: String, walletId: Int) -> Unit), + setPaymentRequestDataToUi: ((receiverAddress: String, amount: ErgoAmount?) -> Unit) + ) { + if (wallet?.walletConfig?.secretStorage != null && isColdSigningRequestChunk(qrCodeData)) { + navigateToColdWalletSigning.invoke(qrCodeData, wallet!!.walletConfig.id) + } else { + val content = parsePaymentRequest(qrCodeData) + content?.let { + setPaymentRequestDataToUi.invoke( + content.address, + content.amount.let { amount -> if (amount.nanoErgs > 0) amount else null } + ) + addTokensFromPaymentRequest(content.tokens) + } + } + } + abstract fun notifyWalletStateLoaded() abstract fun notifyDerivedAddressChanged() abstract fun notifyTokensChosenChanged() diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt index 228af7b6e..29d96f251 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt @@ -77,7 +77,7 @@ class ChooseSpendingWalletViewController( .bottomToSuperview(bottomInset = DEFAULT_MARGIN) amountLabel.setErgoAmount(paymentRequest.amount) - amountLabel.isHidden = paymentRequest.amount.nanoErgs == 0L + amountLabel.isHidden = paymentRequest.amount.isZero() recipientLabel.text = paymentRequest.address } diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 11d18b226..316e9c0ae 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -56,16 +56,13 @@ class SendFundsViewController( ) uiBarButtonItem.setOnClickListener { presentViewController(QrScannerViewController { - // TODO check cold wallet QR - val content = parsePaymentRequest(it) - content?.let { - inputReceiver.text = content.address + uiLogic.qrCodeScanned(it, { data, walletId -> + // TODO cold wallet navigate to signing screen + }, { address, amount -> + inputReceiver.text = address inputReceiver.sendControlEventsActions(UIControlEvents.EditingChanged) - content.amount.let { amount -> - if (amount.nanoErgs > 0) setInputAmount(amount) - } - uiLogic.addTokensFromPaymentRequest(content.tokens) - } + amount?.let { setInputAmount(amount) } + }) }, true) {} } navigationController.topViewController.navigationItem.rightBarButtonItem = uiBarButtonItem diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/WalletDetailsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/WalletDetailsViewController.kt index 1b5ad90a1..b98ea0c9f 100644 --- a/ios/src/main/java/org/ergoplatform/ios/wallet/WalletDetailsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/wallet/WalletDetailsViewController.kt @@ -287,7 +287,7 @@ class WalletDetailsViewController(private val walletId: Int) : CoroutineViewCont val unconfirmed = uiLogic.getUnconfirmedErgoBalance() unconfirmedBalance.text = texts.format(STRING_LABEL_ERG_AMOUNT, unconfirmed.toStringRoundToDecimals()) + " " + texts.get(STRING_LABEL_UNCONFIRMED) - unconfirmedBalance.isHidden = (unconfirmed.nanoErgs == 0L) + unconfirmedBalance.isHidden = (unconfirmed.isZero()) // Fill fiat value val nodeConnector = NodeConnector.getInstance() From 2b0e8b01562a2a9626165440370de6f6f7259fe8 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 11 Jan 2022 14:52:12 +0100 Subject: [PATCH 19/25] iOS Cold wallet introduce ColdWalletSigningViewController #54 --- .../transactions/ColdWalletSigningUiLogic.kt | 5 + .../ColdWalletSigningViewController.kt | 121 ++++++++++++++++++ .../transactions/SendFundsViewController.kt | 16 +-- .../ios/ui/ProgressViewController.kt | 17 +++ .../java/org/ergoplatform/ios/ui/UIUtils.kt | 1 + .../WalletAddressesViewController.kt | 14 +- 6 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt index efc065ddf..e304e0f02 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -16,6 +16,8 @@ import org.ergoplatform.wallet.getSortedDerivedAddressesList abstract class ColdWalletSigningUiLogic { abstract val coroutineScope: CoroutineScope + var state = State.SCANNING + private set var pagesQrCode = 0 val pagesAdded get() = qrCodeChunks.size private val qrCodeChunks = HashMap() @@ -66,6 +68,7 @@ abstract class ColdWalletSigningUiLogic { pagesQrCode = count transactionInfo = buildRequestWhenApplicable() + transactionInfo?.let { state = State.WAITING_TO_CONFIRM } return transactionInfo } @@ -111,6 +114,7 @@ abstract class ColdWalletSigningUiLogic { } notifyUiLocked(false) + state = State.PRESENT_RESULT notifySigningResult(ergoTxResult) } @@ -121,4 +125,5 @@ abstract class ColdWalletSigningUiLogic { abstract fun notifyUiLocked(locked: Boolean) abstract fun notifySigningResult(ergoTxResult: SigningResult) + enum class State { SCANNING, WAITING_TO_CONFIRM, PRESENT_RESULT } } \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt new file mode 100644 index 000000000..a83249377 --- /dev/null +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt @@ -0,0 +1,121 @@ +package org.ergoplatform.ios.transactions + +import kotlinx.coroutines.CoroutineScope +import org.ergoplatform.ios.ui.* +import org.ergoplatform.transactions.SigningResult +import org.ergoplatform.uilogic.STRING_LABEL_QR_PAGES_INFO +import org.ergoplatform.uilogic.STRING_LABEL_SCAN_QR +import org.ergoplatform.uilogic.STRING_TITLE_SIGNING_REQUEST +import org.ergoplatform.uilogic.transactions.ColdWalletSigningUiLogic +import org.robovm.apple.coregraphics.CGRect +import org.robovm.apple.uikit.* + +class ColdWalletSigningViewController(private val signingRequestChunk: String, private val walletId: Int) : + CoroutineViewController() { + + val uiLogic = IosUiLogic() + val texts = getAppDelegate().texts + + val scanningContainer = ScanningContainer() + + override fun viewDidLoad() { + super.viewDidLoad() + + view.layoutMargins = UIEdgeInsets.Zero() + val texts = getAppDelegate().texts + title = texts.get(STRING_TITLE_SIGNING_REQUEST) + view.backgroundColor = UIColor.systemBackground() + navigationController.navigationBar?.tintColor = UIColor.label() + + val scrollingContainer = UIView(CGRect.Zero()) + val scrollView = scrollingContainer.wrapInVerticalScrollView() + + view.addSubview(scrollView) + scrollView.addSubview(scanningContainer) + + scanningContainer.edgesToSuperview() + scrollView.edgesToSuperview() + } + + override fun viewWillAppear(animated: Boolean) { + super.viewWillAppear(animated) + uiLogic.setWalletId(walletId, getAppDelegate().database) + addQrCodeChunk(signingRequestChunk) + } + + private fun addQrCodeChunk(qrCodeChunk: String) { + uiLogic.addQrCodeChunk(qrCodeChunk) + + scanningContainer.statusText.text = + texts.format(STRING_LABEL_QR_PAGES_INFO, uiLogic.pagesAdded, uiLogic.pagesQrCode) + scanningContainer.errorText.text = uiLogic.lastErrorMessage ?: "" + + uiLogic.transactionInfo?.let { + // TODO new tx info, fill container + } + + refreshState() + } + + private fun refreshState() { + scanningContainer.isHidden = uiLogic.state != ColdWalletSigningUiLogic.State.SCANNING + } + + inner class ScanningContainer : UIView(CGRect.Zero()) { + val statusText = Headline2Label().apply { + textAlignment = NSTextAlignment.Center + } + val errorText = Body1BoldLabel().apply { + textColor = uiColorErgo + textAlignment = NSTextAlignment.Center + } + + init { + val image = UIImageView(getIosSystemImage(IMAGE_QR_CODE, UIImageSymbolScale.Large, 150.0)).apply { + contentMode = UIViewContentMode.Center + tintColor = UIColor.secondaryLabel() + } + val scanNextButton = PrimaryButton( + texts.get(STRING_LABEL_SCAN_QR), + getIosSystemImage(IMAGE_QR_SCAN, UIImageSymbolScale.Small) + ).apply { + addOnTouchUpInsideListener { _, _ -> scanNext() } + } + + addSubview(image) + addSubview(statusText) + addSubview(errorText) + addSubview(scanNextButton) + + image.fixedHeight(200.0).topToSuperview().widthMatchesSuperview() + statusText.topToBottomOf(image, DEFAULT_MARGIN * 2).widthMatchesSuperview() + errorText.topToBottomOf(statusText, DEFAULT_MARGIN * 2).widthMatchesSuperview() + scanNextButton.topToBottomOf(errorText).bottomToSuperview(canBeLess = true).fixedWidth(200.0) + .centerHorizontal() + } + } + + private fun scanNext() { + presentViewController(QrScannerViewController { + addQrCodeChunk(it) + }, true) {} + } + + inner class IosUiLogic : ColdWalletSigningUiLogic() { + private val progressViewController = + ProgressViewController.ProgressViewControllerPresenter(this@ColdWalletSigningViewController) + + override val coroutineScope: CoroutineScope + get() = viewControllerScope + + override fun notifyUiLocked(locked: Boolean) { + runOnMainThread { + progressViewController.setUiLocked(locked) + } + } + + override fun notifySigningResult(ergoTxResult: SigningResult) { + TODO("Not yet implemented") + } + } +} \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 316e9c0ae..7c7914d09 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -57,7 +57,7 @@ class SendFundsViewController( uiBarButtonItem.setOnClickListener { presentViewController(QrScannerViewController { uiLogic.qrCodeScanned(it, { data, walletId -> - // TODO cold wallet navigate to signing screen + navigationController.pushViewController(ColdWalletSigningViewController(data, walletId), true) }, { address, amount -> inputReceiver.text = address inputReceiver.sendControlEventsActions(UIControlEvents.EditingChanged) @@ -290,7 +290,8 @@ class SendFundsViewController( } inner class IosSendFundsUiLogic : SendFundsUiLogic() { - private var progressViewController: ProgressViewController? = null + private val progressViewController = + ProgressViewController.ProgressViewControllerPresenter(this@SendFundsViewController) override val coroutineScope: CoroutineScope get() = viewControllerScope @@ -361,16 +362,7 @@ class SendFundsViewController( override fun notifyUiLocked(locked: Boolean) { runOnMainThread { - if (locked) { - if (progressViewController == null) { - forceDismissKeyboard() - progressViewController = ProgressViewController() - progressViewController?.presentModalAbove(this@SendFundsViewController) - } - } else { - progressViewController?.dismissViewController(false) {} - progressViewController = null - } + progressViewController.setUiLocked(true) } } diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/ProgressViewController.kt b/ios/src/main/java/org/ergoplatform/ios/ui/ProgressViewController.kt index 67e73e6b5..3e2da2362 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/ProgressViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/ProgressViewController.kt @@ -25,4 +25,21 @@ class ProgressViewController : UIViewController() { isModalInPresentation = true vc.presentViewController(this, false) {} } + + class ProgressViewControllerPresenter(private val vc: UIViewController) { + private var progressViewController: ProgressViewController? = null + + fun setUiLocked(locked: Boolean) { + if (locked) { + if (progressViewController == null) { + forceDismissKeyboard() + progressViewController = ProgressViewController() + progressViewController?.presentModalAbove(vc) + } + } else { + progressViewController?.dismissViewController(false) {} + progressViewController = null + } + } + } } diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt index 9ac656a0b..0a910a383 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt @@ -27,6 +27,7 @@ const val IMAGE_EXCLAMATION_MARK = "exclamationmark.circle.fill" const val IMAGE_NO_CONNECTION = "icloud.slash" const val IMAGE_SEND = "paperplane" const val IMAGE_RECEIVE = "arrow.down.left" +const val IMAGE_QR_CODE = "qrcode" const val IMAGE_QR_SCAN = "qrcode.viewfinder" const val IMAGE_PLUS_CIRCLE = "plus.circle.fill" const val IMAGE_PLUS = "plus" diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/addresses/WalletAddressesViewController.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/addresses/WalletAddressesViewController.kt index bb6b766cd..175a91dd6 100644 --- a/ios/src/main/java/org/ergoplatform/ios/wallet/addresses/WalletAddressesViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/wallet/addresses/WalletAddressesViewController.kt @@ -108,7 +108,8 @@ class WalletAddressesViewController(private val walletId: Int) : CoroutineViewCo } inner class IosWalletAddressesUiLogic : WalletAddressesUiLogic() { - private var progressViewController: ProgressViewController? = null + private val progressViewController = + ProgressViewController.ProgressViewControllerPresenter(this@WalletAddressesViewController) override val coroutineScope: CoroutineScope get() = viewControllerScope @@ -122,16 +123,7 @@ class WalletAddressesViewController(private val walletId: Int) : CoroutineViewCo override fun notifyUiLocked(locked: Boolean) { runOnMainThread { - if (locked) { - if (progressViewController == null) { - forceDismissKeyboard() - progressViewController = ProgressViewController() - progressViewController?.presentModalAbove(this@WalletAddressesViewController) - } - } else { - progressViewController?.dismissViewController(false) {} - progressViewController = null - } + progressViewController.setUiLocked(locked) } } From 2fb276bfca20903933d14a28eb7ad2c333e073bb Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 11 Jan 2022 16:28:53 +0100 Subject: [PATCH 20/25] iOS Cold wallet ColdWalletSigningViewController add TransactionInfo, introduce TransactionBoxEntryView #54 --- .../ergoplatform/ios/tokens/TokenEntryView.kt | 5 + .../ColdWalletSigningViewController.kt | 109 +++++++++++++++--- .../transactions/TransactionBoxEntryView.kt | 68 +++++++++++ .../org/ergoplatform/ios/ui/ThemedLabels.kt | 8 +- 4 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt diff --git a/ios/src/main/java/org/ergoplatform/ios/tokens/TokenEntryView.kt b/ios/src/main/java/org/ergoplatform/ios/tokens/TokenEntryView.kt index 017f33caa..f30885d89 100644 --- a/ios/src/main/java/org/ergoplatform/ios/tokens/TokenEntryView.kt +++ b/ios/src/main/java/org/ergoplatform/ios/tokens/TokenEntryView.kt @@ -41,6 +41,11 @@ class TokenEntryView : UIStackView(CGRect.Zero()) { return this } + fun bindWalletToken(tokenName: String?, amount: String?) { + labelTokenName.text = tokenName ?: "" + labelTokenVal.text = amount ?: "" + } + fun bindHasMoreTokenHint(moreTokenNum: Int): TokenEntryView { labelTokenVal.text = "+$moreTokenNum" labelTokenName.text = getAppDelegate().texts.get(STRING_LABEL_MORE_TOKENS) diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt index a83249377..6ca4a66a6 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt @@ -1,11 +1,11 @@ package org.ergoplatform.ios.transactions import kotlinx.coroutines.CoroutineScope +import org.ergoplatform.explorer.client.model.TransactionInfo import org.ergoplatform.ios.ui.* import org.ergoplatform.transactions.SigningResult -import org.ergoplatform.uilogic.STRING_LABEL_QR_PAGES_INFO -import org.ergoplatform.uilogic.STRING_LABEL_SCAN_QR -import org.ergoplatform.uilogic.STRING_TITLE_SIGNING_REQUEST +import org.ergoplatform.transactions.reduceBoxes +import org.ergoplatform.uilogic.* import org.ergoplatform.uilogic.transactions.ColdWalletSigningUiLogic import org.robovm.apple.coregraphics.CGRect import org.robovm.apple.uikit.* @@ -16,7 +16,8 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p val uiLogic = IosUiLogic() val texts = getAppDelegate().texts - val scanningContainer = ScanningContainer() + private val scanningContainer = ScanningContainer() + private val transactionContainer = TransactionContainer() override fun viewDidLoad() { super.viewDidLoad() @@ -31,9 +32,11 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p val scrollView = scrollingContainer.wrapInVerticalScrollView() view.addSubview(scrollView) - scrollView.addSubview(scanningContainer) + scrollingContainer.addSubview(scanningContainer) + scrollingContainer.addSubview(transactionContainer) - scanningContainer.edgesToSuperview() + scanningContainer.edgesToSuperview(maxWidth = MAX_WIDTH) + transactionContainer.edgesToSuperview(maxWidth = MAX_WIDTH) scrollView.edgesToSuperview() } @@ -50,8 +53,8 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p texts.format(STRING_LABEL_QR_PAGES_INFO, uiLogic.pagesAdded, uiLogic.pagesQrCode) scanningContainer.errorText.text = uiLogic.lastErrorMessage ?: "" - uiLogic.transactionInfo?.let { - // TODO new tx info, fill container + uiLogic.transactionInfo?.reduceBoxes()?.let { + transactionContainer.bindTransaction(it) } refreshState() @@ -59,6 +62,13 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p private fun refreshState() { scanningContainer.isHidden = uiLogic.state != ColdWalletSigningUiLogic.State.SCANNING + transactionContainer.isHidden = uiLogic.state != ColdWalletSigningUiLogic.State.WAITING_TO_CONFIRM + } + + private fun scanNext() { + presentViewController(QrScannerViewController(invokeAfterDismissal = false) { + addQrCodeChunk(it) + }, true) {} } inner class ScanningContainer : UIView(CGRect.Zero()) { @@ -90,15 +100,86 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p image.fixedHeight(200.0).topToSuperview().widthMatchesSuperview() statusText.topToBottomOf(image, DEFAULT_MARGIN * 2).widthMatchesSuperview() errorText.topToBottomOf(statusText, DEFAULT_MARGIN * 2).widthMatchesSuperview() - scanNextButton.topToBottomOf(errorText).bottomToSuperview(canBeLess = true).fixedWidth(200.0) - .centerHorizontal() + scanNextButton.topToBottomOf(errorText, DEFAULT_MARGIN * 2).bottomToSuperview(canBeLess = true) + .fixedWidth(200.0).centerHorizontal() } } - private fun scanNext() { - presentViewController(QrScannerViewController { - addQrCodeChunk(it) - }, true) {} + inner class TransactionContainer : UIStackView() { + private val inboxesList = UIStackView().apply { + axis = UILayoutConstraintAxis.Vertical + } + private val outBoxesList = UIStackView().apply { + axis = UILayoutConstraintAxis.Vertical + } + + init { + axis = UILayoutConstraintAxis.Vertical + spacing = DEFAULT_MARGIN + layoutMargins = UIEdgeInsets(DEFAULT_MARGIN * 2, 0.0, DEFAULT_MARGIN * 2, 0.0) + isLayoutMarginsRelativeArrangement = true + + addArrangedSubview(Body2BoldLabel().apply { + text = texts.get(STRING_DESC_SIGNING_REQUEST) + }) + addArrangedSubview(createHorizontalSeparator()) + addArrangedSubview(Body1BoldLabel().apply { + text = texts.get(STRING_TITLE_INBOXES) + textColor = uiColorErgo + }) + addArrangedSubview(Body2Label().apply { + text = texts.get(STRING_DESC_INBOXES) + }) + addArrangedSubview(inboxesList) + addArrangedSubview(createHorizontalSeparator()) + addArrangedSubview(Body1BoldLabel().apply { + text = texts.get(STRING_TITLE_OUTBOXES) + textColor = uiColorErgo + }) + addArrangedSubview(Body2Label().apply { + text = texts.get(STRING_DESC_OUTBOXES) + }) + addArrangedSubview(outBoxesList) + + val signButton = PrimaryButton(texts.get(STRING_LABEL_CONFIRM)).apply { + addOnTouchUpInsideListener { _, _ -> + startAuthFlow(uiLogic.wallet!!.walletConfig) { mnemonic -> + uiLogic.signTxWithMnemonicAsync(mnemonic, IosStringProvider(texts)) + } + } + } + val buttonContainer = UIView(CGRect.Zero()).apply { + addSubview(signButton) + signButton.fixedWidth(100.0).centerHorizontal().bottomToSuperview() + .topToSuperview(topInset = DEFAULT_MARGIN * 2) + } + addArrangedSubview(buttonContainer) + } + + fun bindTransaction(transactionInfo: TransactionInfo) { + inboxesList.clearArrangedSubviews() + transactionInfo.inputs.forEach { input -> + inboxesList.addArrangedSubview( + TransactionBoxEntryView().bindBoxView( + input.value, + input.address, + input.assets, + texts + ) + ) + } + outBoxesList.clearArrangedSubviews() + transactionInfo.outputs.forEach { output -> + outBoxesList.addArrangedSubview( + TransactionBoxEntryView().bindBoxView( + output.value, + output.address, + output.assets, + texts + ) + ) + } + } } inner class IosUiLogic : ColdWalletSigningUiLogic() { diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt new file mode 100644 index 000000000..f63cb5b6c --- /dev/null +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt @@ -0,0 +1,68 @@ +package org.ergoplatform.ios.transactions + +import com.badlogic.gdx.utils.I18NBundle +import org.ergoplatform.ErgoAmount +import org.ergoplatform.explorer.client.model.AssetInstanceInfo +import org.ergoplatform.ios.tokens.TokenEntryView +import org.ergoplatform.ios.ui.* +import org.ergoplatform.uilogic.STRING_LABEL_ERG_AMOUNT +import org.robovm.apple.coregraphics.CGRect +import org.robovm.apple.uikit.* + +class TransactionBoxEntryView : UIView(CGRect.Zero()) { + private val labelBoxAddress = Body1BoldLabel().apply { + numberOfLines = 1 + textColor = uiColorErgo + lineBreakMode = NSLineBreakMode.TruncatingMiddle + isUserInteractionEnabled = true + addGestureRecognizer(UITapGestureRecognizer { + numberOfLines = if (numberOfLines == 1L) 10 else 1 + }) + } + private val labelErgAmount = Body1BoldLabel().apply { + numberOfLines = 1 + } + private val tokensList = UIStackView().apply { + axis = UILayoutConstraintAxis.Vertical + } + + init { + layoutMargins = UIEdgeInsets.Zero() + + addSubview(labelBoxAddress) + addSubview(labelErgAmount) + addSubview(tokensList) + + labelBoxAddress.topToSuperview(topInset = DEFAULT_MARGIN).widthMatchesSuperview(inset = DEFAULT_MARGIN) + labelErgAmount.topToBottomOf(labelBoxAddress).widthMatchesSuperview(inset = DEFAULT_MARGIN * 2) + tokensList.topToBottomOf(labelErgAmount).widthMatchesSuperview(inset = DEFAULT_MARGIN * 2).bottomToSuperview() + } + + fun bindBoxView( + value: Long?, + address: String, + assets: List?, + texts: I18NBundle + ): TransactionBoxEntryView { + labelErgAmount.text = value?.let { + if (it != 0L) texts.format( + STRING_LABEL_ERG_AMOUNT, + ErgoAmount(value).toStringTrimTrailingZeros() + ) else null + } ?: "" + labelBoxAddress.text = address + + tokensList.apply { + clearArrangedSubviews() + + assets?.forEach { + addArrangedSubview(TokenEntryView().apply { + // we use the token id here, we don't have the name in the cold wallet context + bindWalletToken(it.tokenId, it.amount.toString()) + }) + } + } + + return this + } +} \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/ThemedLabels.kt b/ios/src/main/java/org/ergoplatform/ios/ui/ThemedLabels.kt index 8775812ba..a7e8ae8a3 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/ThemedLabels.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/ThemedLabels.kt @@ -41,10 +41,16 @@ class Body1BoldLabel() : Body1Label() { } } -class Body2Label() : ThemedLabel() { +open class Body2Label() : ThemedLabel() { override fun getFontSize() = 16.0 } +class Body2BoldLabel() : Body2Label() { + override fun isBold(): Boolean { + return true + } +} + class Headline1Label() : ThemedLabel() { override fun getFontSize() = FONT_SIZE_HEADLINE1 From fade2cc78138ba939209cd135cc446ec913033f1 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 11 Jan 2022 19:55:18 +0100 Subject: [PATCH 21/25] iOS Cold wallet ColdWalletSigningViewController add TransactionInfo, introduce TransactionBoxEntryView #54 --- .../transactions/ColdWalletSigningUiLogic.kt | 4 +- .../ColdWalletSigningViewController.kt | 30 +++- .../ios/transactions/PagedQrCodeContainer.kt | 133 +++++++++++++++++ .../transactions/SendFundsViewController.kt | 2 +- .../SigningPromptViewController.kt | 136 +++--------------- 5 files changed, 185 insertions(+), 120 deletions(-) create mode 100644 ios/src/main/java/org/ergoplatform/ios/transactions/PagedQrCodeContainer.kt diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt index e304e0f02..839903e48 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -114,7 +114,9 @@ abstract class ColdWalletSigningUiLogic { } notifyUiLocked(false) - state = State.PRESENT_RESULT + if (ergoTxResult.success && signedQrCode != null) { + state = State.PRESENT_RESULT + } notifySigningResult(ergoTxResult) } diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt index 6ca4a66a6..7537f4123 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import org.ergoplatform.explorer.client.model.TransactionInfo import org.ergoplatform.ios.ui.* import org.ergoplatform.transactions.SigningResult +import org.ergoplatform.transactions.coldSigningResponseToQrChunks import org.ergoplatform.transactions.reduceBoxes import org.ergoplatform.uilogic.* import org.ergoplatform.uilogic.transactions.ColdWalletSigningUiLogic @@ -18,6 +19,7 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p private val scanningContainer = ScanningContainer() private val transactionContainer = TransactionContainer() + private val signedQrCodesContainer = SignedQrCodeContainer() override fun viewDidLoad() { super.viewDidLoad() @@ -34,9 +36,11 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p view.addSubview(scrollView) scrollingContainer.addSubview(scanningContainer) scrollingContainer.addSubview(transactionContainer) + scrollingContainer.addSubview(signedQrCodesContainer) scanningContainer.edgesToSuperview(maxWidth = MAX_WIDTH) transactionContainer.edgesToSuperview(maxWidth = MAX_WIDTH) + signedQrCodesContainer.edgesToSuperview(maxWidth = MAX_WIDTH) scrollView.edgesToSuperview() } @@ -57,12 +61,13 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p transactionContainer.bindTransaction(it) } - refreshState() + refreshUiState() } - private fun refreshState() { + private fun refreshUiState() { scanningContainer.isHidden = uiLogic.state != ColdWalletSigningUiLogic.State.SCANNING transactionContainer.isHidden = uiLogic.state != ColdWalletSigningUiLogic.State.WAITING_TO_CONFIRM + signedQrCodesContainer.isHidden = uiLogic.state != ColdWalletSigningUiLogic.State.PRESENT_RESULT } private fun scanNext() { @@ -182,6 +187,16 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p } } + inner class SignedQrCodeContainer : PagedQrCodeContainer(texts, texts.get(STRING_LABEL_DISMISS)) { + override fun calcChunksFromRawData(rawData: String, limit: Int): List { + return coldSigningResponseToQrChunks(rawData, limit) + } + + override fun continueButtonPressed() { + navigationController.popViewController(true) + } + } + inner class IosUiLogic : ColdWalletSigningUiLogic() { private val progressViewController = ProgressViewController.ProgressViewControllerPresenter(this@ColdWalletSigningViewController) @@ -196,7 +211,16 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p } override fun notifySigningResult(ergoTxResult: SigningResult) { - TODO("Not yet implemented") + runOnMainThread { + if (ergoTxResult.success && uiLogic.signedQrCode != null) { + signedQrCodesContainer.rawData = uiLogic.signedQrCode + refreshUiState() + } else { + val message = texts.get(STRING_ERROR_PREPARE_TRANSACTION) + + ergoTxResult.errorMsg?.let { "\n\n$it" } + presentViewController(buildSimpleAlertController("", message, texts), true) {} + } + } } } } \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/PagedQrCodeContainer.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/PagedQrCodeContainer.kt new file mode 100644 index 000000000..1796ab019 --- /dev/null +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/PagedQrCodeContainer.kt @@ -0,0 +1,133 @@ +package org.ergoplatform.ios.transactions + +import com.badlogic.gdx.utils.I18NBundle +import org.ergoplatform.ios.ui.* +import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT +import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES +import org.ergoplatform.uilogic.STRING_BUTTON_NEXT +import org.ergoplatform.uilogic.STRING_DESC_PROMPT_SIGNING +import org.ergoplatform.uilogic.STRING_DESC_PROMPT_SIGNING_MULTIPLE +import org.ergoplatform.uilogic.STRING_LABEL_QR_PAGES_INFO +import org.robovm.apple.coregraphics.CGRect +import org.robovm.apple.uikit.* + +abstract class PagedQrCodeContainer( + protected val texts: I18NBundle, + continueButtonText: String +) : UIView(CGRect.Zero()) { + + private var qrPages: List = emptyList() + private var showLowRes = false + var rawData: String? = null + set(value) { + field = value + switchResButton.isHidden = (value?.length ?: 0) <= QR_DATA_LENGTH_LOW_RES + setQrCodesToPager() + } + + private val qrContainer = UIStackView().apply { + axis = UILayoutConstraintAxis.Horizontal + } + private val pager = qrContainer.wrapInHorizontalPager(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2).apply { + delegate = object : UIScrollViewDelegateAdapter() { + override fun didEndDecelerating(scrollView: UIScrollView?) { + super.didEndDecelerating(scrollView) + pageChanged(page) + } + } + } + private val description = Body1Label() + private val currPageLabel = Headline2Label() + private val nextButton = PrimaryButton(texts.get(STRING_BUTTON_NEXT)).apply { + addOnTouchUpInsideListener { _, _ -> + val nextPage = pager.page + 1 + scrollToQrCode(nextPage) + } + } + private val continueButton = PrimaryButton(continueButtonText).apply { + addOnTouchUpInsideListener { _, _ -> + continueButtonPressed() + } + } + + private val descContainer = UIView(CGRect.Zero()).apply { + layoutMargins = UIEdgeInsets.Zero() + } + + private val switchResButton = + UIImageView(getIosSystemImage(IMAGE_SWITCH_RESOLUTION, UIImageSymbolScale.Small)).apply { + tintColor = UIColor.label() + isUserInteractionEnabled = true + addGestureRecognizer(UITapGestureRecognizer { + showLowRes = !showLowRes + setQrCodesToPager() + }) + } + + init { + this.addSubview(pager) + this.addSubview(descContainer) + this.addSubview(switchResButton) + this.addSubview(currPageLabel) + + switchResButton.topToSuperview().rightToSuperview() + pager.topToBottomOf(switchResButton, DEFAULT_MARGIN).centerHorizontal() + descContainer.topToBottomOf(currPageLabel, DEFAULT_MARGIN * 3).widthMatchesSuperview(maxWidth = MAX_WIDTH) + .bottomToSuperview(bottomInset = DEFAULT_MARGIN) + currPageLabel.topToBottomOf(pager, DEFAULT_MARGIN).centerHorizontal() + + descContainer.apply { + addSubview(description) + addSubview(nextButton) + addSubview(continueButton) + + description.topToSuperview().widthMatchesSuperview() + nextButton.topToBottomOf(description, DEFAULT_MARGIN * 3).centerHorizontal().fixedWidth(120.0) + continueButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0) + .bottomToSuperview(canBeLess = true) + } + + } + + private fun setQrCodesToPager() { + rawData?.let { rawData -> + qrPages = calcChunksFromRawData(rawData, if (showLowRes) QR_DATA_LENGTH_LOW_RES else QR_DATA_LENGTH_LIMIT) + qrContainer.clearArrangedSubviews() + qrPages.forEach { + val qrCode = UIImageView(CGRect.Zero()) + qrCode.fixedWidth(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2).fixedHeight(DEFAULT_QR_CODE_SIZE) + qrCode.setQrCode(it, DEFAULT_QR_CODE_SIZE) + qrCode.contentMode = UIViewContentMode.Center + qrContainer.addArrangedSubview(qrCode) + } + pager.layoutIfNeeded() + scrollToQrCode(0) + } + } + + abstract fun calcChunksFromRawData(rawData: String, limit: Int): List + + private fun scrollToQrCode(nextPage: Int) { + pager.page = nextPage + pageChanged(nextPage) + } + + private fun pageChanged(newPage: Int) { + val texts = getAppDelegate().texts + val lastPage = newPage == qrPages.size - 1 + currPageLabel.text = + if (qrPages.size == 1) "" + else texts.format(STRING_LABEL_QR_PAGES_INFO, newPage + 1, qrPages.size) + + descContainer.animateLayoutChanges { + description.text = texts.get( + if (lastPage) STRING_DESC_PROMPT_SIGNING + else STRING_DESC_PROMPT_SIGNING_MULTIPLE + ) + nextButton.isHidden = lastPage + continueButton.isHidden = !lastPage + } + } + + abstract fun continueButtonPressed() +} \ No newline at end of file diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt index 7c7914d09..6188b83a2 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -362,7 +362,7 @@ class SendFundsViewController( override fun notifyUiLocked(locked: Boolean) { runOnMainThread { - progressViewController.setUiLocked(true) + progressViewController.setUiLocked(locked) } } diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index bdecf5c71..73c44d7a1 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -1,14 +1,13 @@ package org.ergoplatform.ios.transactions import org.ergoplatform.ios.ui.* -import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT -import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.transactions.getColdSignedTxChunk -import org.ergoplatform.uilogic.* +import org.ergoplatform.uilogic.STRING_BUTTON_SCAN_SIGNED_TX +import org.ergoplatform.uilogic.STRING_ERROR_QR_PAGES_NUM import org.ergoplatform.uilogic.transactions.SendFundsUiLogic -import org.robovm.apple.coregraphics.CGRect -import org.robovm.apple.uikit.* +import org.robovm.apple.uikit.UIColor +import org.robovm.apple.uikit.UIViewController /** * SigningPromptViewController is shown when user makes a transaction on a read-only address, presenting QR code(s) @@ -18,52 +17,36 @@ class SigningPromptViewController( private val signingPrompt: String, private val uiLogic: SendFundsUiLogic ) : UIViewController() { - private lateinit var description: UILabel - private lateinit var pager: UIScrollView - private lateinit var currPageLabel: Headline2Label - private lateinit var nextButton: PrimaryButton - private lateinit var scanButton: PrimaryButton - private lateinit var descContainer: UIView - private lateinit var switchResButton: UIImageView - private lateinit var qrContainer: UIStackView + private lateinit var qrPresenter: PagedQrCodeContainer - private lateinit var qrPages: List - private var showLowRes = false + val texts = getAppDelegate().texts override fun viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.systemBackground() - val closeButton = addCloseButton() - qrContainer = UIStackView(CGRect.Zero()).apply { - axis = UILayoutConstraintAxis.Horizontal - } + qrPresenter = QrCodeContainer() - pager = qrContainer.wrapInHorizontalPager(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2) - pager.delegate = object : UIScrollViewDelegateAdapter() { - override fun didEndDecelerating(scrollView: UIScrollView?) { - super.didEndDecelerating(scrollView) - pageChanged(pager.page) - } - } + view.addSubview(qrPresenter) + qrPresenter.edgesToSuperview() - currPageLabel = Headline2Label() + addCloseButton() + } - description = Body1Label().apply { - // make this compress as first (iPhone SE) - setContentCompressionResistancePriority(500f, UILayoutConstraintAxis.Vertical) - } + override fun viewWillAppear(animated: Boolean) { + super.viewWillAppear(animated) + qrPresenter.rawData = signingPrompt + } - val texts = getAppDelegate().texts - nextButton = PrimaryButton(texts.get(STRING_BUTTON_NEXT)) - nextButton.addOnTouchUpInsideListener { _, _ -> - val nextPage = pager.page + 1 - scrollToQrCode(nextPage) + inner class QrCodeContainer : PagedQrCodeContainer(texts, texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) { + override fun calcChunksFromRawData(rawData: String, limit: Int): List { + return coldSigningRequestToQrChunks( + rawData, limit + ) } - scanButton = PrimaryButton(texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) - scanButton.addOnTouchUpInsideListener { _, _ -> + override fun continueButtonPressed() { presentViewController( QrScannerViewController(dismissAnimated = false) { qrCode -> getColdSignedTxChunk(qrCode)?.let { @@ -90,82 +73,5 @@ class SigningPromptViewController( }, true ) {} } - switchResButton = UIImageView(getIosSystemImage(IMAGE_SWITCH_RESOLUTION, UIImageSymbolScale.Small)).apply { - tintColor = UIColor.label() - isUserInteractionEnabled = true - addGestureRecognizer(UITapGestureRecognizer { - showLowRes = !showLowRes - setQrCodesToPager() - }) - } - switchResButton.isHidden = signingPrompt.length <= QR_DATA_LENGTH_LOW_RES - - descContainer = UIView(CGRect.Zero()).apply { - layoutMargins = UIEdgeInsets.Zero() - } - - descContainer.addSubview(description) - descContainer.addSubview(nextButton) - descContainer.addSubview(scanButton) - view.addSubview(currPageLabel) - view.addSubview(pager) - view.addSubview(descContainer) - view.addSubview(switchResButton) - - pager.topToBottomOf(closeButton, DEFAULT_MARGIN).centerHorizontal() - currPageLabel.topToBottomOf(pager, DEFAULT_MARGIN).centerHorizontal() - descContainer.topToBottomOf(currPageLabel, DEFAULT_MARGIN * 3).widthMatchesSuperview(maxWidth = MAX_WIDTH) - .bottomToSuperview(bottomInset = DEFAULT_MARGIN) - description.topToSuperview().widthMatchesSuperview(inset = DEFAULT_MARGIN) - nextButton.topToBottomOf(description, DEFAULT_MARGIN * 3).centerHorizontal().fixedWidth(120.0) - .bottomToSuperview(canBeLess = true) - scanButton.topToTopOf(nextButton).centerHorizontal().fixedWidth(200.0) - .bottomToSuperview(canBeLess = true) - switchResButton.topToSuperview(topInset = DEFAULT_MARGIN).rightToSuperview() - - } - - override fun viewWillAppear(animated: Boolean) { - super.viewWillAppear(animated) - setQrCodesToPager() - } - - private fun setQrCodesToPager() { - qrPages = coldSigningRequestToQrChunks( - signingPrompt, - if (showLowRes) QR_DATA_LENGTH_LOW_RES else QR_DATA_LENGTH_LIMIT - ) - qrContainer.clearArrangedSubviews() - qrPages.forEach { - val qrCode = UIImageView(CGRect.Zero()) - qrCode.fixedWidth(DEFAULT_QR_CODE_SIZE + DEFAULT_MARGIN * 2).fixedHeight(DEFAULT_QR_CODE_SIZE) - qrCode.setQrCode(it, DEFAULT_QR_CODE_SIZE) - qrCode.contentMode = UIViewContentMode.Center - qrContainer.addArrangedSubview(qrCode) - } - pager.layoutIfNeeded() - scrollToQrCode(0) - } - - private fun scrollToQrCode(nextPage: Int) { - pager.page = nextPage - pageChanged(nextPage) - } - - private fun pageChanged(newPage: Int) { - val texts = getAppDelegate().texts - val lastPage = newPage == qrPages.size - 1 - currPageLabel.text = - if (qrPages.size == 1) "" - else texts.format(STRING_LABEL_QR_PAGES_INFO, newPage + 1, qrPages.size) - - descContainer.animateLayoutChanges { - description.text = texts.get( - if (lastPage) STRING_DESC_PROMPT_SIGNING - else STRING_DESC_PROMPT_SIGNING_MULTIPLE - ) - nextButton.isHidden = lastPage - scanButton.isHidden = !lastPage - } } } \ No newline at end of file From 9fd1e1cc5ed9c8deaac43d23ef5155b78c97c060 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 12 Jan 2022 10:36:29 +0100 Subject: [PATCH 22/25] SigningPromptDialogFragment support response with paged QR codes #58 --- .../transactions/ColdWalletSigningFragment.kt | 4 +- .../SigningPromptDialogFragment.kt | 48 +++++++++++-------- .../org/ergoplatform/android/ui/UiUtils.kt | 16 +++++++ .../layout/fragment_prompt_signing_dialog.xml | 12 ++++- android/src/main/res/values-es/strings.xml | 1 - android/src/main/res/values-zh/strings.xml | 1 - android/src/main/res/values/strings.xml | 1 - .../transactions/ColdWalletUtils.kt | 31 ++++++++++++ .../ergoplatform/uilogic/StringResources.kt | 1 - .../transactions/ColdWalletSigningUiLogic.kt | 30 ++---------- .../uilogic/transactions/SendFundsUiLogic.kt | 10 +++- ios/resources/i18n/strings.properties | 1 - ios/resources/i18n/strings_es.properties | 1 - ios/resources/i18n/strings_zh.properties | 1 - .../SigningPromptViewController.kt | 21 +++----- 15 files changed, 108 insertions(+), 71 deletions(-) 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 f346c99a3..5045f2589 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt @@ -151,8 +151,8 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() { // refresh information on scanned codes binding.labelScannedPages.text = getString( R.string.label_qr_pages_info, - viewModel.uiLogic.pagesAdded.toString(), - viewModel.uiLogic.pagesQrCode.toString() + viewModel.uiLogic.qrPagesCollector.pagesAdded.toString(), + viewModel.uiLogic.qrPagesCollector.pagesCount.toString() ) binding.cardScanMore.visibility = View.VISIBLE val errorMessage = viewModel.uiLogic.lastErrorMessage 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 0a34d7f16..b44556feb 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.android.material.snackbar.Snackbar import com.google.zxing.integration.android.IntentIntegrator import org.ergoplatform.android.Preferences import org.ergoplatform.android.R @@ -18,8 +17,11 @@ import org.ergoplatform.android.ui.AndroidStringProvider import org.ergoplatform.android.ui.QrPagerAdapter import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES +import org.ergoplatform.transactions.QrCodePagesCollector import org.ergoplatform.transactions.coldSigningRequestToQrChunks -import org.ergoplatform.transactions.getColdSignedTxChunk + +import org.ergoplatform.android.ui.expandBottomSheetOnShow + /** * SigningPromptDialogFragment is shown when user makes a transaction on a read-only address, presenting QR code(s) @@ -37,6 +39,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { savedInstanceState: Bundle? ): View { _binding = FragmentPromptSigningDialogBinding.inflate(inflater, container, false) + expandBottomSheetOnShow() return binding.root } @@ -67,6 +70,18 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { setQrCodePagerData(it) } } + + refreshScannedPagesInfo(viewModel.uiLogic.signedTxQrCodePagesCollector) + } + + private fun refreshScannedPagesInfo(pagesCollector: QrCodePagesCollector?) { + binding.qrScannedPagesInfo.visibility = + if (pagesCollector?.pagesAdded ?: 0 > 0) View.VISIBLE else View.INVISIBLE + + pagesCollector?.let { + binding.qrScannedPagesInfo.text = + getString(R.string.label_qr_pages_info, it.pagesAdded.toString(), it.pagesCount.toString()) + } } private fun setQrCodePagerData(data: String) { @@ -96,24 +111,17 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { result.contents?.let { qrCode -> - val qrChunk = getColdSignedTxChunk(qrCode) - qrChunk?.let { - if (it.pages > 1) { - // TODO cold wallet handle paged QR codes - Snackbar.make( - requireView(), - R.string.error_qr_pages_num, - Snackbar.LENGTH_LONG - ).setAnchorView(R.id.nav_view).show() - } else { - val context = requireContext() - getViewModel().uiLogic.sendColdWalletSignedTx( - listOf(qrCode), - Preferences(context), - AndroidStringProvider(context) - ) - dismiss() - } + val uiLogic = getViewModel().uiLogic + uiLogic.signedTxQrCodePagesCollector?.addPage(qrCode) + if (uiLogic.signedTxQrCodePagesCollector?.hasAllPages() == true) { + val context = requireContext() + uiLogic.sendColdWalletSignedTx( + Preferences(context), + AndroidStringProvider(context) + ) + dismiss() + } else { + refreshScannedPagesInfo(uiLogic.signedTxQrCodePagesCollector) } } } else { 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 d0253b26c..d45d97275 100644 --- a/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt +++ b/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt @@ -19,6 +19,9 @@ import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.navigation.NavOptions import androidx.navigation.Navigator +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.zxing.BarcodeFormat @@ -117,4 +120,17 @@ fun showDialogWithCopyOption(context: Context, message: String) { } .setNegativeButton(R.string.label_dismiss, null) .show() +} + +/** + * expands on first show. Call this in onCreateView() + */ +fun BottomSheetDialogFragment.expandBottomSheetOnShow() { + dialog?.setOnShowListener { dialog -> + val d = dialog as BottomSheetDialog + d.findViewById(com.google.android.material.R.id.design_bottom_sheet)?.let { + BottomSheetBehavior.from(it).state = + BottomSheetBehavior.STATE_EXPANDED + } + } } \ No newline at end of file diff --git a/android/src/main/res/layout/fragment_prompt_signing_dialog.xml b/android/src/main/res/layout/fragment_prompt_signing_dialog.xml index af01357a4..554bf413d 100644 --- a/android/src/main/res/layout/fragment_prompt_signing_dialog.xml +++ b/android/src/main/res/layout/fragment_prompt_signing_dialog.xml @@ -54,8 +54,18 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="@dimen/activity_horizontal_margin" - android:layout_marginBottom="24dp" android:text="@string/button_scan_signed_tx" /> + + \ No newline at end of file diff --git a/android/src/main/res/values-es/strings.xml b/android/src/main/res/values-es/strings.xml index aad996024..1f3d00f54 100644 --- a/android/src/main/res/values-es/strings.xml +++ b/android/src/main/res/values-es/strings.xml @@ -163,7 +163,6 @@ Cantidades salientes %1s de %2s Estas cantidades se enviarán cuando se procese la transacción. - Lo sentimos, los códigos QR fragmentados aún no son compatibles con esta versión de la aplicación. Su transacción fue firmada y debe enviarse a la red. Escanee este código QR para continuar. Su transacción fue firmada y debe enviarse a la red. diff --git a/android/src/main/res/values-zh/strings.xml b/android/src/main/res/values-zh/strings.xml index 4938bd7fa..41877f327 100644 --- a/android/src/main/res/values-zh/strings.xml +++ b/android/src/main/res/values-zh/strings.xml @@ -163,7 +163,6 @@ 支出金额 %2s中的 %1s 这些金额将在处理交易时发送。 - 抱歉,此应用版本尚不支持分块二维码。 您的交易已签名,需要发送到网络。 扫描此二维码以继续。 您的交易已签名,需要发送到网络。 diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 1745a9d6d..7070c563b 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -171,7 +171,6 @@ Outgoing amounts %1s of %2s These amounts will be sent when the transaction is processed. - Sorry, chunked QR codes are not yet supported by this app version. Your transaction was signed and needs to be sent to the network. Scan this QR code to proceed. Your transaction was signed and needs to be sent to the network. diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt index e0fd0aabb..30a3e3cd3 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -274,3 +274,34 @@ private fun getAssetInstanceInfos(tokensColl: Coll>): Lis } } +class QrCodePagesCollector(private val parser: (String) -> QrChunk?) { + private val qrCodeChunks = HashMap() + var pagesCount = 0 + val pagesAdded get() = qrCodeChunks.size + + fun addPage(qrCodeChunk: String): Boolean { + // returns false when QR code could not be not parsed + val qrChunk = parser.invoke(qrCodeChunk) ?: return false + + val page = qrChunk.index + val count = qrChunk.pages + + // returns false if new QR code has other pages count as the former ones, does not fit + if (pagesCount != 0 && count != pagesCount) { + return false + } + + qrCodeChunks.put(page, qrCodeChunk) + pagesCount = count + + return true + } + + fun hasAllPages(): Boolean { + return pagesAdded == pagesCount + } + + fun getAllPages(): Collection { + return qrCodeChunks.values + } +} diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt index 7aa6ef2e6..f1d47441e 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt @@ -104,7 +104,6 @@ const val STRING_ERROR_ICON_CONTENT_DESCRIPTION = "error_icon_content_descriptio const val STRING_ERROR_PASSWORD_EMPTY = "error_password_empty" const val STRING_ERROR_PASSWORD_WRONG = "error_password_wrong" const val STRING_ERROR_PREPARE_TRANSACTION = "error_prepare_transaction" -const val STRING_ERROR_QR_PAGES_NUM = "error_qr_pages_num" const val STRING_ERROR_RECEIVER_ADDRESS = "error_receiver_address" const val STRING_ERROR_REQUEST_TOKEN_AMOUNT = "error_request_token_amount" const val STRING_ERROR_REQUEST_TOKEN_BUT_NO_ERG = "error_request_token_but_no_erg" diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt index 839903e48..93dfe020c 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt @@ -18,9 +18,7 @@ abstract class ColdWalletSigningUiLogic { var state = State.SCANNING private set - var pagesQrCode = 0 - val pagesAdded get() = qrCodeChunks.size - private val qrCodeChunks = HashMap() + val qrPagesCollector = QrCodePagesCollector(::getColdSigningRequestChunk) var signedQrCode: String? = null private set private var signingRequest: PromptSigningResult? = null @@ -46,26 +44,8 @@ abstract class ColdWalletSigningUiLogic { return transactionInfo } - lastErrorMessage = null - - val qrChunk = getColdSigningRequestChunk(qrCodeChunk) - - // qr code not fitting or no qr code chunk - if (qrChunk == null) { - lastErrorMessage = "Not a cold signing QR code" - return null - } - - val page = qrChunk.index - val count = qrChunk.pages - - if (pagesQrCode != 0 && count != pagesQrCode) { - lastErrorMessage = "QR code does not belong to the formerly scanned codes" - return null - } - - qrCodeChunks.put(page, qrCodeChunk) - pagesQrCode = count + val added = qrPagesCollector.addPage(qrCodeChunk) + lastErrorMessage = if (added) null else "QR code does not belong to the formerly scanned codes" transactionInfo = buildRequestWhenApplicable() transactionInfo?.let { state = State.WAITING_TO_CONFIRM } @@ -74,9 +54,9 @@ abstract class ColdWalletSigningUiLogic { } private fun buildRequestWhenApplicable(): TransactionInfo? { - if (pagesAdded == pagesQrCode) { + if (qrPagesCollector.hasAllPages()) { try { - val sr = coldSigningRequestFromQrChunks(qrCodeChunks.values) + val sr = coldSigningRequestFromQrChunks(qrPagesCollector.getAllPages()) signingRequest = sr return buildTransactionInfoFromReduced( sr.serializedTx!!, diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt index 86507081f..1b94c5cd2 100644 --- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt @@ -200,7 +200,11 @@ abstract class SendFundsUiLogic { notifyUiLocked(true) } + var signedTxQrCodePagesCollector: QrCodePagesCollector? = null + private set + fun startColdWalletPayment(preferences: PreferencesProvider, texts: StringProvider) { + signedTxQrCodePagesCollector = QrCodePagesCollector(::getColdSignedTxChunk) wallet?.let { wallet -> val derivedAddresses = derivedAddressIdx?.let { listOf(wallet.getDerivedAddress(it)!!) } @@ -229,10 +233,14 @@ abstract class SendFundsUiLogic { } fun sendColdWalletSignedTx( - qrCodes: List, preferences: PreferencesProvider, texts: StringProvider ) { + val qrCodes = signedTxQrCodePagesCollector?.getAllPages() + signedTxQrCodePagesCollector = null + + if (qrCodes.isNullOrEmpty()) return // should not happen + notifyUiLocked(true) coroutineScope.launch { val ergoTxResult: SendTransactionResult diff --git a/ios/resources/i18n/strings.properties b/ios/resources/i18n/strings.properties index 00e87a237..11cebb7ea 100644 --- a/ios/resources/i18n/strings.properties +++ b/ios/resources/i18n/strings.properties @@ -111,7 +111,6 @@ error_icon_content_description=Error error_password_empty=Please enter a valid password error_password_wrong=Wrong password error_prepare_transaction=Could not prepare transaction -error_qr_pages_num=Sorry, chunked QR codes are not yet supported by this app version. error_receiver_address=Please enter a valid ERG address error_request_token_amount=Could not set requested amount for token \'{0}\' error_request_token_but_no_erg=Tokens requested, but no ERG amount. A default amount will be sent. diff --git a/ios/resources/i18n/strings_es.properties b/ios/resources/i18n/strings_es.properties index df3925f16..d5dcfd749 100644 --- a/ios/resources/i18n/strings_es.properties +++ b/ios/resources/i18n/strings_es.properties @@ -76,7 +76,6 @@ error_icon_content_description=Error error_password_empty=Por favor introduce una contraseña válida error_password_wrong=Wrong password error_prepare_transaction=No se pudo preparar la transacción -error_qr_pages_num=Lo sentimos, los códigos QR fragmentados aún no son compatibles con esta versión de la aplicación. error_receiver_address=Ingrese una dirección de ERG válida error_send_transaction=No se pudo enviar la transacción error_token_amount=Ingrese cantidades de tokens válidas o elimine las entradas de tokens no válidas diff --git a/ios/resources/i18n/strings_zh.properties b/ios/resources/i18n/strings_zh.properties index 1c3a32c2e..f6642d6a5 100644 --- a/ios/resources/i18n/strings_zh.properties +++ b/ios/resources/i18n/strings_zh.properties @@ -71,7 +71,6 @@ error_device_security=安全错误:{0} error_password_empty=请输入有效密码 error_password_wrong=密码错误 error_prepare_transaction=无法准备交易 -error_qr_pages_num=抱歉,此应用版本尚不支持分块二维码。 error_receiver_address=请输入有效 ERG 地址 error_send_transaction=无法发送交易 error_token_amount=请输入有效金额或移除不发送的代币条目 diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index 73c44d7a1..94f5c14c9 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -2,9 +2,7 @@ package org.ergoplatform.ios.transactions import org.ergoplatform.ios.ui.* import org.ergoplatform.transactions.coldSigningRequestToQrChunks -import org.ergoplatform.transactions.getColdSignedTxChunk import org.ergoplatform.uilogic.STRING_BUTTON_SCAN_SIGNED_TX -import org.ergoplatform.uilogic.STRING_ERROR_QR_PAGES_NUM import org.ergoplatform.uilogic.transactions.SendFundsUiLogic import org.robovm.apple.uikit.UIColor import org.robovm.apple.uikit.UIViewController @@ -39,7 +37,8 @@ class SigningPromptViewController( qrPresenter.rawData = signingPrompt } - inner class QrCodeContainer : PagedQrCodeContainer(texts, texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) { + inner class QrCodeContainer : + PagedQrCodeContainer(texts, texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) { override fun calcChunksFromRawData(rawData: String, limit: Int): List { return coldSigningRequestToQrChunks( rawData, limit @@ -49,26 +48,18 @@ class SigningPromptViewController( override fun continueButtonPressed() { presentViewController( QrScannerViewController(dismissAnimated = false) { qrCode -> - getColdSignedTxChunk(qrCode)?.let { - if (it.pages > 1) { - // TODO cold wallet handle paged QR codes - val uac = - buildSimpleAlertController( - "", - texts.get(STRING_ERROR_QR_PAGES_NUM), - getAppDelegate().texts - ) - presentViewController(uac, false) {} - } else { + uiLogic.signedTxQrCodePagesCollector?.let { + it.addPage(qrCode) + if (it.hasAllPages()) { dismissViewController(false) { val delegate = getAppDelegate() uiLogic.sendColdWalletSignedTx( - listOf(qrCode), delegate.prefs, IosStringProvider(delegate.texts) ) } } + // TODO cold wallet show pages info } }, true ) {} From 5adcdc4332ed27549e2f9ec9db773449a16fceec Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 12 Jan 2022 11:44:00 +0100 Subject: [PATCH 23/25] iOS SigningPromptViewController scrollbar, support paged QR codes #54 --- .../ColdWalletSigningViewController.kt | 6 +++- .../SigningPromptViewController.kt | 33 ++++++++++++++++--- .../java/org/ergoplatform/ios/ui/UIUtils.kt | 9 ++++- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt index 7537f4123..3b94452e0 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt @@ -54,7 +54,11 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p uiLogic.addQrCodeChunk(qrCodeChunk) scanningContainer.statusText.text = - texts.format(STRING_LABEL_QR_PAGES_INFO, uiLogic.pagesAdded, uiLogic.pagesQrCode) + texts.format( + STRING_LABEL_QR_PAGES_INFO, + uiLogic.qrPagesCollector.pagesAdded, + uiLogic.qrPagesCollector.pagesCount + ) scanningContainer.errorText.text = uiLogic.lastErrorMessage ?: "" uiLogic.transactionInfo?.reduceBoxes()?.let { diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt index 94f5c14c9..1be8bb981 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt @@ -3,8 +3,12 @@ package org.ergoplatform.ios.transactions import org.ergoplatform.ios.ui.* import org.ergoplatform.transactions.coldSigningRequestToQrChunks import org.ergoplatform.uilogic.STRING_BUTTON_SCAN_SIGNED_TX +import org.ergoplatform.uilogic.STRING_LABEL_QR_PAGES_INFO import org.ergoplatform.uilogic.transactions.SendFundsUiLogic +import org.robovm.apple.coregraphics.CGRect import org.robovm.apple.uikit.UIColor +import org.robovm.apple.uikit.UIScrollView +import org.robovm.apple.uikit.UIView import org.robovm.apple.uikit.UIViewController /** @@ -17,7 +21,9 @@ class SigningPromptViewController( ) : UIViewController() { private lateinit var qrPresenter: PagedQrCodeContainer - val texts = getAppDelegate().texts + private val texts = getAppDelegate().texts + private val scannedPagesLabel = Headline2Label() + private lateinit var scrollView: UIScrollView override fun viewDidLoad() { super.viewDidLoad() @@ -26,9 +32,16 @@ class SigningPromptViewController( qrPresenter = QrCodeContainer() - view.addSubview(qrPresenter) - qrPresenter.edgesToSuperview() + val scrollingContainer = UIView(CGRect.Zero()) + scrollView = scrollingContainer.wrapInVerticalScrollView() + scrollingContainer.addSubview(qrPresenter) + scrollingContainer.addSubview(scannedPagesLabel) + qrPresenter.topToSuperview().widthMatchesSuperview() + scannedPagesLabel.topToBottomOf(qrPresenter).bottomToSuperview().centerHorizontal() + + view.addSubview(scrollView) + scrollView.edgesToSuperview() addCloseButton() } @@ -37,6 +50,15 @@ class SigningPromptViewController( qrPresenter.rawData = signingPrompt } + fun refreshPagesInfo() { + scannedPagesLabel.text = uiLogic.signedTxQrCodePagesCollector?.let { + if (it.pagesAdded > 0) texts.format( + STRING_LABEL_QR_PAGES_INFO, + it.pagesAdded, it.pagesCount + ) else "" + } ?: "" + } + inner class QrCodeContainer : PagedQrCodeContainer(texts, texts.get(STRING_BUTTON_SCAN_SIGNED_TX)) { override fun calcChunksFromRawData(rawData: String, limit: Int): List { @@ -58,8 +80,11 @@ class SigningPromptViewController( IosStringProvider(delegate.texts) ) } + } else { + refreshPagesInfo() + scrollView.layoutIfNeeded() + scrollView.scrollToBottom() } - // TODO cold wallet show pages info } }, true ) {} diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt index 0a910a383..aca7bfb00 100644 --- a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt +++ b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt @@ -270,4 +270,11 @@ var UIScrollView.page get() = (contentOffset.x / frame.size.width).toInt() set(value) { setContentOffset(CGPoint(frame.size.width * value.toDouble(), contentOffset.y), true) - } \ No newline at end of file + } + +fun UIScrollView.scrollToBottom(animated: Boolean = true) { + setContentOffset( + CGPoint(0.0, contentSize.height - bounds.size.height + contentInset.bottom), + animated + ) +} \ No newline at end of file From 76e28516c90d1b9685a0ab78de79d8584a5fc2c5 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 12 Jan 2022 13:58:31 +0100 Subject: [PATCH 24/25] Cold wallet qr code chunking fix wrong size limit usage #58 --- .../java/org/ergoplatform/transactions/ColdWalletUtils.kt | 4 ++-- .../org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt index 30a3e3cd3..3b50556cd 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -118,7 +118,7 @@ private fun buildQrChunks( sizeLimit: Int, serializedSigningRequest: String ): List { - val actualSizeLimit = sizeLimit - 50 - prefix.length // reserve some space for our prefix + val actualSizeLimit = sizeLimit - 30 - prefix.length // reserve some space for our prefix val gson = GsonBuilder().disableHtmlEscaping().create() @@ -127,7 +127,7 @@ private fun buildQrChunks( root.addProperty(prefix, serializedSigningRequest) return listOf(gson.toJson(root)) } else { - val chunks = serializedSigningRequest.chunked(sizeLimit) + val chunks = serializedSigningRequest.chunked(actualSizeLimit) var slice = 0 return chunks.map { slice++ diff --git a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt index e9b4fa9a4..e543356e0 100644 --- a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt +++ b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt @@ -18,7 +18,7 @@ class ColdWalletUtilsKtTest { Assert.assertNotNull(csr) - val manyChunks = coldSigningRequestToQrChunks(csr!!, 30) + val manyChunks = coldSigningRequestToQrChunks(csr!!, 50) val oneChunk = coldSigningRequestToQrChunks(csr, 50000000) Assert.assertEquals(1, oneChunk.size) Assert.assertEquals(1, getColdSigningRequestChunk(oneChunk.first())?.index) From aba4294b6ac681cd3bc4b82f86fe69537d8c53e1 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 12 Jan 2022 14:21:05 +0100 Subject: [PATCH 25/25] Build 2203, raise to 1.5 due to changes in cold wallet interchange format --- android/build.gradle | 4 ++-- ios/robovm.properties | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index f53631b0c..75834ad01 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -13,8 +13,8 @@ android { applicationId "org.ergoplatform.android" minSdkVersion 24 targetSdkVersion 30 - versionCode 2201 - versionName "1.4.2201" + versionCode 2203 + versionName "1.5.2203" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/ios/robovm.properties b/ios/robovm.properties index a7c215c0d..b9faba1ee 100644 --- a/ios/robovm.properties +++ b/ios/robovm.properties @@ -1,6 +1,6 @@ -app.version=1.1.2201 +app.version=1.5.2203 app.id=org.ergoplatform.ios app.mainclass=org.ergoplatform.ios.Main app.executable=ErgoWallet -app.build=2201 +app.build=2203 app.name=Ergo Wallet \ No newline at end of file