From 64cb4ca9e2d7799fda6b331807110817d5cc668d Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 21 Oct 2020 18:16:24 +0200 Subject: [PATCH] Implemented reading QR coding and if it is a EPC QR code navigating to TransferMoneyDialog --- build.gradle | 4 + ui/BankingAndroidApp/build.gradle | 3 + .../src/main/AndroidManifest.xml | 16 ++- .../banking/ui/android/MainActivity.kt | 55 +++++++- .../MainActivityFloatingActionMenuButton.kt | 35 ++++- .../view_floating_action_button_main.xml | 8 ++ .../src/main/res/values-de/strings.xml | 5 + .../src/main/res/values/strings.xml | 5 + .../mainwindow/controls/MainMenuBar.kt | 10 +- ui/BankingUiCommon/build.gradle | 2 + .../banking/ui/presenter/BankingPresenter.kt | 27 +++- ui/BankingUiNativeIntegration/build.gradle | 5 +- .../BankingiOSApp.xcodeproj/project.pbxproj | 8 ++ .../Base.lproj/Localizable.strings | 3 +- ui/BankingiOSApp/BankingiOSApp/Info.plist | 2 + .../BankingiOSApp/SceneDelegate.swift | 2 +- .../BankingiOSApp/TabBarController.swift | 35 ++++- .../de.lproj/Localizable.strings | 1 + .../ui/dialogs/ScanQrCodeViewController.swift | 131 ++++++++++++++++++ .../AccountTransactionListItem.swift | 2 +- 20 files changed, 338 insertions(+), 21 deletions(-) create mode 100644 ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ScanQrCodeViewController.swift diff --git a/build.gradle b/build.gradle index f05eb9fa..37085909 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,8 @@ ext { autocompleteVersion = "1.1.0" + zxingVersion = "3.3.0" + multiDexVersion = "2.0.1" appCompatVersion = "1.1.0" @@ -149,6 +151,7 @@ task jarAll { "fints4k:jvm6Jar", "fints4k-jvm:jar", "BankFinder:jvmJar", + "EpcQrCodeParser:jvmJar", "BankingUiCommon:jvmJar", "fints4kBankingClient:jvmJar", "BankingUiCommon:jvmJar", @@ -162,6 +165,7 @@ task packAllForXcode { "common:packForXcode", "fints4k:packForXcode", "BankFinder:packForXcode", + "EpcQrCodeParser:packForXcode", "BankingUiCommon:packForXcode", "fints4kBankingClient:packForXcode", "BankingUiNativeIntegration:packForXcode" diff --git a/ui/BankingAndroidApp/build.gradle b/ui/BankingAndroidApp/build.gradle index 982c9d0d..41014f43 100644 --- a/ui/BankingAndroidApp/build.gradle +++ b/ui/BankingAndroidApp/build.gradle @@ -106,6 +106,9 @@ dependencies { implementation "com.otaliastudios:autocomplete:$autocompleteVersion" + implementation("com.journeyapps:zxing-android-embedded:4.1.0") { transitive = false } // transitive to use older Zxing version as ZXing 3.4.0 requires Android > 23 + implementation "com.google.zxing:core:$zxingVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion" diff --git a/ui/BankingAndroidApp/src/main/AndroidManifest.xml b/ui/BankingAndroidApp/src/main/AndroidManifest.xml index 87bbfd63..e8552e3c 100644 --- a/ui/BankingAndroidApp/src/main/AndroidManifest.xml +++ b/ui/BankingAndroidApp/src/main/AndroidManifest.xml @@ -1,12 +1,20 @@ - + + + + + + + android:hardwareAccelerated="true" + android:theme="@style/AppTheme" + > , grantResults: IntArray) { + permissionsService.onRequestPermissionsResult(requestCode, permissions, grantResults) + + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val scanQrCodeResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + + if (scanQrCodeResult != null) { + // at this point camera activity is still displayed and not returned yet to MainActivity -> app would crash if we don't wait + Handler(Looper.getMainLooper()).postDelayed(250) { + handleQrCodeScanResult(scanQrCodeResult) + } + } + else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + private fun handleQrCodeScanResult(scanQrCodeResult: IntentResult) { + scanQrCodeResult.contents?.let { decodedQrCode -> + val result = presenter.showTransferMoneyDialogWithDataFromQrCode(decodedQrCode) + + if (result.successful == false) { + showParseQrCodeError(result) + } + } + } + + protected fun showParseQrCodeError(result: ParseEpcQrCodeResult) { + // TODO: show localized error message that matches ParseEpcQrCodeResultCode + val errorMessage = getString(R.string.money_transfer_from_scanning_qr_code_error, result.error, result.decodedQrCode) + + AlertDialog.Builder(this) + .setMessage(errorMessage) + .setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() } + .show() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { if(floatingActionMenuButton.handlesTouch(event)) { // close menu when menu is opened and touch is outside floatingActionMenuButton return true diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/MainActivityFloatingActionMenuButton.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/MainActivityFloatingActionMenuButton.kt index 4bfae511..3a4bc322 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/MainActivityFloatingActionMenuButton.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/MainActivityFloatingActionMenuButton.kt @@ -1,11 +1,13 @@ package net.dankito.banking.ui.android.views +import android.Manifest import android.content.Context import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import com.github.clans.fab.FloatingActionButton import com.github.clans.fab.FloatingActionMenu +import com.google.zxing.integration.android.IntentIntegrator import kotlinx.android.synthetic.main.view_floating_action_button_main.view.* import net.dankito.utils.multiplatform.toFile import net.dankito.banking.ui.android.R @@ -27,6 +29,8 @@ open class MainActivityFloatingActionMenuButton( protected lateinit var fabTransferMoney: FloatingActionButton + protected lateinit var fabMoneyTransferFromScanningQrCode: FloatingActionButton + protected lateinit var fabTransferMoneyFromPdf: FloatingActionButton protected var lastSelectedFolder: File? = null @@ -51,14 +55,19 @@ open class MainActivityFloatingActionMenuButton( } fabTransferMoney = floatingActionMenu.fabTransferMoney + fabMoneyTransferFromScanningQrCode = floatingActionMenu.fabMoneyTransferFromScanningQrCode fabTransferMoneyFromPdf = floatingActionMenu.fabTransferMoneyFromPdf fabTransferMoney.setOnClickListener { executeAndCloseMenu { presenter.showTransferMoneyDialog() } } + fabMoneyTransferFromScanningQrCode.setOnClickListener { + executeAndCloseMenu { scanQrCode() } + } + fabTransferMoneyFromPdf.setOnClickListener { - executeAndCloseMenu { transferMoneyWithDataFromPdf() } + executeAndCloseMenu { showTransferMoneyDialogWithDataFromPdf() } } } } @@ -67,11 +76,27 @@ open class MainActivityFloatingActionMenuButton( protected open fun checkIfThereAreAccountsThatCanTransferMoney() { fabTransferMoney.isEnabled = presenter.hasAccountsSupportTransferringMoney + fabMoneyTransferFromScanningQrCode.isEnabled = presenter.hasAccountsSupportTransferringMoney + fabTransferMoneyFromPdf.isEnabled = presenter.hasAccountsSupportTransferringMoney } - protected open fun transferMoneyWithDataFromPdf() { + protected open fun scanQrCode() { + permissionsService.checkPermission(Manifest.permission.CAMERA, R.string.rationale_camera_permission_to_scan_qr_code) { _, isGranted -> + if (isGranted) { + floatingActionMenu.context.asActivity()?.let { activity -> + val intentIntegrator = IntentIntegrator(activity) + intentIntegrator.setOrientationLocked(false) + intentIntegrator.initiateScan(listOf(IntentIntegrator.QR_CODE)) + + // parsing decoded QR-code and showing TransferMoneyDialog is done in MainActivity.handleQrCodeScanResult() + } + } + } + } + + protected open fun showTransferMoneyDialogWithDataFromPdf() { (floatingActionMenu.context.asActivity() as? FragmentActivity)?.let { activity -> val config = FileChooserDialogConfig(listOf("*.pdf"), lastSelectedFolder) @@ -79,17 +104,17 @@ open class MainActivityFloatingActionMenuButton( selectedFile?.let { lastSelectedFolder = selectedFile.parentFile - val result = presenter.transferMoneyWithDataFromPdf(selectedFile.toFile()) + val result = presenter.showTransferMoneyDialogWithDataFromPdf(selectedFile.toFile()) if (result.type != ExtractTransferMoneyDataFromPdfResultType.Success) { - showTransferMoneyWithDataFromPdfError(activity, selectedFile, result) + showTransferMoneyDialogWithDataFromPdfError(activity, selectedFile, result) } } } } } - protected open fun showTransferMoneyWithDataFromPdfError(context: Context, pdfFile: File, result: ExtractTransferMoneyDataFromPdfResult) { + protected open fun showTransferMoneyDialogWithDataFromPdfError(context: Context, pdfFile: File, result: ExtractTransferMoneyDataFromPdfResult) { val errorMessage = when (result.type) { ExtractTransferMoneyDataFromPdfResultType.NotASearchablePdf -> context.getString(R.string.transfer_money_from_pdf_error_message_not_a_searchable_pdf, pdfFile.absolutePath) diff --git a/ui/BankingAndroidApp/src/main/res/layout/view_floating_action_button_main.xml b/ui/BankingAndroidApp/src/main/res/layout/view_floating_action_button_main.xml index a24b5f16..f5aaa8d9 100644 --- a/ui/BankingAndroidApp/src/main/res/layout/view_floating_action_button_main.xml +++ b/ui/BankingAndroidApp/src/main/res/layout/view_floating_action_button_main.xml @@ -25,6 +25,14 @@ fab:fab_label="@string/floating_action_menu_transfer_money_from_pdf" /> + + Konto Überweisung + Überweisung aus QR-Code Überweisung aus PDF + Um QR-Codes scannen zu können wird der Zugriff auf die Kamera benötigt. + Umsätze Umsätze aktualisieren @@ -107,6 +110,8 @@ %1$s %2$s wurden erfolgreich an %3$s überwiesen. Konnte nicht %1$s %2$s an %3$s überweisen.\n\nFehlermeldung Ihrer Bank:\n\n%4$s + Überweisungsdaten konnten nicht aus QR-Code gelesen werden: %1$s\n\Gelesener QR-Code ist:\n%2$s + Konnte Text nicht aus Datei "%1$s" extrahieren. Enthält die Datei auch Text oder nur Bilder? Text konnte nicht aus Datei "%1$s" extrahiert werden:\n\n%2$s Überweisungsdaten konnten aus der Datei "%1$s" nicht ausgelesen werden:\n\n%2$s diff --git a/ui/BankingAndroidApp/src/main/res/values/strings.xml b/ui/BankingAndroidApp/src/main/res/values/strings.xml index 198d7f10..1743d880 100644 --- a/ui/BankingAndroidApp/src/main/res/values/strings.xml +++ b/ui/BankingAndroidApp/src/main/res/values/strings.xml @@ -63,8 +63,11 @@ Account Transfer money + Money transfer from scanning QR-Code Transfer money from PDF + To scan QR-Codes permission to access camera is required. + Home Update transactions @@ -107,6 +110,8 @@ Successfully transferred %1$s %2$s to %3$s. Could not transfer %1$s %2$s to %3$s.\n\nError message from your bank:\n\n%4$s + Could not extract transfer data from QR-Code: %1$s\n\nExtracted QR-Code was:\n%2$s + File "%1$s" is not a searchable PDF. Could therefore not extract text from it. Could not extract text from file "%1$s":\n\n%2$s Could not extract cash transfer data from file "%1$s":\n\n%2$s diff --git a/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/controls/MainMenuBar.kt b/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/controls/MainMenuBar.kt index 4b78d472..b0ebccf1 100644 --- a/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/controls/MainMenuBar.kt +++ b/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/controls/MainMenuBar.kt @@ -54,7 +54,7 @@ open class MainMenuBar(protected val presenter: BankingPresenter) : View() { item(messages["main.window.menu.file.new.cash.transfer.from.pdf"], KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCodeCombination.SHIFT_DOWN)) { enableWhen(areAccountsThatCanTransferMoneyAdded) - action { transferMoneyWithDataFromPdf() } + action { showTransferMoneyDialogWithDataFromPdf() } } } @@ -71,7 +71,7 @@ open class MainMenuBar(protected val presenter: BankingPresenter) : View() { areAccountsThatCanTransferMoneyAdded.value = presenter.hasAccountsSupportTransferringMoney } - protected open fun transferMoneyWithDataFromPdf() { + protected open fun showTransferMoneyDialogWithDataFromPdf() { val fileChooser = FileChooser() fileChooser.initialDirectory = lastSelectedFolder @@ -80,15 +80,15 @@ open class MainMenuBar(protected val presenter: BankingPresenter) : View() { fileChooser.showOpenDialog(currentStage)?.let { pdfFile -> lastSelectedFolder = pdfFile.parentFile - val result = presenter.transferMoneyWithDataFromPdf(pdfFile.toFile()) + val result = presenter.showTransferMoneyDialogWithDataFromPdf(pdfFile.toFile()) if (result.type != ExtractTransferMoneyDataFromPdfResultType.Success) { - showTransferMoneyWithDataFromPdfError(pdfFile, result) + showTransferMoneyDialogWithDataFromPdfError(pdfFile, result) } } } - protected open fun showTransferMoneyWithDataFromPdfError(pdfFile: File, result: ExtractTransferMoneyDataFromPdfResult) { + protected open fun showTransferMoneyDialogWithDataFromPdfError(pdfFile: File, result: ExtractTransferMoneyDataFromPdfResult) { val errorMessageKey = when (result.type) { ExtractTransferMoneyDataFromPdfResultType.NotASearchablePdf -> "transfer.money.from.pdf.error.message.not.a.searchable.pdf" ExtractTransferMoneyDataFromPdfResultType.CouldNotExtractText -> "transfer.money.from.pdf.error.message.could.not.extract.text" diff --git a/ui/BankingUiCommon/build.gradle b/ui/BankingUiCommon/build.gradle index 69926251..6f720331 100644 --- a/ui/BankingUiCommon/build.gradle +++ b/ui/BankingUiCommon/build.gradle @@ -40,6 +40,8 @@ kotlin { api project(":fints4k") api project(":BankFinder") + + api project(":EpcQrCodeParser") } } diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt index de8f16c8..866fa4d4 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt @@ -32,6 +32,7 @@ import net.dankito.banking.util.extraction.IInvoiceDataExtractor import net.dankito.banking.util.extraction.ITextExtractorRegistry import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry +import net.codinux.banking.tools.epcqrcode.* import net.dankito.utils.multiplatform.* import net.dankito.utils.multiplatform.log.LoggerFactory import kotlin.collections.ArrayList @@ -49,7 +50,8 @@ open class BankingPresenter( protected val textExtractorRegistry: ITextExtractorRegistry = NoOpTextExtractorRegistry(), protected val invoiceDataExtractor: IInvoiceDataExtractor = NoOpInvoiceDataExtractor(), protected val currencyInfoProvider: ICurrencyInfoProvider = CurrencyInfoProvider(), - protected val asyncRunner: IAsyncRunner = CoroutinesAsyncRunner() + protected val asyncRunner: IAsyncRunner = CoroutinesAsyncRunner(), + protected val qrCodeParser: EpcQrCodeParser = EpcQrCodeParser() // TODO: create interface ) { companion object { @@ -586,7 +588,28 @@ open class BankingPresenter( } } - open fun transferMoneyWithDataFromPdf(pdf: File): ExtractTransferMoneyDataFromPdfResult { + open fun showTransferMoneyDialogWithDataFromQrCode(decodedQrCode: String): ParseEpcQrCodeResult { + val result = qrCodeParser.parseEpcQrCode(decodedQrCode) + + if (result.successful) { + result.epcQrCode?.let { epcQrCode -> + // TODO: show originatorInformation to user + + val transferMoneyData = TransferMoneyData( + allAccounts.first(), + epcQrCode.receiverName, + epcQrCode.iban, + epcQrCode.bic ?: "", + epcQrCode.amount?.let { BigDecimal(it) } ?: BigDecimal.Zero, + epcQrCode.remittance) + showTransferMoneyDialog(transferMoneyData) + } + } + + return result + } + + open fun showTransferMoneyDialogWithDataFromPdf(pdf: File): ExtractTransferMoneyDataFromPdfResult { val extractionResult = textExtractorRegistry.extractTextWithBestExtractorForFile(pdf) if (extractionResult.couldExtractText == false || extractionResult.text == null) { diff --git a/ui/BankingUiNativeIntegration/build.gradle b/ui/BankingUiNativeIntegration/build.gradle index c29d1da6..be4c6c68 100644 --- a/ui/BankingUiNativeIntegration/build.gradle +++ b/ui/BankingUiNativeIntegration/build.gradle @@ -6,6 +6,8 @@ plugins { ext.artifactName = "banking-ui-native-integration" +def frameworkName = "BankingUiSwift" + kotlin { @@ -15,12 +17,13 @@ kotlin { fromPreset(iOSTarget, 'ios') { binaries { framework { - baseName = "BankingUiSwift" + baseName = frameworkName // transitiveExport = true export(project(":BankingUiCommon")) export(project(":fints4kBankingClient")) export(project(":BankFinder")) + export(project(":EpcQrCodeParser")) // do not add fints4k to exports, would lead to a lot of naming conflicts. In this way fints4k classes get prefixed with 'Fints4k' which is Ok // export(project(":fints4k")) // exporting common would lead to naming conflicts with Foundation classes like Date, UUID, Thread, ... diff --git a/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj b/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj index 957f105c..df5c3f98 100644 --- a/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj +++ b/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ 3642F01A2502931F005186FE /* RealTimeTransferInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3642F0192502931F005186FE /* RealTimeTransferInfoView.swift */; }; 3642F04B25031157005186FE /* SectionHeaderWithRightAlignedEditButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3642F04A25031157005186FE /* SectionHeaderWithRightAlignedEditButton.swift */; }; 36671255253A761500BD2301 /* BankCredentialsPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36671254253A761500BD2301 /* BankCredentialsPasswordView.swift */; }; + 36671290253F8D5200BD2301 /* ScanQrCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3667128F253F8D5200BD2301 /* ScanQrCodeViewController.swift */; }; + 36671294254045E100BD2301 /* EpcQrCodeParser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 36671293254045E100BD2301 /* EpcQrCodeParser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 36693A4E25280BCB00BB7AE5 /* InfoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36693A4D25280BCB00BB7AE5 /* InfoButton.swift */; }; 366FA4DA24C472A90094F009 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4D924C472A90094F009 /* Extensions.swift */; }; 366FA4DC24C479120094F009 /* BankInfoListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4DB24C479120094F009 /* BankInfoListItem.swift */; }; @@ -157,6 +159,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 36671294254045E100BD2301 /* EpcQrCodeParser.framework in Embed Frameworks */, 3684EB8F250B7F3C0001139E /* BankingUiCommon.framework in Embed Frameworks */, 36BCF86A24BA550D005BEC29 /* BankFinder.framework in Embed Frameworks */, 36BCF85F24BA4DA8005BEC29 /* MultiplatformUtils.framework in Embed Frameworks */, @@ -199,6 +202,8 @@ 3642F0192502931F005186FE /* RealTimeTransferInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealTimeTransferInfoView.swift; sourceTree = ""; }; 3642F04A25031157005186FE /* SectionHeaderWithRightAlignedEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderWithRightAlignedEditButton.swift; sourceTree = ""; }; 36671254253A761500BD2301 /* BankCredentialsPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankCredentialsPasswordView.swift; sourceTree = ""; }; + 3667128F253F8D5200BD2301 /* ScanQrCodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQrCodeViewController.swift; sourceTree = ""; }; + 36671293254045E100BD2301 /* EpcQrCodeParser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EpcQrCodeParser.framework; path = "../../tools/EpcQrCodeParser/build/xcode-frameworks/EpcQrCodeParser.framework"; sourceTree = ""; }; 36693A4D25280BCB00BB7AE5 /* InfoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoButton.swift; sourceTree = ""; }; 366FA4D924C472A90094F009 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 366FA4DB24C479120094F009 /* BankInfoListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankInfoListItem.swift; sourceTree = ""; }; @@ -371,6 +376,7 @@ 36E21ED424DC549800649DC8 /* BankSettingsDialog.swift */, 36E21ED624DC617200649DC8 /* BankAccountSettingsDialog.swift */, 36B8A4472503D12100C15359 /* ProtectAppSettingsDialog.swift */, + 3667128F253F8D5200BD2301 /* ScanQrCodeViewController.swift */, ); path = dialogs; sourceTree = ""; @@ -481,6 +487,7 @@ 36FC928F24B39A05002B12E9 = { isa = PBXGroup; children = ( + 36671293254045E100BD2301 /* EpcQrCodeParser.framework */, 3684EB8E250B7F3C0001139E /* BankingUiCommon.framework */, 3684EB8C250B7F2B0001139E /* BankingUiCommon.framework.dSYM */, 36FC929A24B39A05002B12E9 /* BankingiOSApp */, @@ -992,6 +999,7 @@ 36BE068F24CEE1BD00CBBB68 /* AllBanksListItem.swift in Sources */, 360782C324E49FF70098FEFE /* ValidationLabel.swift in Sources */, 3684EB92250FD4AF0001139E /* LabelledValue.swift in Sources */, + 36671290253F8D5200BD2301 /* ScanQrCodeViewController.swift in Sources */, 36BE069124CEF52800CBBB68 /* UpdateButton.swift in Sources */, 36E21ED124DC540400649DC8 /* SettingsDialog.swift in Sources */, 3684EB8B2508F6F00001139E /* SearchBarWithLabel.swift in Sources */, diff --git a/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings b/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings index 5b983e25..f03f56d0 100644 --- a/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings +++ b/ui/BankingiOSApp/BankingiOSApp/Base.lproj/Localizable.strings @@ -130,7 +130,8 @@ /* New action sheet */ -"Show transfer money dialog" = "Transfer money"; +"Show transfer money dialog" = "Money transfer"; +"Money transfer from scanning QR-Code" = "Money transfer from QR-Code"; /* TransferMoneyDialog */ diff --git a/ui/BankingiOSApp/BankingiOSApp/Info.plist b/ui/BankingiOSApp/BankingiOSApp/Info.plist index 80ba9484..c510e18d 100644 --- a/ui/BankingiOSApp/BankingiOSApp/Info.plist +++ b/ui/BankingiOSApp/BankingiOSApp/Info.plist @@ -2,6 +2,8 @@ + NSCameraUsageDescription + Used to scan QR codes to initiate money transfer CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift b/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift index a01aef80..b0be94a6 100644 --- a/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift +++ b/ui/BankingiOSApp/BankingiOSApp/SceneDelegate.swift @@ -62,7 +62,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { DependencyInjector.register(dependency: presenter) - window.rootViewController = UINavigationController(rootViewController: TabBarController()) + window.rootViewController = UINavigationController(rootViewController: TabBarController(presenter)) } } diff --git a/ui/BankingiOSApp/BankingiOSApp/TabBarController.swift b/ui/BankingiOSApp/BankingiOSApp/TabBarController.swift index 5f372655..5bcdae78 100644 --- a/ui/BankingiOSApp/BankingiOSApp/TabBarController.swift +++ b/ui/BankingiOSApp/BankingiOSApp/TabBarController.swift @@ -1,10 +1,25 @@ import SwiftUI +import AVFoundation +import BankingUiSwift class TabBarController : UITabBarController, UITabBarControllerDelegate { @ObservedObject var data: AppData = AppData() + private let presenter: BankingPresenterSwift + + + init(_ presenter: BankingPresenterSwift) { + self.presenter = presenter + + super.init(nibName: nil, bundle: Bundle.main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -88,15 +103,33 @@ class TabBarController : UITabBarController, UITabBarControllerDelegate { private func showNewOptionsActionSheet() { - let transferMoneyAction = UIAlertAction.default("Show transfer money dialog".localize()) { SceneDelegate.navigateToView(TransferMoneyDialog()) } + let moneyTransferFromQrCodeAction = UIAlertAction.default("Money transfer from scanning QR-Code".localize()) { self.scanQrCodeIfCameraAccessGranted() } + moneyTransferFromQrCodeAction.isEnabled = data.hasAccountsThatSupportTransferringMoney + + let transferMoneyAction = UIAlertAction.default("Show transfer money dialog".localize()) { self.presenter.showTransferMoneyDialog(preselectedValues: nil) } transferMoneyAction.isEnabled = data.hasAccountsThatSupportTransferringMoney ActionSheet( nil, + moneyTransferFromQrCodeAction, transferMoneyAction, UIAlertAction.default("Add account") { SceneDelegate.navigateToView(AddAccountDialog()) }, UIAlertAction.cancel() ).show(self.tabBar, self.tabBar.bounds.midX, 0) } + private func scanQrCodeIfCameraAccessGranted() { + AVCaptureDevice.requestAccess(for: .video) { granted in + if granted { + DispatchQueue.main.async { // completionHandler is called on an arbitrary dispatch queue + SceneDelegate.navigateToViewController(ScanQrCodeViewController() { decodedQrCode in + if let decodedQrCode = decodedQrCode { + self.presenter.showTransferMoneyDialogWithDataFromQrCode(decodedQrCode: decodedQrCode) + } + }) + } + } + } + } + } diff --git a/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings b/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings index 1f5bf517..b7fa9a0a 100644 --- a/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings +++ b/ui/BankingiOSApp/BankingiOSApp/de.lproj/Localizable.strings @@ -132,6 +132,7 @@ /* New action sheet */ "Show transfer money dialog" = "Überweisung"; +"Money transfer from scanning QR-Code" = "Überweisung aus QR-Code"; /* TransferMoneyDialog */ diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ScanQrCodeViewController.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ScanQrCodeViewController.swift new file mode 100644 index 00000000..eb6a8a4c --- /dev/null +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/ScanQrCodeViewController.swift @@ -0,0 +1,131 @@ +import AVFoundation +import UIKit + + +/** + Copied from Hacking with Swift: https://www.hackingwithswift.com/example-code/media/how-to-scan-a-qr-code + */ +class ScanQrCodeViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { + + private let scanResult: (String?) -> Void + + private var captureSession: AVCaptureSession! + + private var previewLayer: AVCaptureVideoPreviewLayer! + + + init(_ scanResult: @escaping (String?) -> Void) { + self.scanResult = scanResult + + super.init(nibName: nil, bundle: Bundle.main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.black + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return } + let videoInput: AVCaptureDeviceInput + + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + return + } + + if (captureSession.canAddInput(videoInput)) { + captureSession.addInput(videoInput) + } else { + failed() + return + } + + let metadataOutput = AVCaptureMetadataOutput() + + if (captureSession.canAddOutput(metadataOutput)) { + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] + } else { + failed() + return + } + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + + captureSession.startRunning() + } + + func failed() { + self.closeDialog() + + let ac = UIAlertController(title: "Scanning not supported", message: "Your device does not support scanning a code from an item. Please use a device with a camera.", preferredStyle: .alert) + ac.addAction(UIAlertAction(title: "OK", style: .default)) + present(ac, animated: true) + captureSession = nil + + scanResult(nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if (captureSession?.isRunning == false) { + captureSession.startRunning() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if (captureSession?.isRunning == true) { + captureSession.stopRunning() + } + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + captureSession.stopRunning() + var didCloseDialog = false + + if let metadataObject = metadataObjects.first { + if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject { + if let decodedQrCode = readableObject.stringValue { + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + + didCloseDialog = true + SceneDelegate.dismissCurrentView() + + scanResult(decodedQrCode) + } + } + } + + if didCloseDialog == false { // for all other cases where QR code could not successfully be read + closeDialog() + } + } + + private func closeDialog() { + SceneDelegate.dismissCurrentView() + } + + override var prefersStatusBarHidden: Bool { + return true + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + +} diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift index ebad1402..3066e9c8 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift @@ -94,7 +94,7 @@ struct AccountTransactionListItem: View { } private func navigateToTransferMoneyDialog(_ preselectedValues: TransferMoneyData) { - SceneDelegate.navigateToView(TransferMoneyDialog(preselectedValues: preselectedValues)) + SceneDelegate.navigateToView(TransferMoneyDialog(preselectedValues)) } }