From c98e33bae67f254c075358009fb3b72d98cbd9be Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Mon, 22 Jul 2024 14:40:51 +0200 Subject: [PATCH] added local package --- .gitignore | 2 - example/wallet/.gitignore | 4 +- .../qr-bar-code-scanner-dialog/.gitignore | 57 +++ .../qr-bar-code-scanner-dialog/LICENSE | 201 +++++++++ .../analysis_options.yaml | 4 + .../android/.gitignore | 9 + .../android/build.gradle | 47 +++ .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../android/gradlew | 240 +++++++++++ .../android/gradlew.bat | 91 +++++ .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../QrBarCodeScannerDialogPlugin.kt | 35 ++ .../qr-bar-code-scanner-dialog/ios/.gitignore | 38 ++ .../ios/Assets/.gitkeep | 0 .../Classes/QrBarCodeScannerDialogPlugin.h | 4 + .../Classes/QrBarCodeScannerDialogPlugin.m | 15 + .../SwiftQrBarCodeScannerDialogPlugin.swift | 14 + .../ios/qr_bar_code_scanner_dialog.podspec | 23 ++ .../lib/qr_bar_code_scanner_dialog.dart | 15 + ...ar_code_scanner_dialog_method_channel.dart | 136 ++++++ ...ode_scanner_dialog_platform_interface.dart | 36 ++ .../lib/qr_bar_code_scanner_dialog_web.dart | 161 ++++++++ .../packages/qr_code_scanner/.gitignore | 116 ++++++ .../packages/qr_code_scanner/LICENSE | 9 + .../qr_code_scanner/analysis_options.yaml | 1 + .../qr_code_scanner/android/.gitignore | 67 +++ .../qr_code_scanner/android/build.gradle | 64 +++ .../qr_code_scanner/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../qr_code_scanner/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 4 + .../flutterqr/CustomFramingRectBarcodeView.kt | 45 ++ .../qr/flutterqr/FlutterQrPlugin.kt | 47 +++ .../net/touchcapture/qr/flutterqr/QRView.kt | 386 ++++++++++++++++++ .../qr/flutterqr/QRViewFactory.kt | 30 ++ .../flutterqr/QrActivityLifecycleCallbacks.kt | 41 ++ .../net/touchcapture/qr/flutterqr/QrShared.kt | 15 + .../packages/qr_code_scanner/ios/.gitignore | 78 ++++ .../qr_code_scanner/ios/Assets/.gitkeep | 0 .../ios/Classes/FlutterQrPlugin.h | 4 + .../ios/Classes/FlutterQrPlugin.m | 13 + .../qr_code_scanner/ios/Classes/QRView.swift | 304 ++++++++++++++ .../ios/Classes/QRViewFactory.swift | 27 ++ .../ios/Classes/SwiftFlutterQrPlugin.swift | 22 + .../ios/qr_code_scanner.podspec | 23 ++ .../qr_code_scanner/lib/qr_code_scanner.dart | 7 + .../lib/src/lifecycle_event_handler.dart | 25 ++ .../lib/src/qr_code_scanner.dart | 373 +++++++++++++++++ .../lib/src/qr_scanner_overlay_shape.dart | 183 +++++++++ .../lib/src/types/barcode.dart | 16 + .../lib/src/types/barcode_format.dart | 148 +++++++ .../qr_code_scanner/lib/src/types/camera.dart | 10 + .../lib/src/types/camera_exception.dart | 14 + .../lib/src/types/features.dart | 13 + .../lib/src/web/flutter_qr_stub.dart | 6 + .../lib/src/web/flutter_qr_web.dart | 334 +++++++++++++++ .../qr_code_scanner/lib/src/web/jsqr.dart | 12 + .../qr_code_scanner/lib/src/web/media.dart | 36 ++ .../packages/qr_code_scanner/pubspec.yaml | 31 ++ .../qr-bar-code-scanner-dialog/pubspec.yaml | 80 ++++ ...de_scanner_dialog_method_channel_test.dart | 25 ++ .../test/qr_bar_code_scanner_dialog_test.dart | 51 +++ 64 files changed, 3806 insertions(+), 5 deletions(-) create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/.gitignore create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/LICENSE create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/analysis_options.yaml create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/.gitignore create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/build.gradle create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 example/wallet/packages/qr-bar-code-scanner-dialog/android/gradlew create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/gradlew.bat create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/settings.gradle create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/AndroidManifest.xml create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/kotlin/com/northladder/qr_bar_code_scanner_dialog/QrBarCodeScannerDialogPlugin.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/ios/.gitignore create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/ios/Assets/.gitkeep create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.h create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.m create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/SwiftQrBarCodeScannerDialogPlugin.swift create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/ios/qr_bar_code_scanner_dialog.podspec create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_method_channel.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_platform_interface.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_web.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/.gitignore create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/LICENSE create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/analysis_options.yaml create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/.gitignore create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/build.gradle create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle.properties create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/settings.gradle create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/AndroidManifest.xml create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/CustomFramingRectBarcodeView.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/FlutterQrPlugin.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrActivityLifecycleCallbacks.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrShared.kt create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/.gitignore create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Assets/.gitkeep create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.h create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.m create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRView.swift create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRViewFactory.swift create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/SwiftFlutterQrPlugin.swift create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/qr_code_scanner.podspec create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/qr_code_scanner.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/lifecycle_event_handler.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_code_scanner.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_scanner_overlay_shape.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode_format.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera_exception.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/features.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_stub.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_web.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/jsqr.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/media.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/pubspec.yaml create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/pubspec.yaml create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_method_channel_test.dart create mode 100644 example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_test.dart diff --git a/.gitignore b/.gitignore index a5675c3c..c68c1976 100644 --- a/.gitignore +++ b/.gitignore @@ -55,5 +55,3 @@ app.*.map.json # Run scripts *.sh *.env.secret - -example/wallet/packages/ \ No newline at end of file diff --git a/example/wallet/.gitignore b/example/wallet/.gitignore index 0ad2af4f..0bf5b9a8 100644 --- a/example/wallet/.gitignore +++ b/example/wallet/.gitignore @@ -44,6 +44,4 @@ app.*.map.json /android/app/release # Run scripts -*.sh - -packages/ \ No newline at end of file +*.sh \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/.gitignore b/example/wallet/packages/qr-bar-code-scanner-dialog/.gitignore new file mode 100644 index 00000000..c68c1976 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/.gitignore @@ -0,0 +1,57 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +pubspec.lock +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# fvm +.fvm/ + +# vscode +.vscode/ + +# Run scripts +*.sh +*.env.secret diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/LICENSE b/example/wallet/packages/qr-bar-code-scanner-dialog/LICENSE new file mode 100644 index 00000000..989e2c59 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/analysis_options.yaml b/example/wallet/packages/qr-bar-code-scanner-dialog/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/.gitignore b/example/wallet/packages/qr-bar-code-scanner-dialog/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/build.gradle b/example/wallet/packages/qr-bar-code-scanner-dialog/android/build.gradle new file mode 100644 index 00000000..c8261124 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/android/build.gradle @@ -0,0 +1,47 @@ +group 'com.northladder.qr_bar_code_scanner_dialog' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace "com.northladder.qr_bar_code_scanner_dialog" + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/gradle/wrapper/gradle-wrapper.jar b/example/wallet/packages/qr-bar-code-scanner-dialog/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/gradlew.bat b/example/wallet/packages/qr-bar-code-scanner-dialog/android/gradlew.bat new file mode 100644 index 00000000..53a6b238 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/android/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/settings.gradle b/example/wallet/packages/qr-bar-code-scanner-dialog/android/settings.gradle new file mode 100644 index 00000000..3ccf202c --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'qr_bar_code_scanner_dialog' diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/AndroidManifest.xml b/example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1bdf510d --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/kotlin/com/northladder/qr_bar_code_scanner_dialog/QrBarCodeScannerDialogPlugin.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/kotlin/com/northladder/qr_bar_code_scanner_dialog/QrBarCodeScannerDialogPlugin.kt new file mode 100644 index 00000000..756b4e74 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/android/src/main/kotlin/com/northladder/qr_bar_code_scanner_dialog/QrBarCodeScannerDialogPlugin.kt @@ -0,0 +1,35 @@ +package com.northladder.qr_bar_code_scanner_dialog + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** QrBarCodeScannerDialogPlugin */ +class QrBarCodeScannerDialogPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "qr_bar_code_scanner_dialog") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/ios/.gitignore b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/.gitignore new file mode 100644 index 00000000..0c885071 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Assets/.gitkeep b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.h b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.h new file mode 100644 index 00000000..92177c5e --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface QrBarCodeScannerDialogPlugin : NSObject +@end diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.m b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.m new file mode 100644 index 00000000..6a44d71e --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/QrBarCodeScannerDialogPlugin.m @@ -0,0 +1,15 @@ +#import "QrBarCodeScannerDialogPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "qr_bar_code_scanner_dialog-Swift.h" +#endif + +@implementation QrBarCodeScannerDialogPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftQrBarCodeScannerDialogPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/SwiftQrBarCodeScannerDialogPlugin.swift b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/SwiftQrBarCodeScannerDialogPlugin.swift new file mode 100644 index 00000000..5e8f75dc --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/Classes/SwiftQrBarCodeScannerDialogPlugin.swift @@ -0,0 +1,14 @@ +import Flutter +import UIKit + +public class SwiftQrBarCodeScannerDialogPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "qr_bar_code_scanner_dialog", binaryMessenger: registrar.messenger()) + let instance = SwiftQrBarCodeScannerDialogPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/ios/qr_bar_code_scanner_dialog.podspec b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/qr_bar_code_scanner_dialog.podspec new file mode 100644 index 00000000..bd15a309 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/ios/qr_bar_code_scanner_dialog.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint qr_bar_code_scanner_dialog.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'qr_bar_code_scanner_dialog' + s.version = '0.0.1' + s.summary = 'Plugin to scan Bar/QR code easly with Flutter.' + s.description = <<-DESC +Plugin to scan Bar/QR code easly with Flutter. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog.dart new file mode 100644 index 00000000..779b9ebc --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog.dart @@ -0,0 +1,15 @@ +import 'qr_bar_code_scanner_dialog_platform_interface.dart'; + +import 'package:flutter/widgets.dart'; + +class QrBarCodeScannerDialog { + Future getPlatformVersion() { + return QrBarCodeScannerDialogPlatform.instance.getPlatformVersion(); + } + + void getScannedQrBarCode( + {BuildContext? context, required Function(String?) onCode}) { + QrBarCodeScannerDialogPlatform.instance + .scanBarOrQrCode(context: context, onScanSuccess: onCode); + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_method_channel.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_method_channel.dart new file mode 100644 index 00000000..3c28a02f --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_method_channel.dart @@ -0,0 +1,136 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; + +import 'qr_bar_code_scanner_dialog_platform_interface.dart'; + +/// An implementation of [QrBarCodeScannerDialogPlatform] that uses method channels. +class MethodChannelQrBarCodeScannerDialog + extends QrBarCodeScannerDialogPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('qr_bar_code_scanner_dialog'); + + @override + Future getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } + + @override + void scanBarOrQrCode( + {BuildContext? context, required Function(String? code) onScanSuccess}) { + /// context is required to show alert in non-web platforms + assert(context != null); + + showDialog( + context: context!, + builder: (context) => Container( + alignment: Alignment.center, + child: Container( + height: 400, + width: 600, + margin: const EdgeInsets.all(20), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: ScannerWidget(onScanSuccess: (code) { + if (code != null) { + Navigator.pop(context); + onScanSuccess(code); + } + }), + ), + )); + } +} + +class ScannerWidget extends StatefulWidget { + final void Function(String? code) onScanSuccess; + + const ScannerWidget({super.key, required this.onScanSuccess}); + + @override + createState() => _ScannerWidgetState(); +} + +class _ScannerWidgetState extends State { + QRViewController? controller; + GlobalKey qrKey = GlobalKey(debugLabel: 'scanner'); + + bool isScanned = false; + + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller?.pauseCamera(); + } else if (Platform.isIOS) { + controller?.resumeCamera(); + } + } + + @override + void dispose() { + /// dispose the controller + controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _buildQrView(context), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("Stop scanning"), + ), + ], + ); + } + + Widget _buildQrView(BuildContext context) { + double smallestDimension = min( + MediaQuery.of(context).size.width, MediaQuery.of(context).size.height); + + smallestDimension = min(smallestDimension, 550); + + return QRView( + key: qrKey, + onQRViewCreated: (controller) { + _onQRViewCreated(controller); + }, + overlay: QrScannerOverlayShape( + borderColor: Colors.black, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: smallestDimension - 140), + ); + } + + void _onQRViewCreated(QRViewController controller) { + this.controller = controller; + controller.scannedDataStream.listen((Barcode scanData) async { + if (!isScanned) { + isScanned = true; + widget.onScanSuccess(scanData.code); + } + }); + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_platform_interface.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_platform_interface.dart new file mode 100644 index 00000000..e2b2c123 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_platform_interface.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'qr_bar_code_scanner_dialog_method_channel.dart'; + +abstract class QrBarCodeScannerDialogPlatform extends PlatformInterface { + /// Constructs a QrBarCodeScannerDialogPlatform. + QrBarCodeScannerDialogPlatform() : super(token: _token); + + static final Object _token = Object(); + + static QrBarCodeScannerDialogPlatform _instance = + MethodChannelQrBarCodeScannerDialog(); + + /// The default instance of [QrBarCodeScannerDialogPlatform] to use. + /// + /// Defaults to [MethodChannelQrBarCodeScannerDialog]. + static QrBarCodeScannerDialogPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [QrBarCodeScannerDialogPlatform] when + /// they register themselves. + static set instance(QrBarCodeScannerDialogPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + void scanBarOrQrCode( + {BuildContext? context, required Function(String?) onScanSuccess}) { + throw UnimplementedError('scanBarOrQrCodeWeb() has not been implemented.'); + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_web.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_web.dart new file mode 100644 index 00000000..5318dc91 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/lib/qr_bar_code_scanner_dialog_web.dart @@ -0,0 +1,161 @@ +// In order to *not* need this ignore, consider extracting the "web" version +// of your plugin as a separate package, instead of inlining it in the same +// package as the core of your plugin. +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html + show window, Element, ScriptElement, StyleElement, querySelector, Text; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'qr_bar_code_scanner_dialog_platform_interface.dart'; +import 'dart:js' as js; + +const String _kQrBarCodeScannerModelDomId = '__qr_bar_code_scanner_web-model'; + +/// A web implementation of the QrBarCodeScannerDialogPlatform of the QrBarCodeScannerDialog plugin. +class QrBarCodeScannerDialogWeb extends QrBarCodeScannerDialogPlatform { + /// Constructs a QrBarCodeScannerDialogWeb + QrBarCodeScannerDialogWeb() { + _ensureInitialized(_kQrBarCodeScannerModelDomId); + } + + static void registerWith(Registrar registrar) { + QrBarCodeScannerDialogPlatform.instance = QrBarCodeScannerDialogWeb(); + } + + /// Initializes a DOM container where we can host input elements. + html.Element _ensureInitialized(String id) { + var target = html.querySelector('#$id'); + if (target == null) { + final html.Element targetElement = html.Element.div() + ..id = id + ..className = "modal"; + + final html.Element content = html.Element.div() + ..className = "modal-content"; + + final html.Element div = html.Element.div() + ..setAttribute("style", "container"); + + final html.Element reader = html.Element.div() + ..id = "qr-reader" + ..setAttribute("width", "400px"); + div.children.add(reader); + + content.children.add(div); + targetElement.children.add(content); + + final body = html.querySelector('body')!; + + body.children.add(targetElement); + + final script = html.ScriptElement() + ..src = "https://unpkg.com/html5-qrcode"; + body.children.add(script); + + final head = html.querySelector('head')!; + final style = html.StyleElement(); + + final styleContent = html.Text(""" + + /* The Modal (background) */ + .modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + padding-top: 100px; /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ + } + + /* Modal Content */ + .modal-content { + margin: auto; + max-width: 600px; + border-radius: 10px; + } + + #qr-reader { + position: relative; + background: white; + margin: 25px; + border-radius: 10px; + border: none; + } + + #qr-reader__filescan_input, + #qr-reader__camera_permission_button { + background: #3b99e8; + border: none; + padding: 8px; + border-radius: 5px; + color: white; + cursor:pointer; + margin-bottom: 10px; + } + + """); + + final codeScript = html.ScriptElement(); + final scriptText = html.Text(r""" + + var html5QrcodeScanner; + + // Get the modal + var modal = document.getElementById("__qr_bar_code_scanner_web-model"); + + // When the user clicks anywhere outside of the modal, close it + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + if(html5QrcodeScanner!=null) + html5QrcodeScanner.clear(); + } + } + + async function scanCode(message) { + + html5QrcodeScanner = new Html5QrcodeScanner("qr-reader", { fps: 20, qrbox: 250 }); + + modal.style.display = "block"; + html5QrcodeScanner.render((decodedText, decodedResult) => { + console.log(`Code scanned = ${decodedText}`, decodedResult); + message(`Code scanned = ${decodedText}`); + html5QrcodeScanner.clear(); + modal.style.display = "none"; + }); + + + } + + """); + codeScript.nodes.add(scriptText); + + style.nodes.add(styleContent); + head.children.add(style); + head.children.add(codeScript); + + target = targetElement; + } + return target; + } + + /// Returns a [String] containing the version of the platform. + @override + Future getPlatformVersion() async { + final version = html.window.navigator.userAgent; + return version; + } + + @override + void scanBarOrQrCode( + {BuildContext? context, required Function(String?) onScanSuccess}) { + js.context.callMethod("scanCode", [onScanSuccess]); + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/.gitignore b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/.gitignore new file mode 100644 index 00000000..aef8b20a --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/.gitignore @@ -0,0 +1,116 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.last_build_id + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/ + +# Flutter repo-specific +/bin/cache/ +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/.symlinks/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/Flutter-Debug.xcconfig +**/macos/Flutter/Flutter-Release.xcconfig +**/macos/Flutter/Flutter-Profile.xcconfig + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/LICENSE b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/LICENSE new file mode 100644 index 00000000..7279e1fc --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/LICENSE @@ -0,0 +1,9 @@ +Copyright 2018 Julius Canute + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/analysis_options.yaml b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/analysis_options.yaml new file mode 100644 index 00000000..a3be6b82 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/.gitignore b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/.gitignore new file mode 100644 index 00000000..d36c7964 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/.gitignore @@ -0,0 +1,67 @@ +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/build.gradle b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/build.gradle new file mode 100644 index 00000000..b660a44f --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/build.gradle @@ -0,0 +1,64 @@ +group 'net.touchcapture.qr.flutterqr' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.9.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace "net.touchcapture.qr.flutterqr" + compileSdk 33 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + // minSdkVersion is determined by Native View. + minSdkVersion 20 + targetSdkVersion 33 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true + } + + kotlinOptions { + jvmTarget = '11' + } + + compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + // Sets Java compatibility to Java 11 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + if (project.android.hasProperty('namespace')) { + namespace 'net.touchcapture.qr.flutterqr' + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.zxing:core:3.5.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle.properties b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle.properties new file mode 100644 index 00000000..08f2b5f9 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableJetifier=true +android.useAndroidX=true diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle/wrapper/gradle-wrapper.properties b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df9c58f3 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jul 22 12:15:07 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/settings.gradle b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/settings.gradle new file mode 100644 index 00000000..16c4243a --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_qr' diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/AndroidManifest.xml b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..28f2e86f --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/CustomFramingRectBarcodeView.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/CustomFramingRectBarcodeView.kt new file mode 100644 index 00000000..3ad74318 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/CustomFramingRectBarcodeView.kt @@ -0,0 +1,45 @@ +package net.touchcapture.qr.flutterqr + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import com.journeyapps.barcodescanner.BarcodeView +import com.journeyapps.barcodescanner.Size + +class CustomFramingRectBarcodeView : BarcodeView { + private var bottomOffset = BOTTOM_OFFSET_NOT_SET_VALUE + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + override fun calculateFramingRect(container: Rect, surface: Rect): Rect { + val containerArea = Rect(container) + val intersects = + containerArea.intersect(surface) //adjusts the containerArea (code from super.calculateFramingRect) + val scanAreaRect = super.calculateFramingRect(container, surface) + if (bottomOffset != BOTTOM_OFFSET_NOT_SET_VALUE) { //if the setFramingRect function was called, then we shift the scan area by Y + val scanAreaRectWithOffset = Rect(scanAreaRect) + scanAreaRectWithOffset.bottom -= bottomOffset + scanAreaRectWithOffset.top -= bottomOffset + val belongsToContainer = scanAreaRectWithOffset.intersect(containerArea) + if (belongsToContainer) { + return scanAreaRectWithOffset + } + } + return scanAreaRect + } + + fun setFramingRect(rectWidth: Int, rectHeight: Int, bottomOffset: Int) { + this.bottomOffset = bottomOffset + framingRectSize = Size(rectWidth, rectHeight) + } + + companion object { + private const val BOTTOM_OFFSET_NOT_SET_VALUE = -1 + } +} \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/FlutterQrPlugin.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/FlutterQrPlugin.kt new file mode 100644 index 00000000..97977a40 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/FlutterQrPlugin.kt @@ -0,0 +1,47 @@ +package net.touchcapture.qr.flutterqr + +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding + +class FlutterQrPlugin : FlutterPlugin, ActivityAware { + + /** Plugin registration embedding v2 */ + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + flutterPluginBinding.platformViewRegistry + .registerViewFactory( + VIEW_TYPE_ID, + QRViewFactory(flutterPluginBinding.binaryMessenger) + ) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + // Leave empty + // Nullifying QrShared.activity and QrShared.binding here will cause errors if plugin is detached by another plugin + } + + override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { + QrShared.activity = activityPluginBinding.activity + QrShared.binding = activityPluginBinding + } + + override fun onDetachedFromActivityForConfigChanges() { + QrShared.activity = null + QrShared.binding = null + } + + override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) { + QrShared.activity = activityPluginBinding.activity + QrShared.binding = activityPluginBinding + } + + override fun onDetachedFromActivity() { + QrShared.activity = null + QrShared.binding = null + } + + companion object { + private const val VIEW_TYPE_ID = "net.touchcapture.qr.flutterqr/qrview" + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt new file mode 100644 index 00000000..4589f2f2 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt @@ -0,0 +1,386 @@ +package net.touchcapture.qr.flutterqr + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.view.View +import androidx.core.content.ContextCompat +import com.google.zxing.BarcodeFormat +import com.google.zxing.ResultPoint +import com.journeyapps.barcodescanner.BarcodeCallback +import com.journeyapps.barcodescanner.BarcodeResult +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry +import io.flutter.plugin.platform.PlatformView + +class QRView( + private val context: Context, + messenger: BinaryMessenger, + private val id: Int, + private val params: HashMap +) : PlatformView, MethodChannel.MethodCallHandler, PluginRegistry.RequestPermissionsResultListener { + + private val cameraRequestCode = QrShared.CAMERA_REQUEST_ID + this.id + + private val channel: MethodChannel = MethodChannel( + messenger, "net.touchcapture.qr.flutterqr/qrview_$id" + ) + private val cameraFacingBack = 0 + private val cameraFacingFront = 1 + + private var isRequestingPermission = false + private var isTorchOn = false + private var isPaused = false + private var barcodeView: CustomFramingRectBarcodeView? = null + private var unRegisterLifecycleCallback: UnRegisterLifecycleCallback? = null + + init { + QrShared.binding?.addRequestPermissionsResultListener(this) + + channel.setMethodCallHandler(this) + + unRegisterLifecycleCallback = QrShared.activity?.registerLifecycleCallbacks( + onPause = { + if (!isPaused && hasCameraPermission) barcodeView?.pause() + + }, + onResume = { + if (!hasCameraPermission && !isRequestingPermission) checkAndRequestPermission() + else if (!isPaused && hasCameraPermission) barcodeView?.resume() + } + ) + } + + override fun dispose() { + unRegisterLifecycleCallback?.invoke() + + QrShared.binding?.removeRequestPermissionsResultListener(this) + + barcodeView?.pause() + barcodeView = null + } + + override fun getView(): View = initBarCodeView() + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + @Suppress("UNCHECKED_CAST") + when (call.method) { + "startScan" -> startScan(call.arguments as? List, result) + + "stopScan" -> stopScan() + + "flipCamera" -> flipCamera(result) + + "toggleFlash" -> toggleFlash(result) + + "pauseCamera" -> pauseCamera(result) + + // Stopping camera is the same as pausing camera + "stopCamera" -> pauseCamera(result) + + "resumeCamera" -> resumeCamera(result) + + "requestPermissions" -> checkAndRequestPermission() + + "getCameraInfo" -> getCameraInfo(result) + + "getFlashInfo" -> getFlashInfo(result) + + "getSystemFeatures" -> getSystemFeatures(result) + + "changeScanArea" -> changeScanArea( + dpScanAreaWidth = requireNotNull(call.argument("scanAreaWidth")), + dpScanAreaHeight = requireNotNull(call.argument("scanAreaHeight")), + cutOutBottomOffset = requireNotNull(call.argument("cutOutBottomOffset")), + result = result, + ) + + "invertScan" -> setInvertScan( + isInvert = call.argument("isInvertScan") ?: false, + ) + + else -> result.notImplemented() + } + } + + private fun initBarCodeView(): CustomFramingRectBarcodeView { + var barcodeView = barcodeView + + if (barcodeView == null) { + barcodeView = CustomFramingRectBarcodeView(QrShared.activity).also { + this.barcodeView = it + } + + barcodeView.decoderFactory = DefaultDecoderFactory(null, null, null, 2) + + if (params[PARAMS_CAMERA_FACING] as Int == 1) { + barcodeView.cameraSettings?.requestedCameraId = cameraFacingFront + } + } else if (!isPaused) { + barcodeView.resume() + } + + return barcodeView + } + + // region Camera Info + + private fun getCameraInfo(result: MethodChannel.Result) { + val barcodeView = barcodeView ?: return barCodeViewNotSet(result) + + result.success(barcodeView.cameraSettings.requestedCameraId) + } + + private fun getFlashInfo(result: MethodChannel.Result) { + if (barcodeView == null) return barCodeViewNotSet(result) + + result.success(isTorchOn) + } + + private fun hasFlash(): Boolean { + return hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH) + } + + @SuppressLint("UnsupportedChromeOsCameraSystemFeature") + private fun hasBackCamera(): Boolean { + return hasSystemFeature(PackageManager.FEATURE_CAMERA) + } + + private fun hasFrontCamera(): Boolean { + return hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT) + } + + private fun hasSystemFeature(feature: String): Boolean = + context.packageManager.hasSystemFeature(feature) + + private fun getSystemFeatures(result: MethodChannel.Result) { + try { + result.success( + mapOf( + "hasFrontCamera" to hasFrontCamera(), + "hasBackCamera" to hasBackCamera(), + "hasFlash" to hasFlash(), + "activeCamera" to barcodeView?.cameraSettings?.requestedCameraId + ) + ) + } catch (e: Exception) { + result.error("", e.message, null) + } + } + + // endregion + + // region Camera Controls + + private fun flipCamera(result: MethodChannel.Result) { + val barcodeView = barcodeView ?: return barCodeViewNotSet(result) + + barcodeView.pause() + + val settings = barcodeView.cameraSettings + if (settings.requestedCameraId == cameraFacingFront) { + settings.requestedCameraId = cameraFacingBack + } else settings.requestedCameraId = cameraFacingFront + + barcodeView.resume() + + result.success(settings.requestedCameraId) + } + + private fun toggleFlash(result: MethodChannel.Result) { + val barcodeView = barcodeView ?: return barCodeViewNotSet(result) + + if (hasFlash()) { + barcodeView.setTorch(!isTorchOn) + isTorchOn = !isTorchOn + result.success(isTorchOn) + } else { + result.error(ERROR_CODE_NOT_SET, ERROR_MESSAGE_FLASH_NOT_FOUND, null) + } + } + + private fun pauseCamera(result: MethodChannel.Result) { + val barcodeView = barcodeView ?: return barCodeViewNotSet(result) + + if (barcodeView.isPreviewActive) { + isPaused = true + barcodeView.pause() + } + + result.success(true) + } + + private fun resumeCamera(result: MethodChannel.Result) { + val barcodeView = barcodeView ?: return barCodeViewNotSet(result) + + if (!barcodeView.isPreviewActive) { + isPaused = false + barcodeView.resume() + } + + result.success(true) + } + + private fun startScan(arguments: List?, result: MethodChannel.Result) { + checkAndRequestPermission() + + val allowedBarcodeTypes = getAllowedBarcodeTypes(arguments, result) + + if (arguments == null) { + barcodeView?.decoderFactory = DefaultDecoderFactory(null, null, null, 2) + } else { + barcodeView?.decoderFactory = DefaultDecoderFactory(allowedBarcodeTypes, null, null, 2) + } + + barcodeView?.decodeContinuous( + object : BarcodeCallback { + override fun barcodeResult(result: BarcodeResult) { + if (allowedBarcodeTypes.isEmpty() || allowedBarcodeTypes.contains(result.barcodeFormat)) { + val code = mapOf( + "code" to result.text, + "type" to result.barcodeFormat.name, + "rawBytes" to result.rawBytes + ) + + channel.invokeMethod(CHANNEL_METHOD_ON_RECOGNIZE_QR, code) + } + } + + override fun possibleResultPoints(resultPoints: List) = Unit + } + ) + } + + private fun stopScan() { + barcodeView?.stopDecoding() + } + + private fun setInvertScan(isInvert: Boolean) { + val barcodeView = barcodeView ?: return + with(barcodeView) { + pause() + cameraSettings.isScanInverted = isInvert + resume() + } + } + + private fun changeScanArea( + dpScanAreaWidth: Double, + dpScanAreaHeight: Double, + cutOutBottomOffset: Double, + result: MethodChannel.Result + ) { + setScanAreaSize(dpScanAreaWidth, dpScanAreaHeight, cutOutBottomOffset) + + result.success(true) + } + + private fun setScanAreaSize( + dpScanAreaWidth: Double, + dpScanAreaHeight: Double, + dpCutOutBottomOffset: Double + ) { + barcodeView?.setFramingRect( + dpScanAreaWidth.convertDpToPixels(), + dpScanAreaHeight.convertDpToPixels(), + dpCutOutBottomOffset.convertDpToPixels(), + ) + } + + // endregion + + // region permissions + + private val hasCameraPermission: Boolean + get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + if (requestCode != cameraRequestCode) return false + isRequestingPermission = false + + val permissionGranted = + grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED + + channel.invokeMethod(CHANNEL_METHOD_ON_PERMISSION_SET, permissionGranted) + + return permissionGranted + } + + + + private fun checkAndRequestPermission() { + if (hasCameraPermission) { + channel.invokeMethod(CHANNEL_METHOD_ON_PERMISSION_SET, true) + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isRequestingPermission) { + QrShared.activity?.requestPermissions( + arrayOf(Manifest.permission.CAMERA), + cameraRequestCode + ) + } + } + + // endregion + + // region barcode common + + private fun getAllowedBarcodeTypes( + arguments: List?, + result: MethodChannel.Result + ): List { + return try { + arguments?.map { + BarcodeFormat.values()[it] + }.orEmpty() + } catch (e: Exception) { + result.error("", e.message, null) + + emptyList() + } + } + + private fun barCodeViewNotSet(result: MethodChannel.Result) { + result.error( + ERROR_CODE_NOT_SET, + ERROR_MESSAGE_NOT_SET, + null + ) + } + + // endregion + + // region helpers + + private fun Double.convertDpToPixels() = + (this * context.resources.displayMetrics.density).toInt() + + // endregion + + companion object { + private const val CHANNEL_METHOD_ON_PERMISSION_SET = "onPermissionSet" + private const val CHANNEL_METHOD_ON_RECOGNIZE_QR = "onRecognizeQR" + + private const val PARAMS_CAMERA_FACING = "cameraFacing" + + private const val ERROR_CODE_NOT_SET = "404" + + private const val ERROR_MESSAGE_NOT_SET = "No barcode view found" + private const val ERROR_MESSAGE_FLASH_NOT_FOUND = "This device doesn't support flash" + } +} + diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt new file mode 100644 index 00000000..013c7251 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt @@ -0,0 +1,30 @@ +package net.touchcapture.qr.flutterqr + +import android.content.Context +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + + +class QRViewFactory( + private val messenger: BinaryMessenger +) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + + override fun create( + context: Context?, + viewId: Int, + args: Any? + ): PlatformView { + + @Suppress("UNCHECKED_CAST") + val params = args as HashMap + + return QRView( + context = requireNotNull(context), + id = viewId, + messenger = messenger, + params = params + ) + } +} \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrActivityLifecycleCallbacks.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrActivityLifecycleCallbacks.kt new file mode 100644 index 00000000..e22f711d --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrActivityLifecycleCallbacks.kt @@ -0,0 +1,41 @@ +package net.touchcapture.qr.flutterqr + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +class UnRegisterLifecycleCallback( + private val application: Application, + private val callback: Application.ActivityLifecycleCallbacks, +) { + operator fun invoke() = application.unregisterActivityLifecycleCallbacks(callback) +} + +fun Activity.registerLifecycleCallbacks( + onPause: (() -> Unit)? = null, + onResume: (() -> Unit)? = null, +): UnRegisterLifecycleCallback { + val callback = object : Application.ActivityLifecycleCallbacks { + override fun onActivityPaused(p0: Activity) { + if (p0 == this@registerLifecycleCallbacks) onPause?.invoke() + } + + override fun onActivityResumed(p0: Activity) { + if (p0 == this@registerLifecycleCallbacks) onResume?.invoke() + } + + override fun onActivityStarted(p0: Activity) = Unit + + override fun onActivityDestroyed(p0: Activity) = Unit + + override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) = Unit + + override fun onActivityStopped(p0: Activity) = Unit + + override fun onActivityCreated(p0: Activity, p1: Bundle?) = Unit + } + + application.registerActivityLifecycleCallbacks(callback) + + return UnRegisterLifecycleCallback(application, callback) +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrShared.kt b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrShared.kt new file mode 100644 index 00000000..68146400 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QrShared.kt @@ -0,0 +1,15 @@ +package net.touchcapture.qr.flutterqr + +import android.annotation.SuppressLint +import android.app.Activity +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding + +@SuppressLint("StaticFieldLeak") +object QrShared { + const val CAMERA_REQUEST_ID = 513469796 + + var activity: Activity? = null + + var binding: ActivityPluginBinding? = null + +} \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/.gitignore b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/.gitignore new file mode 100644 index 00000000..fce96a30 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/.gitignore @@ -0,0 +1,78 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ \ No newline at end of file diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Assets/.gitkeep b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.h b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.h new file mode 100644 index 00000000..c3d077dd --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FlutterQrPlugin : NSObject +@end diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.m b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.m new file mode 100644 index 00000000..d52bedcf --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/FlutterQrPlugin.m @@ -0,0 +1,13 @@ +#import "FlutterQrPlugin.h" + +#if __has_include() +#import +#else +#import "qr_code_scanner-Swift.h" +#endif + +@implementation FlutterQrPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlutterQrPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRView.swift b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRView.swift new file mode 100644 index 00000000..1af75816 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRView.swift @@ -0,0 +1,304 @@ +// +// QRView.swift +// flutter_qr +// +// Created by Julius Canute on 21/12/18. +// + +import Foundation +import MTBBarcodeScanner + +public class QRView:NSObject,FlutterPlatformView { + @IBOutlet var previewView: UIView! + var scanner: MTBBarcodeScanner? + var registrar: FlutterPluginRegistrar + var channel: FlutterMethodChannel + var cameraFacing: MTBCamera + + // Codabar, maxicode, rss14 & rssexpanded not supported. Replaced with qr. + // UPCa uses ean13 object. + var QRCodeTypes = [ + 0: AVMetadataObject.ObjectType.aztec, + 1: AVMetadataObject.ObjectType.qr, + 2: AVMetadataObject.ObjectType.code39, + 3: AVMetadataObject.ObjectType.code93, + 4: AVMetadataObject.ObjectType.code128, + 5: AVMetadataObject.ObjectType.dataMatrix, + 6: AVMetadataObject.ObjectType.ean8, + 7: AVMetadataObject.ObjectType.ean13, + 8: AVMetadataObject.ObjectType.interleaved2of5, + 9: AVMetadataObject.ObjectType.qr, + 10: AVMetadataObject.ObjectType.pdf417, + 11: AVMetadataObject.ObjectType.qr, + 12: AVMetadataObject.ObjectType.qr, + 13: AVMetadataObject.ObjectType.qr, + 14: AVMetadataObject.ObjectType.ean13, + 15: AVMetadataObject.ObjectType.upce + ] + + public init(withFrame frame: CGRect, withRegistrar registrar: FlutterPluginRegistrar, withId id: Int64, params: Dictionary){ + self.registrar = registrar + previewView = UIView(frame: frame) + cameraFacing = MTBCamera.init(rawValue: UInt(Int(params["cameraFacing"] as! Double))) ?? MTBCamera.back + channel = FlutterMethodChannel(name: "net.touchcapture.qr.flutterqr/qrview_\(id)", binaryMessenger: registrar.messenger()) + } + + deinit { + scanner?.stopScanning() + } + + public func view() -> UIView { + channel.setMethodCallHandler({ + [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + switch(call.method){ + case "setDimensions": + let arguments = call.arguments as! Dictionary + self?.setDimensions(result, + width: arguments["width"] ?? 0, + height: arguments["height"] ?? 0, + scanAreaWidth: arguments["scanAreaWidth"] ?? 0, + scanAreaHeight: arguments["scanAreaHeight"] ?? 0, + scanAreaOffset: arguments["scanAreaOffset"] ?? 0) + case "startScan": + self?.startScan(call.arguments as! Array, result) + case "flipCamera": + self?.flipCamera(result) + case "toggleFlash": + self?.toggleFlash(result) + case "pauseCamera": + self?.pauseCamera(result) + case "stopCamera": + self?.stopCamera(result) + case "resumeCamera": + self?.resumeCamera(result) + case "getCameraInfo": + self?.getCameraInfo(result) + case "getFlashInfo": + self?.getFlashInfo(result) + case "getSystemFeatures": + self?.getSystemFeatures(result) + default: + result(FlutterMethodNotImplemented) + return + } + }) + return previewView + } + + func setDimensions(_ result: @escaping FlutterResult, width: Double, height: Double, scanAreaWidth: Double, scanAreaHeight: Double, scanAreaOffset: Double) { + // Then set the size of the preview area. + previewView.frame = CGRect(x: 0, y: 0, width: width, height: height) + + // Then set the size of the scan area. + let midX = self.view().bounds.midX + let midY = self.view().bounds.midY + + if let sc: MTBBarcodeScanner = scanner { + // Set the size of the preview if preview is already created. + if let previewLayer = sc.previewLayer { + previewLayer.frame = self.previewView.bounds + } + } else { + // Create new preview. + scanner = MTBBarcodeScanner(previewView: previewView) + } + + // Set scanArea if provided. + if (scanAreaWidth != 0 && scanAreaHeight != 0) { + scanner?.didStartScanningBlock = { + self.scanner?.scanRect = CGRect(x: Double(midX) - (scanAreaWidth / 2), y: Double(midY) - (scanAreaHeight / 2), width: scanAreaWidth, height: scanAreaHeight) + + // Set offset if provided. + if (scanAreaOffset != 0) { + let reversedOffset = -scanAreaOffset + self.scanner?.scanRect = (self.scanner?.scanRect.offsetBy(dx: 0, dy: CGFloat(reversedOffset)))! + + } + } + } + return result(width) + + } + + func startScan(_ arguments: Array, _ result: @escaping FlutterResult) { + // Check for allowed barcodes + var allowedBarcodeTypes: Array = [] + arguments.forEach { arg in + allowedBarcodeTypes.append( QRCodeTypes[arg]!) + } + MTBBarcodeScanner.requestCameraPermission(success: { [weak self] permissionGranted in + guard let self = self else { return } + + self.channel.invokeMethod("onPermissionSet", arguments: permissionGranted) + + if permissionGranted { + do { + try self.scanner?.startScanning(with: self.cameraFacing, resultBlock: { [weak self] codes in + if let codes = codes { + for code in codes { + var typeString: String; + switch(code.type) { + case AVMetadataObject.ObjectType.aztec: + typeString = "AZTEC" + case AVMetadataObject.ObjectType.code39: + typeString = "CODE_39" + case AVMetadataObject.ObjectType.code93: + typeString = "CODE_93" + case AVMetadataObject.ObjectType.code128: + typeString = "CODE_128" + case AVMetadataObject.ObjectType.dataMatrix: + typeString = "DATA_MATRIX" + case AVMetadataObject.ObjectType.ean8: + typeString = "EAN_8" + case AVMetadataObject.ObjectType.ean13: + typeString = "EAN_13" + case AVMetadataObject.ObjectType.itf14, + AVMetadataObject.ObjectType.interleaved2of5: + typeString = "ITF" + case AVMetadataObject.ObjectType.pdf417: + typeString = "PDF_417" + case AVMetadataObject.ObjectType.qr: + typeString = "QR_CODE" + case AVMetadataObject.ObjectType.upce: + typeString = "UPC_E" + default: + return + } + let bytes = { () -> Data? in + if #available(iOS 11.0, *) { + switch (code.descriptor) { + case let qrDescriptor as CIQRCodeDescriptor: + return qrDescriptor.errorCorrectedPayload + case let aztecDescriptor as CIAztecCodeDescriptor: + return aztecDescriptor.errorCorrectedPayload + case let pdf417Descriptor as CIPDF417CodeDescriptor: + return pdf417Descriptor.errorCorrectedPayload + case let dataMatrixDescriptor as CIDataMatrixCodeDescriptor: + return dataMatrixDescriptor.errorCorrectedPayload + default: + return nil + } + } else { + return nil + } + }() + let result = { () -> [String : Any]? in + guard let stringValue = code.stringValue else { + guard let safeBytes = bytes else { + return nil + } + return ["type": typeString, "rawBytes": safeBytes] + } + guard let safeBytes = bytes else { + return ["code": stringValue, "type": typeString] + } + return ["code": stringValue, "type": typeString, "rawBytes": safeBytes] + }() + guard result != nil else { continue } + if allowedBarcodeTypes.count == 0 || allowedBarcodeTypes.contains(code.type) { + self?.channel.invokeMethod("onRecognizeQR", arguments: result) + } + + } + } + + }) + } catch { + let scanError = FlutterError(code: "unknown-error", message: "Unable to start scanning", details: error) + result(scanError) + } + } + }) + } + + func stopCamera(_ result: @escaping FlutterResult) { + if let sc: MTBBarcodeScanner = self.scanner { + if sc.isScanning() { + sc.stopScanning() + } + } + } + + func getCameraInfo(_ result: @escaping FlutterResult) { + result(self.cameraFacing.rawValue) + } + + func flipCamera(_ result: @escaping FlutterResult) { + if let sc: MTBBarcodeScanner = self.scanner { + if sc.hasOppositeCamera() { + sc.flipCamera() + self.cameraFacing = sc.camera + } + return result(sc.camera.rawValue) + } + return result(FlutterError(code: "404", message: "No barcode scanner found", details: nil)) + } + + func getFlashInfo(_ result: @escaping FlutterResult) { + if let sc: MTBBarcodeScanner = self.scanner { + result(sc.torchMode.rawValue != 0) + } else { + let error = FlutterError(code: "cameraInformationError", message: "Could not get flash information", details: nil) + result(error) + } + } + + func toggleFlash(_ result: @escaping FlutterResult){ + if let sc: MTBBarcodeScanner = self.scanner { + if sc.hasTorch() { + sc.toggleTorch() + return result(sc.torchMode == MTBTorchMode(rawValue: 1)) + } + return result(FlutterError(code: "404", message: "This device doesn\'t support flash", details: nil)) + } + return result(FlutterError(code: "404", message: "No barcode scanner found", details: nil)) + } + + func pauseCamera(_ result: @escaping FlutterResult) { + if let sc: MTBBarcodeScanner = self.scanner { + if sc.isScanning() { + sc.freezeCapture() + } + return result(true) + } + return result(FlutterError(code: "404", message: "No barcode scanner found", details: nil)) + } + + func resumeCamera(_ result: @escaping FlutterResult) { + if let sc: MTBBarcodeScanner = self.scanner { + if !sc.isScanning() { + sc.unfreezeCapture() + } + return result(true) + } + return result(FlutterError(code: "404", message: "No barcode scanner found", details: nil)) + } + + func getSystemFeatures(_ result: @escaping FlutterResult) { + if let sc: MTBBarcodeScanner = scanner { + var hasBackCameraVar = false + var hasFrontCameraVar = false + let camera = sc.camera + + if(camera == MTBCamera(rawValue: 0)){ + hasBackCameraVar = true + if sc.hasOppositeCamera() { + hasFrontCameraVar = true + } + }else{ + hasFrontCameraVar = true + if sc.hasOppositeCamera() { + hasBackCameraVar = true + } + } + return result([ + "hasFrontCamera": hasFrontCameraVar, + "hasBackCamera": hasBackCameraVar, + "hasFlash": sc.hasTorch(), + "activeCamera": camera.rawValue + ]) + } + return result(FlutterError(code: "404", message: nil, details: nil)) + } + + } diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRViewFactory.swift b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRViewFactory.swift new file mode 100644 index 00000000..6f851f69 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/QRViewFactory.swift @@ -0,0 +1,27 @@ +// +// QRViewFactory.swift +// flutter_qr +// +// Created by Julius Canute on 21/12/18. +// + +import Foundation + +public class QRViewFactory: NSObject, FlutterPlatformViewFactory { + + var registrar: FlutterPluginRegistrar? + + public init(withRegistrar registrar: FlutterPluginRegistrar){ + super.init() + self.registrar = registrar + } + + public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { + let params = args as! Dictionary + return QRView(withFrame: frame, withRegistrar: registrar!,withId: viewId, params: params) + } + + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec(readerWriter: FlutterStandardReaderWriter()) + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/SwiftFlutterQrPlugin.swift b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/SwiftFlutterQrPlugin.swift new file mode 100644 index 00000000..f26aff62 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/Classes/SwiftFlutterQrPlugin.swift @@ -0,0 +1,22 @@ +import Flutter +import UIKit + +public class SwiftFlutterQrPlugin: NSObject, FlutterPlugin { + + var factory: QRViewFactory + public init(with registrar: FlutterPluginRegistrar) { + self.factory = QRViewFactory(withRegistrar: registrar) + registrar.register(factory, withId: "net.touchcapture.qr.flutterqr/qrview") + } + + public static func register(with registrar: FlutterPluginRegistrar) { + registrar.addApplicationDelegate(SwiftFlutterQrPlugin(with: registrar)) + } + + public func applicationDidEnterBackground(_ application: UIApplication) { + } + + public func applicationWillTerminate(_ application: UIApplication) { + } + +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/qr_code_scanner.podspec b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/qr_code_scanner.podspec new file mode 100644 index 00000000..f94e7cc1 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/ios/qr_code_scanner.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'qr_code_scanner' + s.version = '0.2.0' + s.summary = 'QR Code Scanner for flutter.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'https://github.com/juliuscanute/qr_code_scanner' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'juliuscanute[*]touchcapture.net' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.dependency 'MTBBarcodeScanner' + s.ios.deployment_target = '8.0' + s.swift_version = '4.0' +end + diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/qr_code_scanner.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/qr_code_scanner.dart new file mode 100644 index 00000000..42206139 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/qr_code_scanner.dart @@ -0,0 +1,7 @@ +export 'src/qr_code_scanner.dart'; +export 'src/qr_scanner_overlay_shape.dart'; +export 'src/types/barcode.dart'; +export 'src/types/barcode_format.dart'; +export 'src/types/camera.dart'; +export 'src/types/camera_exception.dart'; +export 'src/types/features.dart'; diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/lifecycle_event_handler.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/lifecycle_event_handler.dart new file mode 100644 index 00000000..ad38c607 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/lifecycle_event_handler.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +class LifecycleEventHandler extends WidgetsBindingObserver { + LifecycleEventHandler({ + required this.resumeCallBack, + }); + + late final AsyncCallback resumeCallBack; + + @override + Future didChangeAppLifecycleState(AppLifecycleState state) async { + switch (state) { + case AppLifecycleState.resumed: + await resumeCallBack(); + break; + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + // TODO: Handle this case. + break; + } + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_code_scanner.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_code_scanner.dart new file mode 100644 index 00000000..0a1deb3a --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_code_scanner.dart @@ -0,0 +1,373 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'lifecycle_event_handler.dart'; +import 'qr_scanner_overlay_shape.dart'; +import 'types/barcode.dart'; +import 'types/barcode_format.dart'; +import 'types/camera.dart'; +import 'types/camera_exception.dart'; +import 'types/features.dart'; +import 'web/flutter_qr_stub.dart' +// ignore: uri_does_not_exist + if (dart.library.html) 'web/flutter_qr_web.dart'; + +typedef QRViewCreatedCallback = void Function(QRViewController); +typedef PermissionSetCallback = void Function(QRViewController, bool); + +/// The [QRView] is the view where the camera +/// and the barcode scanner gets displayed. +class QRView extends StatefulWidget { + const QRView({ + required Key key, + required this.onQRViewCreated, + this.overlay, + this.overlayMargin = EdgeInsets.zero, + this.cameraFacing = CameraFacing.back, + this.onPermissionSet, + this.formatsAllowed = const [], + }) : super(key: key); + + /// [onQRViewCreated] gets called when the view is created + final QRViewCreatedCallback onQRViewCreated; + + /// Use [overlay] to provide an overlay for the view. + /// This can be used to create a certain scan area. + final QrScannerOverlayShape? overlay; + + /// Use [overlayMargin] to provide a margin to [overlay] + final EdgeInsetsGeometry overlayMargin; + + /// Set which camera to use on startup. + /// + /// [cameraFacing] can either be CameraFacing.front or CameraFacing.back. + /// Defaults to CameraFacing.back + final CameraFacing cameraFacing; + + /// Calls the provided [onPermissionSet] callback when the permission is set. + final PermissionSetCallback? onPermissionSet; + + /// Use [formatsAllowed] to specify which formats needs to be scanned. + final List formatsAllowed; + + @override + State createState() => _QRViewState(); +} + +class _QRViewState extends State { + late MethodChannel _channel; + late LifecycleEventHandler _observer; + + @override + void initState() { + super.initState(); + _observer = LifecycleEventHandler(resumeCallBack: updateDimensions); + WidgetsBinding.instance.addObserver(_observer); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: onNotification, + child: SizeChangedLayoutNotifier( + child: (widget.overlay != null) + ? _getPlatformQrViewWithOverlay() + : _getPlatformQrView(), + ), + ); + } + + @override + void dispose() { + super.dispose(); + WidgetsBinding.instance.removeObserver(_observer); + } + + Future updateDimensions() async { + await QRViewController.updateDimensions( + widget.key as GlobalKey>, _channel, + overlay: widget.overlay); + } + + bool onNotification(notification) { + updateDimensions(); + return false; + } + + Widget _getPlatformQrViewWithOverlay() { + return Stack( + children: [ + _getPlatformQrView(), + Padding( + padding: widget.overlayMargin, + child: Container( + decoration: ShapeDecoration( + shape: widget.overlay!, + ), + ), + ) + ], + ); + } + + Widget _getPlatformQrView() { + Widget _platformQrView; + if (kIsWeb) { + _platformQrView = createWebQrView( + onPlatformViewCreated: widget.onQRViewCreated, + onPermissionSet: widget.onPermissionSet, + cameraFacing: widget.cameraFacing, + ); + } else { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + _platformQrView = AndroidView( + viewType: 'net.touchcapture.qr.flutterqr/qrview', + onPlatformViewCreated: _onPlatformViewCreated, + creationParams: + _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(), + creationParamsCodec: const StandardMessageCodec(), + ); + break; + case TargetPlatform.iOS: + _platformQrView = UiKitView( + viewType: 'net.touchcapture.qr.flutterqr/qrview', + onPlatformViewCreated: _onPlatformViewCreated, + creationParams: + _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(), + creationParamsCodec: const StandardMessageCodec(), + ); + break; + default: + throw UnsupportedError( + "Trying to use the default qrview implementation for $defaultTargetPlatform but there isn't a default one"); + } + } + return _platformQrView; + } + + void _onPlatformViewCreated(int id) { + _channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id'); + + // Start scan after creation of the view + final controller = QRViewController._( + _channel, + widget.key as GlobalKey>?, + widget.onPermissionSet, + widget.cameraFacing) + .._startScan(widget.key as GlobalKey>, + widget.overlay, widget.formatsAllowed); + + // Initialize the controller for controlling the QRView + widget.onQRViewCreated(controller); + } +} + +class _QrCameraSettings { + _QrCameraSettings({ + this.cameraFacing = CameraFacing.unknown, + }); + + final CameraFacing cameraFacing; + + Map toMap() { + return { + 'cameraFacing': cameraFacing.index, + }; + } +} + +class QRViewController { + QRViewController._(MethodChannel channel, GlobalKey? qrKey, + PermissionSetCallback? onPermissionSet, CameraFacing cameraFacing) + : _channel = channel, + _cameraFacing = cameraFacing { + _channel.setMethodCallHandler((call) async { + switch (call.method) { + case 'onRecognizeQR': + if (call.arguments != null) { + final args = call.arguments as Map; + final code = args['code'] as String?; + final rawType = args['type'] as String; + // Raw bytes are only supported by Android. + final rawBytes = args['rawBytes'] as List?; + final format = BarcodeTypesExtension.fromString(rawType); + if (format != BarcodeFormat.unknown) { + final barcode = Barcode(code, format, rawBytes); + _scanUpdateController.sink.add(barcode); + } else { + throw Exception('Unexpected barcode type $rawType'); + } + } + break; + case 'onPermissionSet': + if (call.arguments != null && call.arguments is bool) { + _hasPermissions = call.arguments; + if (onPermissionSet != null) { + onPermissionSet(this, _hasPermissions); + } + } + break; + } + }); + } + + final MethodChannel _channel; + final CameraFacing _cameraFacing; + final StreamController _scanUpdateController = + StreamController(); + + Stream get scannedDataStream => _scanUpdateController.stream; + + bool _hasPermissions = false; + bool get hasPermissions => _hasPermissions; + + /// Starts the barcode scanner + Future _startScan(GlobalKey key, QrScannerOverlayShape? overlay, + List? barcodeFormats) async { + // We need to update the dimension before the scan is started. + try { + await QRViewController.updateDimensions(key, _channel, overlay: overlay); + return await _channel.invokeMethod( + 'startScan', barcodeFormats?.map((e) => e.asInt()).toList() ?? []); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets information about which camera is active. + Future getCameraInfo() async { + try { + var cameraFacing = await _channel.invokeMethod('getCameraInfo') as int; + if (cameraFacing == -1) return _cameraFacing; + return CameraFacing + .values[await _channel.invokeMethod('getCameraInfo') as int]; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Flips the camera between available modes + Future flipCamera() async { + try { + return CameraFacing + .values[await _channel.invokeMethod('flipCamera') as int]; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Get flashlight status + Future getFlashStatus() async { + try { + return await _channel.invokeMethod('getFlashInfo'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Toggles the flashlight between available modes + Future toggleFlash() async { + try { + await _channel.invokeMethod('toggleFlash') as bool?; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Pauses the camera and barcode scanning + Future pauseCamera() async { + try { + await _channel.invokeMethod('pauseCamera'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stops barcode scanning and the camera + Future stopCamera() async { + try { + await _channel.invokeMethod('stopCamera'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes barcode scanning + Future resumeCamera() async { + try { + await _channel.invokeMethod('resumeCamera'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Returns which features are available on device. + Future getSystemFeatures() async { + try { + var features = + await _channel.invokeMapMethod('getSystemFeatures'); + if (features != null) { + return SystemFeatures.fromJson(features); + } + throw CameraException('Error', 'Could not get system features'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stops the camera and disposes the barcode stream. + void dispose() { + if (defaultTargetPlatform == TargetPlatform.iOS) stopCamera(); + _scanUpdateController.close(); + } + + /// Updates the view dimensions for iOS. + static Future updateDimensions(GlobalKey key, MethodChannel channel, + {QrScannerOverlayShape? overlay}) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + // Add small delay to ensure the render box is loaded + await Future.delayed(const Duration(milliseconds: 300)); + if (key.currentContext == null) return false; + final renderBox = key.currentContext!.findRenderObject() as RenderBox; + try { + await channel.invokeMethod('setDimensions', { + 'width': renderBox.size.width, + 'height': renderBox.size.height, + 'scanAreaWidth': overlay?.cutOutWidth ?? 0, + 'scanAreaHeight': overlay?.cutOutHeight ?? 0, + 'scanAreaOffset': overlay?.cutOutBottomOffset ?? 0 + }); + return true; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } else if (defaultTargetPlatform == TargetPlatform.android) { + if (overlay == null) { + return false; + } + await channel.invokeMethod('changeScanArea', { + 'scanAreaWidth': overlay.cutOutWidth, + 'scanAreaHeight': overlay.cutOutHeight, + 'cutOutBottomOffset': overlay.cutOutBottomOffset + }); + return true; + } + return false; + } + + //Starts/Stops invert scanning. + Future scanInvert(bool isScanInvert) async { + if (defaultTargetPlatform == TargetPlatform.android) { + try { + await _channel + .invokeMethod('invertScan', {"isInvertScan": isScanInvert}); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_scanner_overlay_shape.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_scanner_overlay_shape.dart new file mode 100644 index 00000000..83fc5efc --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/qr_scanner_overlay_shape.dart @@ -0,0 +1,183 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class QrScannerOverlayShape extends ShapeBorder { + QrScannerOverlayShape({ + this.borderColor = Colors.red, + this.borderWidth = 3.0, + this.overlayColor = const Color.fromRGBO(0, 0, 0, 80), + this.borderRadius = 0, + this.borderLength = 40, + double? cutOutSize, + double? cutOutWidth, + double? cutOutHeight, + this.cutOutBottomOffset = 0, + }) : cutOutWidth = cutOutWidth ?? cutOutSize ?? 250, + cutOutHeight = cutOutHeight ?? cutOutSize ?? 250 { + assert( + borderLength <= + min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2, + "Border can't be larger than ${min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2}", + ); + assert( + (cutOutWidth == null && cutOutHeight == null) || + (cutOutSize == null && cutOutWidth != null && cutOutHeight != null), + 'Use only cutOutWidth and cutOutHeight or only cutOutSize'); + } + + final Color borderColor; + final double borderWidth; + final Color overlayColor; + final double borderRadius; + final double borderLength; + final double cutOutWidth; + final double cutOutHeight; + final double cutOutBottomOffset; + + @override + EdgeInsetsGeometry get dimensions => const EdgeInsets.all(10); + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) { + return Path() + ..fillType = PathFillType.evenOdd + ..addPath(getOuterPath(rect), Offset.zero); + } + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) { + Path _getLeftTopPath(Rect rect) { + return Path() + ..moveTo(rect.left, rect.bottom) + ..lineTo(rect.left, rect.top) + ..lineTo(rect.right, rect.top); + } + + return _getLeftTopPath(rect) + ..lineTo( + rect.right, + rect.bottom, + ) + ..lineTo( + rect.left, + rect.bottom, + ) + ..lineTo( + rect.left, + rect.top, + ); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { + final width = rect.width; + final borderWidthSize = width / 2; + final height = rect.height; + final borderOffset = borderWidth / 2; + final _borderLength = + borderLength > min(cutOutHeight, cutOutHeight) / 2 + borderWidth * 2 + ? borderWidthSize / 2 + : borderLength; + final _cutOutWidth = + cutOutWidth < width ? cutOutWidth : width - borderOffset; + final _cutOutHeight = + cutOutHeight < height ? cutOutHeight : height - borderOffset; + + final backgroundPaint = Paint() + ..color = overlayColor + ..style = PaintingStyle.fill; + + final borderPaint = Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth; + + final boxPaint = Paint() + ..color = borderColor + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + final cutOutRect = Rect.fromLTWH( + rect.left + width / 2 - _cutOutWidth / 2 + borderOffset, + -cutOutBottomOffset + + rect.top + + height / 2 - + _cutOutHeight / 2 + + borderOffset, + _cutOutWidth - borderOffset * 2, + _cutOutHeight - borderOffset * 2, + ); + + canvas + ..saveLayer( + rect, + backgroundPaint, + ) + ..drawRect( + rect, + backgroundPaint, + ) + // Draw top right corner + ..drawRRect( + RRect.fromLTRBAndCorners( + cutOutRect.right - _borderLength, + cutOutRect.top, + cutOutRect.right, + cutOutRect.top + _borderLength, + topRight: Radius.circular(borderRadius), + ), + borderPaint, + ) + // Draw top left corner + ..drawRRect( + RRect.fromLTRBAndCorners( + cutOutRect.left, + cutOutRect.top, + cutOutRect.left + _borderLength, + cutOutRect.top + _borderLength, + topLeft: Radius.circular(borderRadius), + ), + borderPaint, + ) + // Draw bottom right corner + ..drawRRect( + RRect.fromLTRBAndCorners( + cutOutRect.right - _borderLength, + cutOutRect.bottom - _borderLength, + cutOutRect.right, + cutOutRect.bottom, + bottomRight: Radius.circular(borderRadius), + ), + borderPaint, + ) + // Draw bottom left corner + ..drawRRect( + RRect.fromLTRBAndCorners( + cutOutRect.left, + cutOutRect.bottom - _borderLength, + cutOutRect.left + _borderLength, + cutOutRect.bottom, + bottomLeft: Radius.circular(borderRadius), + ), + borderPaint, + ) + ..drawRRect( + RRect.fromRectAndRadius( + cutOutRect, + Radius.circular(borderRadius), + ), + boxPaint, + ) + ..restore(); + } + + @override + ShapeBorder scale(double t) { + return QrScannerOverlayShape( + borderColor: borderColor, + borderWidth: borderWidth, + overlayColor: overlayColor, + ); + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode.dart new file mode 100644 index 00000000..c8c3105a --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode.dart @@ -0,0 +1,16 @@ +import 'barcode_format.dart'; + +/// The [Barcode] object holds information about the barcode or qr code. +/// +/// [code] is the string-content of the barcode. +/// [format] displays which type the code is. +/// Only for Android and iOS, [rawBytes] gives a list of bytes of the result. +class Barcode { + Barcode(this.code, this.format, this.rawBytes); + + final String? code; + final BarcodeFormat format; + + /// Raw bytes are only supported by Android and iOS. + final List? rawBytes; +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode_format.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode_format.dart new file mode 100644 index 00000000..49bcee2d --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/barcode_format.dart @@ -0,0 +1,148 @@ +enum BarcodeFormat { + /// Aztec 2D barcode format. + aztec, + + /// CODABAR 1D format. + /// Not supported in iOS + codabar, + + /// Code 39 1D format. + code39, + + /// Code 93 1D format. + code93, + + /// Code 128 1D format. + code128, + + /// Data Matrix 2D barcode format. + dataMatrix, + + /// EAN-8 1D format. + ean8, + + /// EAN-13 1D format. + ean13, + + /// ITF (Interleaved Two of Five) 1D format. + itf, + + /// MaxiCode 2D barcode format. + /// Not supported in iOS. + maxicode, + + /// PDF417 format. + pdf417, + + /// QR Code 2D barcode format. + qrcode, + + /// RSS 14 + /// Not supported in iOS. + rss14, + + /// RSS EXPANDED + /// Not supported in iOS. + rssExpanded, + + /// UPC-A 1D format. + /// Same as ean-13 on iOS. + upcA, + + /// UPC-E 1D format. + upcE, + + /// UPC/EAN extension format. Not a stand-alone format. + upcEanExtension, + + /// Unknown + unknown +} + +extension BarcodeTypesExtension on BarcodeFormat { + int asInt() { + return index; + } + + static BarcodeFormat fromString(String format) { + switch (format) { + case 'AZTEC': + return BarcodeFormat.aztec; + case 'CODABAR': + return BarcodeFormat.codabar; + case 'CODE_39': + return BarcodeFormat.code39; + case 'CODE_93': + return BarcodeFormat.code93; + case 'CODE_128': + return BarcodeFormat.code128; + case 'DATA_MATRIX': + return BarcodeFormat.dataMatrix; + case 'EAN_8': + return BarcodeFormat.ean8; + case 'EAN_13': + return BarcodeFormat.ean13; + case 'ITF': + return BarcodeFormat.itf; + case 'MAXICODE': + return BarcodeFormat.maxicode; + case 'PDF_417': + return BarcodeFormat.pdf417; + case 'QR_CODE': + return BarcodeFormat.qrcode; + case 'RSS14': + return BarcodeFormat.rss14; + case 'RSS_EXPANDED': + return BarcodeFormat.rssExpanded; + case 'UPC_A': + return BarcodeFormat.upcA; + case 'UPC_E': + return BarcodeFormat.upcE; + case 'UPC_EAN_EXTENSION': + return BarcodeFormat.upcEanExtension; + default: + return BarcodeFormat.unknown; + } + } + + String get formatName { + switch (this) { + case BarcodeFormat.aztec: + return 'AZTEC'; + case BarcodeFormat.codabar: + return 'CODABAR'; + case BarcodeFormat.code39: + return 'CODE_39'; + case BarcodeFormat.code93: + return 'CODE_93'; + case BarcodeFormat.code128: + return 'CODE_128'; + case BarcodeFormat.dataMatrix: + return 'DATA_MATRIX'; + case BarcodeFormat.ean8: + return 'EAN_8'; + case BarcodeFormat.ean13: + return 'EAN_13'; + case BarcodeFormat.itf: + return 'ITF'; + case BarcodeFormat.maxicode: + return 'MAXICODE'; + case BarcodeFormat.pdf417: + return 'PDF_417'; + case BarcodeFormat.qrcode: + return 'QR_CODE'; + case BarcodeFormat.rss14: + return 'RSS14'; + case BarcodeFormat.rssExpanded: + return 'RSS_EXPANDED'; + case BarcodeFormat.upcA: + return 'UPC_A'; + case BarcodeFormat.upcE: + return 'UPC_E'; + case BarcodeFormat.upcEanExtension: + return 'UPC_EAN_EXTENSION'; + default: + return 'UNKNOWN'; + } + } +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera.dart new file mode 100644 index 00000000..67fe8cb1 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera.dart @@ -0,0 +1,10 @@ +enum CameraFacing { + /// Shows back facing camera. + back, + + /// Shows front facing camera. + front, + + /// Unknown camera + unknown +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera_exception.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera_exception.dart new file mode 100644 index 00000000..0abb639d --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/camera_exception.dart @@ -0,0 +1,14 @@ +/// This is thrown when the plugin reports an error. +class CameraException implements Exception { + /// Creates a new camera exception with the given error code and description. + CameraException(this.code, this.description); + + /// Error code. + String code; + + /// Textual description of the error. + String? description; + + @override + String toString() => 'CameraException($code, $description)'; +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/features.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/features.dart new file mode 100644 index 00000000..0c3618aa --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/types/features.dart @@ -0,0 +1,13 @@ +class SystemFeatures { + SystemFeatures(this.hasFlash, this.hasBackCamera, this.hasFrontCamera); + + factory SystemFeatures.fromJson(Map features) => + SystemFeatures( + features['hasFlash'] ?? false, + features['hasBackCamera'] ?? false, + features['hasFrontCamera'] ?? false); + + final bool hasFlash; + final bool hasFrontCamera; + final bool hasBackCamera; +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_stub.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_stub.dart new file mode 100644 index 00000000..a6bc04de --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_stub.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:qr_code_scanner/src/types/camera.dart'; + +Widget createWebQrView( + {onPlatformViewCreated, onPermissionSet, CameraFacing? cameraFacing}) => + const SizedBox(); diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_web.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_web.dart new file mode 100644 index 00000000..de368ace --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/flutter_qr_web.dart @@ -0,0 +1,334 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + +import 'dart:async'; +import 'dart:core'; +import 'dart:html' as html; +import 'dart:js_util'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +import '../../qr_code_scanner.dart'; +import 'jsqr.dart'; +import 'media.dart'; + +/// Even though it has been highly modified, the origial implementation has been +/// adopted from https://github.com:treeder/jsqr_flutter +/// +/// Copyright 2020 @treeder +/// Copyright 2021 The one with the braid + +class WebQrView extends StatefulWidget { + final QRViewCreatedCallback onPlatformViewCreated; + final PermissionSetCallback? onPermissionSet; + final CameraFacing? cameraFacing; + + const WebQrView( + {Key? key, + required this.onPlatformViewCreated, + this.onPermissionSet, + this.cameraFacing = CameraFacing.front}) + : super(key: key); + + @override + _WebQrViewState createState() => _WebQrViewState(); + + static html.DivElement vidDiv = + html.DivElement(); // need a global for the registerViewFactory + + static Future cameraAvailable() async { + final sources = + await html.window.navigator.mediaDevices!.enumerateDevices(); + // List vidIds = []; + var hasCam = false; + for (final e in sources) { + if (e.kind == 'videoinput') { + // vidIds.add(e['deviceId']); + hasCam = true; + } + } + return hasCam; + } +} + +class _WebQrViewState extends State { + html.MediaStream? _localStream; + // html.CanvasElement canvas; + // html.CanvasRenderingContext2D ctx; + bool _currentlyProcessing = false; + + QRViewControllerWeb? _controller; + + late Size _size = const Size(0, 0); + Timer? timer; + String? code; + String? _errorMsg; + html.VideoElement video = html.VideoElement(); + String viewID = 'QRVIEW-' + DateTime.now().millisecondsSinceEpoch.toString(); + + final StreamController _scanUpdateController = + StreamController(); + late CameraFacing facing; + + Timer? _frameIntervall; + + @override + void initState() { + super.initState(); + + facing = widget.cameraFacing ?? CameraFacing.front; + + // video = html.VideoElement(); + WebQrView.vidDiv.children = [video]; + // ignore: UNDEFINED_PREFIXED_NAME + ui.platformViewRegistry + .registerViewFactory(viewID, (int id) => WebQrView.vidDiv); + // giving JavaScipt some time to process the DOM changes + Timer(const Duration(milliseconds: 500), () { + start(); + }); + } + + Future start() async { + await _makeCall(); + _frameIntervall?.cancel(); + _frameIntervall = + Timer.periodic(const Duration(milliseconds: 200), (timer) { + _captureFrame2(); + }); + } + + void cancel() { + if (timer != null) { + timer!.cancel(); + timer = null; + } + if (_currentlyProcessing) { + _stopStream(); + } + } + + @override + void dispose() { + cancel(); + super.dispose(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future _makeCall() async { + if (_localStream != null) { + return; + } + + try { + var constraints = UserMediaOptions( + video: VideoOptions( + facingMode: (facing == CameraFacing.front ? 'user' : 'environment'), + )); + // dart style, not working properly: + // var stream = + // await html.window.navigator.mediaDevices.getUserMedia(constraints); + // straight JS: + if (_controller == null) { + _controller = QRViewControllerWeb(this); + widget.onPlatformViewCreated(_controller!); + } + var stream = await promiseToFuture(getUserMedia(constraints)); + widget.onPermissionSet?.call(_controller!, true); + _localStream = stream; + video.srcObject = _localStream; + video.setAttribute('playsinline', + 'true'); // required to tell iOS safari we don't want fullscreen + await video.play(); + } catch (e) { + cancel(); + if (e.toString().contains("NotAllowedError")) { + widget.onPermissionSet?.call(_controller!, false); + } + setState(() { + _errorMsg = e.toString(); + }); + return; + } + if (!mounted) return; + + setState(() { + _currentlyProcessing = true; + }); + } + + Future _stopStream() async { + try { + // await _localStream.dispose(); + _localStream!.getTracks().forEach((track) { + if (track.readyState == 'live') { + track.stop(); + } + }); + // video.stop(); + video.srcObject = null; + _localStream = null; + // _localRenderer.srcObject = null; + // ignore: empty_catches + } catch (e) {} + } + + Future _captureFrame2() async { + if (_localStream == null) { + return null; + } + final canvas = + html.CanvasElement(width: video.videoWidth, height: video.videoHeight); + final ctx = canvas.context2D; + // canvas.width = video.videoWidth; + // canvas.height = video.videoHeight; + ctx.drawImage(video, 0, 0); + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); + + final size = + Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0); + if (size != _size) { + setState(() { + _setCanvasSize(size); + }); + } + + try { + final code = jsQR(imgData.data, canvas.width, canvas.height); + // ignore: unnecessary_null_comparison + if (code != null && code.data != null) { + _scanUpdateController + .add(Barcode(code.data, BarcodeFormat.qrcode, code.data.codeUnits)); + } + } on NoSuchMethodError { + // Do nothing, this exception occurs continously in web release when no + // code is found. + // NoSuchMethodError: method not found: 'get$data' on null + } + } + + @override + Widget build(BuildContext context) { + if (_errorMsg != null) { + return Center(child: Text(_errorMsg!)); + } + if (_localStream == null) { + return const Center(child: CircularProgressIndicator()); + } + return LayoutBuilder( + builder: (context, constraints) { + var zoom = 1.0; + + if (_size.height != 0) zoom = constraints.maxHeight / _size.height; + + if (_size.width != 0) { + final horizontalZoom = constraints.maxWidth / _size.width; + if (horizontalZoom > zoom) { + zoom = horizontalZoom; + } + } + + return SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: Center( + child: SizedBox.fromSize( + size: _size, + child: Transform.scale( + alignment: Alignment.center, + scale: zoom, + child: HtmlElementView(viewType: viewID), + ), + ), + ), + ); + }, + ); + } + + void _setCanvasSize(ui.Size size) { + setState(() { + _size = size; + }); + } +} + +class QRViewControllerWeb implements QRViewController { + final _WebQrViewState _state; + + QRViewControllerWeb(this._state); + @override + void dispose() => _state.cancel(); + + @override + Future flipCamera() async { + // TODO: improve error handling + _state.facing = _state.facing == CameraFacing.front + ? CameraFacing.back + : CameraFacing.front; + await _state.start(); + return _state.facing; + } + + @override + Future getCameraInfo() async { + return _state.facing; + } + + @override + Future getFlashStatus() async { + // TODO: flash is simply not supported by JavaScipt. To avoid issuing applications, we always return it to be off. + return false; + } + + @override + Future getSystemFeatures() { + // TODO: implement getSystemFeatures + throw UnimplementedError(); + } + + @override + // TODO: implement hasPermissions. Blocking: WebQrView.cameraAvailable() returns a Future whereas a bool is required + bool get hasPermissions => throw UnimplementedError(); + + @override + Future pauseCamera() { + // TODO: implement pauseCamera + throw UnimplementedError(); + } + + @override + Future resumeCamera() { + // TODO: implement resumeCamera + throw UnimplementedError(); + } + + @override + Stream get scannedDataStream => _state._scanUpdateController.stream; + + @override + Future stopCamera() { + // TODO: implement stopCamera + throw UnimplementedError(); + } + + @override + Future toggleFlash() async { + // TODO: flash is simply not supported by JavaScipt + return; + } + + @override + Future scanInvert(bool isScanInvert) { + // TODO: implement scanInvert + throw UnimplementedError(); + } +} + +Widget createWebQrView( + {onPlatformViewCreated, onPermissionSet, CameraFacing? cameraFacing}) => + WebQrView( + onPlatformViewCreated: onPlatformViewCreated, + onPermissionSet: onPermissionSet, + cameraFacing: cameraFacing, + ); diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/jsqr.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/jsqr.dart new file mode 100644 index 00000000..386936c6 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/jsqr.dart @@ -0,0 +1,12 @@ +@JS() +library jsqr; + +import 'package:js/js.dart'; + +@JS('jsQR') +external Code jsQR(var data, int? width, int? height); + +@JS() +class Code { + external String get data; +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/media.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/media.dart new file mode 100644 index 00000000..cacd527a --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/lib/src/web/media.dart @@ -0,0 +1,36 @@ +// This is here because dart doesn't seem to support this properly +// https://stackoverflow.com/questions/61161135/adding-support-for-navigator-mediadevices-getusermedia-to-dart + +@JS('navigator.mediaDevices') +library media_devices; + +import 'package:js/js.dart'; + +@JS('getUserMedia') +external Future getUserMedia(UserMediaOptions constraints); + +@JS() +@anonymous +class UserMediaOptions { + external VideoOptions get video; + + external factory UserMediaOptions({VideoOptions? video}); +} + +@JS() +@anonymous +class VideoOptions { + external String get facingMode; + // external DeviceIdOptions get deviceId; + + external factory VideoOptions( + {String? facingMode, DeviceIdOptions? deviceId}); +} + +@JS() +@anonymous +class DeviceIdOptions { + external String get exact; + + external factory DeviceIdOptions({String? exact}); +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/pubspec.yaml b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/pubspec.yaml new file mode 100644 index 00000000..72f21cb8 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/packages/qr_code_scanner/pubspec.yaml @@ -0,0 +1,31 @@ +name: qr_code_scanner +description: QR code scanner that can be embedded inside flutter. It uses zxing in Android and MTBBarcode scanner in iOS. +version: 1.0.0 +homepage: https://juliuscanute.com +repository: https://github.com/juliuscanute/qr_code_scanner + +environment: + sdk: '>=2.17.0 <3.0.0' + flutter: ">=1.12.0" + +dependencies: + js: ^0.6.3 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + +dev_dependencies: + flutter_lints: ^1.0.4 + +flutter: + plugin: + platforms: + android: + package: net.touchcapture.qr.flutterqr + pluginClass: FlutterQrPlugin + ios: + pluginClass: FlutterQrPlugin +# web: +# pluginClass: FlutterQrPlugin +# fileName: flutter_qr_web.dart diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/pubspec.yaml b/example/wallet/packages/qr-bar-code-scanner-dialog/pubspec.yaml new file mode 100644 index 00000000..47953352 --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/pubspec.yaml @@ -0,0 +1,80 @@ +name: qr_bar_code_scanner_dialog +description: Plugin to show a simple scanner dialog and capture Bar/QR code easily. Works with Android iOS and Web. It uses html5-qrcode scanner for web and qr_code_scanner for Android and iOS +version: 0.0.5 +homepage: https://github.com/Afinas-nl/qr-bar-code-scanner-dialog +publish_to: none + +environment: + sdk: '>=2.18.0 <3.0.0' + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + plugin_platform_interface: ^2.1.2 + qr_code_scanner: + path: packages/qr_code_scanner + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.northladder.qr_bar_code_scanner_dialog + pluginClass: QrBarCodeScannerDialogPlugin + ios: + pluginClass: QrBarCodeScannerDialogPlugin + web: + pluginClass: QrBarCodeScannerDialogWeb + fileName: qr_bar_code_scanner_dialog_web.dart + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_method_channel_test.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_method_channel_test.dart new file mode 100644 index 00000000..1f72e4aa --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_method_channel_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog_method_channel.dart'; + +void main() { + MethodChannelQrBarCodeScannerDialog platform = + MethodChannelQrBarCodeScannerDialog(); + const MethodChannel channel = MethodChannel('qr_bar_code_scanner_dialog'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_test.dart b/example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_test.dart new file mode 100644 index 00000000..911dff1c --- /dev/null +++ b/example/wallet/packages/qr-bar-code-scanner-dialog/test/qr_bar_code_scanner_dialog_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog.dart'; +import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog_platform_interface.dart'; +import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockQrBarCodeScannerDialogPlatform + with MockPlatformInterfaceMixin + implements QrBarCodeScannerDialogPlatform { + @override + Future getPlatformVersion() => Future.value('42'); + + @override + void scanBarOrQrCode( + {BuildContext? context, required Function(String? p1) onScanSuccess}) { + onScanSuccess("CODE"); + } +} + +void main() { + final QrBarCodeScannerDialogPlatform initialPlatform = + QrBarCodeScannerDialogPlatform.instance; + + test('$MethodChannelQrBarCodeScannerDialog is the default instance', () { + expect( + initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + QrBarCodeScannerDialog qrBarCodeScannerDialogPlugin = + QrBarCodeScannerDialog(); + MockQrBarCodeScannerDialogPlatform fakePlatform = + MockQrBarCodeScannerDialogPlatform(); + QrBarCodeScannerDialogPlatform.instance = fakePlatform; + + expect(await qrBarCodeScannerDialogPlugin.getPlatformVersion(), '42'); + }); + + test('scanBarOrQrCodeWeb', () async { + QrBarCodeScannerDialog qrBarCodeScannerDialogPlugin = + QrBarCodeScannerDialog(); + MockQrBarCodeScannerDialogPlatform fakePlatform = + MockQrBarCodeScannerDialogPlatform(); + QrBarCodeScannerDialogPlatform.instance = fakePlatform; + + qrBarCodeScannerDialogPlugin.getScannedQrBarCode(onCode: (code) { + expect(code, 'CODE'); + }); + }); +}