├── .github ├── CODEOWNERS └── workflows │ ├── build.yml │ └── danger.yml ├── .gitignore ├── .swiftlint-danger.yml ├── .swiftlint.yml ├── CONTRIBUTING.md ├── CreditCardReader.podspec ├── CreditCardReader ├── CreditCardReader.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── CreditCardReader │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── CreditCardReaderApp.swift │ ├── Info.plist │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── CreditCardReaderAlt │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── SceneDelegate.swift ├── CreditCardReaderAltTests │ ├── CreditCardReaderAltTests.swift │ └── Info.plist ├── CreditCardReaderAltUITests │ ├── CreditCardReaderAltUITests.swift │ └── Info.plist ├── CreditCardReaderTests │ ├── CreditCardReaderTests.swift │ └── Info.plist └── CreditCardReaderUITests │ ├── CreditCardReaderUITests.swift │ └── Info.plist ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── AltSwiftUI │ └── CreditCardReadView.swift ├── Model │ ├── CreditCard.swift │ ├── CreditCardImageAnalyzer.swift │ └── Error.swift ├── SwiftUI │ └── CreditCardReaderView.swift ├── UIKit │ └── CreditCardReaderViewController.swift └── Views │ ├── CardCaptureController.swift │ ├── CardCaptureView.swift │ ├── OverlayDefaultViewParams.swift │ └── ViewConstants.swift ├── docResources ├── CreditCardReader1.png └── CreditCardReader2.png └── xcode └── package.xcworkspace └── contents.xcworkspacedata /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kevinwl02 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | DEVELOPER_DIR: /Applications/Xcode_12_beta.app/Contents/Developer 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run tests 20 | run: xcodebuild test -project 'CreditCardReader/CreditCardReader.xcodeproj' -scheme CreditCardReaderTests -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 8,OS=latest" 21 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger CI 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.swiftlint-danger.yml' 7 | - '**/*.swift' 8 | jobs: 9 | Danger: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Danger 14 | uses: 417-72KI/danger-swiftlint@v1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | .swiftpm/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | 71 | .DS_Store 72 | 73 | # userstate 74 | 75 | */*.xcodeproj/xcuserdata/ 76 | */*.xcodeproj/project.xcworkspace/xcuserdata/ 77 | -------------------------------------------------------------------------------- /.swiftlint-danger.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - implicit_return 3 | - modifier_order 4 | - first_where 5 | - fatal_error_message 6 | - empty_string 7 | - empty_count 8 | - convenience_type 9 | - toggle_bool 10 | - redundant_nil_coalescing 11 | - sorted_first_last 12 | - contains_over_first_not_nil 13 | - contains_over_filter_count 14 | - contains_over_filter_is_empty 15 | - last_where 16 | - redundant_type_annotation 17 | - enum_case_associated_values_count 18 | disabled_rules: 19 | - trailing_whitespace 20 | - identifier_name 21 | - file_length 22 | - multiple_closures_with_trailing_closure 23 | - type_name 24 | - unused_setter_value 25 | - type_body_length 26 | - weak_delegate 27 | - function_parameter_count 28 | - cyclomatic_complexity 29 | - nesting 30 | - duplicate_imports 31 | - todo 32 | excluded: 33 | - AltSwiftUITests.swift 34 | function_body_length: 35 | warning: 60 36 | error: 100 37 | line_length: 38 | warning: 400 39 | error: 600 40 | custom_rules: 41 | travel_verb_getter_function: 42 | name: "Verb Prefixed Getter Function" 43 | regex: '(?<=func\s)(get|calculate|fetch|retrieve|generate)\w+\([^\)]*\)[\n\t\s]+\-\>[\n\t\s]+\w+\??[\n\t\s]+\{' 44 | message: "Avoid the use of verbs on getter methods unless its a mutating/nonmutating method pair or a factory method." 45 | travel_danger_dictionary_function: 46 | name: "Dictionary initializer is error prone" 47 | regex: 'Dictionary\(uniqueKeysWithValues' 48 | message: "Avoid use of uniqueKeysWithValues initializer as it is crash prone." 49 | severity: error 50 | 51 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - implicit_return 3 | - modifier_order 4 | - first_where 5 | - fatal_error_message 6 | - empty_string 7 | - empty_count 8 | - convenience_type 9 | - toggle_bool 10 | - redundant_nil_coalescing 11 | - sorted_first_last 12 | - contains_over_first_not_nil 13 | - contains_over_filter_count 14 | - contains_over_filter_is_empty 15 | - last_where 16 | - redundant_type_annotation 17 | - enum_case_associated_values_count 18 | disabled_rules: 19 | - trailing_whitespace 20 | - identifier_name 21 | - file_length 22 | - multiple_closures_with_trailing_closure 23 | - type_name 24 | - unused_setter_value 25 | - type_body_length 26 | - weak_delegate 27 | - function_parameter_count 28 | - cyclomatic_complexity 29 | - nesting 30 | - duplicate_imports 31 | excluded: 32 | - AltSwiftUITests.swift 33 | function_body_length: 34 | warning: 60 35 | error: 100 36 | line_length: 37 | warning: 400 38 | error: 600 39 | custom_rules: 40 | travel_verb_getter_function: 41 | name: "Verb Prefixed Getter Function" 42 | regex: '(?<=func\s)(get|calculate|fetch|retrieve|generate)\w+\([^\)]*\)[\n\t\s]+\-\>[\n\t\s]+\w+\??[\n\t\s]+\{' 43 | message: "Avoid the use of verbs on getter methods unless its a mutating/nonmutating method pair or a factory method." 44 | travel_danger_dictionary_function: 45 | name: "Dictionary initializer is error prone" 46 | regex: 'Dictionary\(uniqueKeysWithValues' 47 | message: "Avoid use of uniqueKeysWithValues initializer as it is crash prone." 48 | severity: error 49 | # Uncomment to find closure assignments to non-self variables and possible harder to find retain cycles 50 | # find_non_self_stored_closures: 51 | # name: "Stored closure in variable" 52 | # regex: '[A-z0-1\_\-]+\.[A-z0-1\_\-]+\ \=\ \{' 53 | # message: "Found assignment of stored closure of variable" 54 | 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Submit an Issue 4 | Submit an issue for bugs and improvement/feature requests. Please fill the following information in each issue you submit: 5 | 6 | * Title: Use a clear and descriptive title for the issue to identify the problem. 7 | * Description: Description of the issue. 8 | * Scenario/Steps to Reproduce: numbered step by step. (1,2,3.… and so on) 9 | * Expected behaviour: What you expect to happen. 10 | * Actual behaviour (for bugs): What actually happens. 11 | * How often reproduces? (for bugs): what percentage of the time does it reproduce? 12 | * Version: the version of the library. 13 | * Operating system: The operating system used. 14 | * Additional information: Any additional to help to reproduce. (screenshots, animated gifs) 15 | 16 | ## Pull Requests 17 | 1. Fork the project 18 | 2. Submit a pull request to `master` branch with the following information: 19 | 20 | * Title: Add a summary of what this pull request accomplishes 21 | * Description: Descibes the motivation and further details of this pull request 22 | * Issues: **Important!** Link existing issues that this pull request will close (if any) by using one of the [supported keywords / manually](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) 23 | 24 | ## Coding Guidelines 25 | * All public types and methods must be documented 26 | * Code should follow the [coding guidelines](https://github.com/RakutenTravel/ios-coding-guidelines). 27 | 28 | ## Commit messages 29 | Each commit message consists of a header and a body. 30 | 31 | ``` 32 |
33 | 34 | 35 | ``` 36 | 37 | The **header** is mandatory. 38 | 39 | Any line of the commit message cannot be longer 100 characters! This allows the message to be easier 40 | to read on GitHub as well as in various git tools. 41 | 42 | ### Revert 43 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted. 44 | 45 | ### Breaking Changes 46 | 47 | When a commit has **Breaking Changes**, the **header** should be prefixed by the keyword `BREAKING:`. 48 | 49 | ### Header 50 | The header contains succinct description of the change: 51 | 52 | * use the imperative, present tense: "change" not "changed" nor "changes" 53 | * don't capitalize first letter 54 | * no dot (.) at the end 55 | 56 | ### Body 57 | Just as in the **header**, use the imperative, present tense: "change" not "changed" nor "changes". 58 | The body should include the motivation for the change and contrast this with previous behavior. -------------------------------------------------------------------------------- /CreditCardReader.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CreditCardReader" 3 | s.version = "1.0.0" 4 | s.ios.deployment_target = "11.0" 5 | s.swift_version = "5.3" 6 | s.summary = "Extracts credit card information with the device's camera" 7 | s.description = <<-DESC 8 | Customizable library for credit card information detection. 9 | DESC 10 | s.homepage = "https://github.com/rakutentech/iOS-CreditCardReader" 11 | s.license = "MIT" 12 | s.author = { "Kevin Wong" => "kevin.a.wong@rakuten.com" } 13 | s.source = { :git => "https://github.com/rakutentech/iOS-CreditCardReader.git", :tag => "#{s.version}" } 14 | 15 | s.subspec 'SwiftUI' do |u| 16 | u.source_files = ["Sources/Model/*.swift", "Sources/Views/*.swift", "Sources/SwiftUI/*.swift"] 17 | end 18 | 19 | s.subspec 'AltSwiftUI' do |u| 20 | u.source_files = ["Sources/Model/*.swift", "Sources/Views/*.swift", "Sources/AltSwiftUI/*.swift"] 21 | u.dependency 'AltSwiftUI', '~> 1.5' 22 | end 23 | 24 | s.subspec 'UIKit' do |u| 25 | u.source_files = ["Sources/Model/*.swift", "Sources/Views/*.swift", "Sources/UIKit/*.swift"] 26 | end 27 | end -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2CA26181262E9BFA00B5FD6D /* CardCaptureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA26180262E9BFA00B5FD6D /* CardCaptureController.swift */; }; 11 | 2CA26267262ECCF900B5FD6D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA26266262ECCF900B5FD6D /* AppDelegate.swift */; }; 12 | 2CA26269262ECCF900B5FD6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA26268262ECCF900B5FD6D /* SceneDelegate.swift */; }; 13 | 2CA2626B262ECCF900B5FD6D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA2626A262ECCF900B5FD6D /* ContentView.swift */; }; 14 | 2CA2626D262ECCFB00B5FD6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA2626C262ECCFB00B5FD6D /* Assets.xcassets */; }; 15 | 2CA26270262ECCFB00B5FD6D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA2626F262ECCFB00B5FD6D /* Preview Assets.xcassets */; }; 16 | 2CA26273262ECCFB00B5FD6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2CA26271262ECCFB00B5FD6D /* LaunchScreen.storyboard */; }; 17 | 2CA2627E262ECCFC00B5FD6D /* CreditCardReaderAltTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA2627D262ECCFC00B5FD6D /* CreditCardReaderAltTests.swift */; }; 18 | 2CA26289262ECCFC00B5FD6D /* CreditCardReaderAltUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA26288262ECCFC00B5FD6D /* CreditCardReaderAltUITests.swift */; }; 19 | 2CA2629A262ECD0200B5FD6D /* CreditCardReadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA261D4262EAA0C00B5FD6D /* CreditCardReadView.swift */; }; 20 | 2CA262A1262ECD0400B5FD6D /* CreditCardImageAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC337A9262D7121003E8FDB /* CreditCardImageAnalyzer.swift */; }; 21 | 2CA262A2262ECD0400B5FD6D /* CreditCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE9686E262D7752007A5045 /* CreditCard.swift */; }; 22 | 2CA262A3262ECD0400B5FD6D /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC33795262D2B0B003E8FDB /* Error.swift */; }; 23 | 2CA262AA262ECD0700B5FD6D /* OverlayDefaultViewParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC33780262D22D0003E8FDB /* OverlayDefaultViewParams.swift */; }; 24 | 2CA262AB262ECD0700B5FD6D /* CardCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3377B262D227E003E8FDB /* CardCaptureView.swift */; }; 25 | 2CA262AC262ECD0700B5FD6D /* CreditCardReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3378A262D2309003E8FDB /* CreditCardReaderViewController.swift */; }; 26 | 2CA262AD262ECD0700B5FD6D /* CardCaptureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA26180262E9BFA00B5FD6D /* CardCaptureController.swift */; }; 27 | 2CA262C2262ECD7B00B5FD6D /* AltSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 2CA262C1262ECD7B00B5FD6D /* AltSwiftUI */; }; 28 | 2CB2DCAC26537ACE0052C2B4 /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB2DCAB26537ACE0052C2B4 /* ViewConstants.swift */; }; 29 | 2CB2DCB326537CC80052C2B4 /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB2DCAB26537ACE0052C2B4 /* ViewConstants.swift */; }; 30 | 2CC3374D262D221C003E8FDB /* CreditCardReaderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3374C262D221C003E8FDB /* CreditCardReaderApp.swift */; }; 31 | 2CC3374F262D221C003E8FDB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3374E262D221C003E8FDB /* ContentView.swift */; }; 32 | 2CC33751262D221E003E8FDB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CC33750262D221E003E8FDB /* Assets.xcassets */; }; 33 | 2CC33754262D221E003E8FDB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CC33753262D221E003E8FDB /* Preview Assets.xcassets */; }; 34 | 2CC3375F262D221F003E8FDB /* CreditCardReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3375E262D221F003E8FDB /* CreditCardReaderTests.swift */; }; 35 | 2CC3376A262D221F003E8FDB /* CreditCardReaderUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC33769262D221F003E8FDB /* CreditCardReaderUITests.swift */; }; 36 | 2CC3377C262D227E003E8FDB /* CardCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3377B262D227E003E8FDB /* CardCaptureView.swift */; }; 37 | 2CC33781262D22D0003E8FDB /* OverlayDefaultViewParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC33780262D22D0003E8FDB /* OverlayDefaultViewParams.swift */; }; 38 | 2CC3378B262D2309003E8FDB /* CreditCardReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3378A262D2309003E8FDB /* CreditCardReaderViewController.swift */; }; 39 | 2CC33796262D2B0B003E8FDB /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC33795262D2B0B003E8FDB /* Error.swift */; }; 40 | 2CC3379C262D425C003E8FDB /* CreditCardReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3379B262D425C003E8FDB /* CreditCardReaderView.swift */; }; 41 | 2CC337AA262D7121003E8FDB /* CreditCardImageAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC337A9262D7121003E8FDB /* CreditCardImageAnalyzer.swift */; }; 42 | 2CE9686F262D7752007A5045 /* CreditCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE9686E262D7752007A5045 /* CreditCard.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXContainerItemProxy section */ 46 | 2CA2627A262ECCFC00B5FD6D /* PBXContainerItemProxy */ = { 47 | isa = PBXContainerItemProxy; 48 | containerPortal = 2CC33741262D221C003E8FDB /* Project object */; 49 | proxyType = 1; 50 | remoteGlobalIDString = 2CA26263262ECCF900B5FD6D; 51 | remoteInfo = CreditCardReaderAlt; 52 | }; 53 | 2CA26285262ECCFC00B5FD6D /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = 2CC33741262D221C003E8FDB /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = 2CA26263262ECCF900B5FD6D; 58 | remoteInfo = CreditCardReaderAlt; 59 | }; 60 | 2CC3375B262D221F003E8FDB /* PBXContainerItemProxy */ = { 61 | isa = PBXContainerItemProxy; 62 | containerPortal = 2CC33741262D221C003E8FDB /* Project object */; 63 | proxyType = 1; 64 | remoteGlobalIDString = 2CC33748262D221C003E8FDB; 65 | remoteInfo = CreditCardReader; 66 | }; 67 | 2CC33766262D221F003E8FDB /* PBXContainerItemProxy */ = { 68 | isa = PBXContainerItemProxy; 69 | containerPortal = 2CC33741262D221C003E8FDB /* Project object */; 70 | proxyType = 1; 71 | remoteGlobalIDString = 2CC33748262D221C003E8FDB; 72 | remoteInfo = CreditCardReader; 73 | }; 74 | /* End PBXContainerItemProxy section */ 75 | 76 | /* Begin PBXFileReference section */ 77 | 2CA26180262E9BFA00B5FD6D /* CardCaptureController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardCaptureController.swift; sourceTree = ""; }; 78 | 2CA261D4262EAA0C00B5FD6D /* CreditCardReadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReadView.swift; sourceTree = ""; }; 79 | 2CA26264262ECCF900B5FD6D /* CreditCardReaderAlt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CreditCardReaderAlt.app; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | 2CA26266262ECCF900B5FD6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 81 | 2CA26268262ECCF900B5FD6D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 82 | 2CA2626A262ECCF900B5FD6D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 83 | 2CA2626C262ECCFB00B5FD6D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 84 | 2CA2626F262ECCFB00B5FD6D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 85 | 2CA26272262ECCFB00B5FD6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 86 | 2CA26274262ECCFB00B5FD6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 87 | 2CA26279262ECCFC00B5FD6D /* CreditCardReaderAltTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CreditCardReaderAltTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | 2CA2627D262ECCFC00B5FD6D /* CreditCardReaderAltTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderAltTests.swift; sourceTree = ""; }; 89 | 2CA2627F262ECCFC00B5FD6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 90 | 2CA26284262ECCFC00B5FD6D /* CreditCardReaderAltUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CreditCardReaderAltUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | 2CA26288262ECCFC00B5FD6D /* CreditCardReaderAltUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderAltUITests.swift; sourceTree = ""; }; 92 | 2CA2628A262ECCFC00B5FD6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 93 | 2CB2DCAB26537ACE0052C2B4 /* ViewConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewConstants.swift; sourceTree = ""; }; 94 | 2CC33749262D221C003E8FDB /* CreditCardReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CreditCardReader.app; sourceTree = BUILT_PRODUCTS_DIR; }; 95 | 2CC3374C262D221C003E8FDB /* CreditCardReaderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderApp.swift; sourceTree = ""; }; 96 | 2CC3374E262D221C003E8FDB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 97 | 2CC33750262D221E003E8FDB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 98 | 2CC33753262D221E003E8FDB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 99 | 2CC33755262D221E003E8FDB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 100 | 2CC3375A262D221F003E8FDB /* CreditCardReaderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CreditCardReaderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 101 | 2CC3375E262D221F003E8FDB /* CreditCardReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderTests.swift; sourceTree = ""; }; 102 | 2CC33760262D221F003E8FDB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 103 | 2CC33765262D221F003E8FDB /* CreditCardReaderUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CreditCardReaderUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 104 | 2CC33769262D221F003E8FDB /* CreditCardReaderUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderUITests.swift; sourceTree = ""; }; 105 | 2CC3376B262D221F003E8FDB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 106 | 2CC3377B262D227E003E8FDB /* CardCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardCaptureView.swift; sourceTree = ""; }; 107 | 2CC33780262D22D0003E8FDB /* OverlayDefaultViewParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayDefaultViewParams.swift; sourceTree = ""; }; 108 | 2CC3378A262D2309003E8FDB /* CreditCardReaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderViewController.swift; sourceTree = ""; }; 109 | 2CC33795262D2B0B003E8FDB /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 110 | 2CC3379B262D425C003E8FDB /* CreditCardReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardReaderView.swift; sourceTree = ""; }; 111 | 2CC337A9262D7121003E8FDB /* CreditCardImageAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardImageAnalyzer.swift; sourceTree = ""; }; 112 | 2CE9686E262D7752007A5045 /* CreditCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCard.swift; sourceTree = ""; }; 113 | /* End PBXFileReference section */ 114 | 115 | /* Begin PBXFrameworksBuildPhase section */ 116 | 2CA26261262ECCF900B5FD6D /* Frameworks */ = { 117 | isa = PBXFrameworksBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | 2CA262C2262ECD7B00B5FD6D /* AltSwiftUI in Frameworks */, 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | 2CA26276262ECCFC00B5FD6D /* Frameworks */ = { 125 | isa = PBXFrameworksBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | 2CA26281262ECCFC00B5FD6D /* Frameworks */ = { 132 | isa = PBXFrameworksBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | 2CC33746262D221C003E8FDB /* Frameworks */ = { 139 | isa = PBXFrameworksBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | 2CC33757262D221F003E8FDB /* Frameworks */ = { 146 | isa = PBXFrameworksBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | 2CC33762262D221F003E8FDB /* Frameworks */ = { 153 | isa = PBXFrameworksBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXFrameworksBuildPhase section */ 160 | 161 | /* Begin PBXGroup section */ 162 | 2CA26185262E9C7100B5FD6D /* AltSwiftUI */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 2CA261D4262EAA0C00B5FD6D /* CreditCardReadView.swift */, 166 | ); 167 | path = AltSwiftUI; 168 | sourceTree = ""; 169 | }; 170 | 2CA26265262ECCF900B5FD6D /* CreditCardReaderAlt */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 2CA26266262ECCF900B5FD6D /* AppDelegate.swift */, 174 | 2CA26268262ECCF900B5FD6D /* SceneDelegate.swift */, 175 | 2CA2626A262ECCF900B5FD6D /* ContentView.swift */, 176 | 2CA2626C262ECCFB00B5FD6D /* Assets.xcassets */, 177 | 2CA26271262ECCFB00B5FD6D /* LaunchScreen.storyboard */, 178 | 2CA26274262ECCFB00B5FD6D /* Info.plist */, 179 | 2CA2626E262ECCFB00B5FD6D /* Preview Content */, 180 | ); 181 | path = CreditCardReaderAlt; 182 | sourceTree = ""; 183 | }; 184 | 2CA2626E262ECCFB00B5FD6D /* Preview Content */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 2CA2626F262ECCFB00B5FD6D /* Preview Assets.xcassets */, 188 | ); 189 | path = "Preview Content"; 190 | sourceTree = ""; 191 | }; 192 | 2CA2627C262ECCFC00B5FD6D /* CreditCardReaderAltTests */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 2CA2627D262ECCFC00B5FD6D /* CreditCardReaderAltTests.swift */, 196 | 2CA2627F262ECCFC00B5FD6D /* Info.plist */, 197 | ); 198 | path = CreditCardReaderAltTests; 199 | sourceTree = ""; 200 | }; 201 | 2CA26287262ECCFC00B5FD6D /* CreditCardReaderAltUITests */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 2CA26288262ECCFC00B5FD6D /* CreditCardReaderAltUITests.swift */, 205 | 2CA2628A262ECCFC00B5FD6D /* Info.plist */, 206 | ); 207 | path = CreditCardReaderAltUITests; 208 | sourceTree = ""; 209 | }; 210 | 2CA262C0262ECD7B00B5FD6D /* Frameworks */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | ); 214 | name = Frameworks; 215 | sourceTree = ""; 216 | }; 217 | 2CB2DCBA2653A2840052C2B4 /* UIKit */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | 2CC3378A262D2309003E8FDB /* CreditCardReaderViewController.swift */, 221 | ); 222 | path = UIKit; 223 | sourceTree = ""; 224 | }; 225 | 2CC33740262D221C003E8FDB = { 226 | isa = PBXGroup; 227 | children = ( 228 | 2CC3374B262D221C003E8FDB /* CreditCardReader */, 229 | 2CC3375D262D221F003E8FDB /* CreditCardReaderTests */, 230 | 2CC33768262D221F003E8FDB /* CreditCardReaderUITests */, 231 | 2CA26265262ECCF900B5FD6D /* CreditCardReaderAlt */, 232 | 2CA2627C262ECCFC00B5FD6D /* CreditCardReaderAltTests */, 233 | 2CA26287262ECCFC00B5FD6D /* CreditCardReaderAltUITests */, 234 | 2CC3374A262D221C003E8FDB /* Products */, 235 | 2CA262C0262ECD7B00B5FD6D /* Frameworks */, 236 | ); 237 | sourceTree = ""; 238 | }; 239 | 2CC3374A262D221C003E8FDB /* Products */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | 2CC33749262D221C003E8FDB /* CreditCardReader.app */, 243 | 2CC3375A262D221F003E8FDB /* CreditCardReaderTests.xctest */, 244 | 2CC33765262D221F003E8FDB /* CreditCardReaderUITests.xctest */, 245 | 2CA26264262ECCF900B5FD6D /* CreditCardReaderAlt.app */, 246 | 2CA26279262ECCFC00B5FD6D /* CreditCardReaderAltTests.xctest */, 247 | 2CA26284262ECCFC00B5FD6D /* CreditCardReaderAltUITests.xctest */, 248 | ); 249 | name = Products; 250 | sourceTree = ""; 251 | }; 252 | 2CC3374B262D221C003E8FDB /* CreditCardReader */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 2CC3377A262D226A003E8FDB /* Sources */, 256 | 2CC3374C262D221C003E8FDB /* CreditCardReaderApp.swift */, 257 | 2CC3374E262D221C003E8FDB /* ContentView.swift */, 258 | 2CC33750262D221E003E8FDB /* Assets.xcassets */, 259 | 2CC33755262D221E003E8FDB /* Info.plist */, 260 | 2CC33752262D221E003E8FDB /* Preview Content */, 261 | ); 262 | path = CreditCardReader; 263 | sourceTree = ""; 264 | }; 265 | 2CC33752262D221E003E8FDB /* Preview Content */ = { 266 | isa = PBXGroup; 267 | children = ( 268 | 2CC33753262D221E003E8FDB /* Preview Assets.xcassets */, 269 | ); 270 | path = "Preview Content"; 271 | sourceTree = ""; 272 | }; 273 | 2CC3375D262D221F003E8FDB /* CreditCardReaderTests */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | 2CC3375E262D221F003E8FDB /* CreditCardReaderTests.swift */, 277 | 2CC33760262D221F003E8FDB /* Info.plist */, 278 | ); 279 | path = CreditCardReaderTests; 280 | sourceTree = ""; 281 | }; 282 | 2CC33768262D221F003E8FDB /* CreditCardReaderUITests */ = { 283 | isa = PBXGroup; 284 | children = ( 285 | 2CC33769262D221F003E8FDB /* CreditCardReaderUITests.swift */, 286 | 2CC3376B262D221F003E8FDB /* Info.plist */, 287 | ); 288 | path = CreditCardReaderUITests; 289 | sourceTree = ""; 290 | }; 291 | 2CC3377A262D226A003E8FDB /* Sources */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | 2CB2DCBA2653A2840052C2B4 /* UIKit */, 295 | 2CA26185262E9C7100B5FD6D /* AltSwiftUI */, 296 | 2CE9686A262D7742007A5045 /* Model */, 297 | 2CE96869262D7726007A5045 /* Views */, 298 | 2CC3379A262D422E003E8FDB /* SwiftUI */, 299 | ); 300 | name = Sources; 301 | path = ../../Sources; 302 | sourceTree = ""; 303 | }; 304 | 2CC3379A262D422E003E8FDB /* SwiftUI */ = { 305 | isa = PBXGroup; 306 | children = ( 307 | 2CC3379B262D425C003E8FDB /* CreditCardReaderView.swift */, 308 | ); 309 | path = SwiftUI; 310 | sourceTree = ""; 311 | }; 312 | 2CE96869262D7726007A5045 /* Views */ = { 313 | isa = PBXGroup; 314 | children = ( 315 | 2CC3377B262D227E003E8FDB /* CardCaptureView.swift */, 316 | 2CC33780262D22D0003E8FDB /* OverlayDefaultViewParams.swift */, 317 | 2CA26180262E9BFA00B5FD6D /* CardCaptureController.swift */, 318 | 2CB2DCAB26537ACE0052C2B4 /* ViewConstants.swift */, 319 | ); 320 | path = Views; 321 | sourceTree = ""; 322 | }; 323 | 2CE9686A262D7742007A5045 /* Model */ = { 324 | isa = PBXGroup; 325 | children = ( 326 | 2CC337A9262D7121003E8FDB /* CreditCardImageAnalyzer.swift */, 327 | 2CC33795262D2B0B003E8FDB /* Error.swift */, 328 | 2CE9686E262D7752007A5045 /* CreditCard.swift */, 329 | ); 330 | path = Model; 331 | sourceTree = ""; 332 | }; 333 | /* End PBXGroup section */ 334 | 335 | /* Begin PBXNativeTarget section */ 336 | 2CA26263262ECCF900B5FD6D /* CreditCardReaderAlt */ = { 337 | isa = PBXNativeTarget; 338 | buildConfigurationList = 2CA2628B262ECCFC00B5FD6D /* Build configuration list for PBXNativeTarget "CreditCardReaderAlt" */; 339 | buildPhases = ( 340 | 2CB2DCDD265503400052C2B4 /* SwiftLint */, 341 | 2CA26260262ECCF900B5FD6D /* Sources */, 342 | 2CA26261262ECCF900B5FD6D /* Frameworks */, 343 | 2CA26262262ECCF900B5FD6D /* Resources */, 344 | ); 345 | buildRules = ( 346 | ); 347 | dependencies = ( 348 | ); 349 | name = CreditCardReaderAlt; 350 | packageProductDependencies = ( 351 | 2CA262C1262ECD7B00B5FD6D /* AltSwiftUI */, 352 | ); 353 | productName = CreditCardReaderAlt; 354 | productReference = 2CA26264262ECCF900B5FD6D /* CreditCardReaderAlt.app */; 355 | productType = "com.apple.product-type.application"; 356 | }; 357 | 2CA26278262ECCFC00B5FD6D /* CreditCardReaderAltTests */ = { 358 | isa = PBXNativeTarget; 359 | buildConfigurationList = 2CA2628E262ECCFC00B5FD6D /* Build configuration list for PBXNativeTarget "CreditCardReaderAltTests" */; 360 | buildPhases = ( 361 | 2CA26275262ECCFC00B5FD6D /* Sources */, 362 | 2CA26276262ECCFC00B5FD6D /* Frameworks */, 363 | 2CA26277262ECCFC00B5FD6D /* Resources */, 364 | ); 365 | buildRules = ( 366 | ); 367 | dependencies = ( 368 | 2CA2627B262ECCFC00B5FD6D /* PBXTargetDependency */, 369 | ); 370 | name = CreditCardReaderAltTests; 371 | productName = CreditCardReaderAltTests; 372 | productReference = 2CA26279262ECCFC00B5FD6D /* CreditCardReaderAltTests.xctest */; 373 | productType = "com.apple.product-type.bundle.unit-test"; 374 | }; 375 | 2CA26283262ECCFC00B5FD6D /* CreditCardReaderAltUITests */ = { 376 | isa = PBXNativeTarget; 377 | buildConfigurationList = 2CA26291262ECCFC00B5FD6D /* Build configuration list for PBXNativeTarget "CreditCardReaderAltUITests" */; 378 | buildPhases = ( 379 | 2CA26280262ECCFC00B5FD6D /* Sources */, 380 | 2CA26281262ECCFC00B5FD6D /* Frameworks */, 381 | 2CA26282262ECCFC00B5FD6D /* Resources */, 382 | ); 383 | buildRules = ( 384 | ); 385 | dependencies = ( 386 | 2CA26286262ECCFC00B5FD6D /* PBXTargetDependency */, 387 | ); 388 | name = CreditCardReaderAltUITests; 389 | productName = CreditCardReaderAltUITests; 390 | productReference = 2CA26284262ECCFC00B5FD6D /* CreditCardReaderAltUITests.xctest */; 391 | productType = "com.apple.product-type.bundle.ui-testing"; 392 | }; 393 | 2CC33748262D221C003E8FDB /* CreditCardReader */ = { 394 | isa = PBXNativeTarget; 395 | buildConfigurationList = 2CC3376E262D221F003E8FDB /* Build configuration list for PBXNativeTarget "CreditCardReader" */; 396 | buildPhases = ( 397 | 2CB2DCDC265503300052C2B4 /* SwiftLint */, 398 | 2CC33745262D221C003E8FDB /* Sources */, 399 | 2CC33746262D221C003E8FDB /* Frameworks */, 400 | 2CC33747262D221C003E8FDB /* Resources */, 401 | ); 402 | buildRules = ( 403 | ); 404 | dependencies = ( 405 | ); 406 | name = CreditCardReader; 407 | productName = CreditCardReader; 408 | productReference = 2CC33749262D221C003E8FDB /* CreditCardReader.app */; 409 | productType = "com.apple.product-type.application"; 410 | }; 411 | 2CC33759262D221F003E8FDB /* CreditCardReaderTests */ = { 412 | isa = PBXNativeTarget; 413 | buildConfigurationList = 2CC33771262D221F003E8FDB /* Build configuration list for PBXNativeTarget "CreditCardReaderTests" */; 414 | buildPhases = ( 415 | 2CC33756262D221F003E8FDB /* Sources */, 416 | 2CC33757262D221F003E8FDB /* Frameworks */, 417 | 2CC33758262D221F003E8FDB /* Resources */, 418 | ); 419 | buildRules = ( 420 | ); 421 | dependencies = ( 422 | 2CC3375C262D221F003E8FDB /* PBXTargetDependency */, 423 | ); 424 | name = CreditCardReaderTests; 425 | productName = CreditCardReaderTests; 426 | productReference = 2CC3375A262D221F003E8FDB /* CreditCardReaderTests.xctest */; 427 | productType = "com.apple.product-type.bundle.unit-test"; 428 | }; 429 | 2CC33764262D221F003E8FDB /* CreditCardReaderUITests */ = { 430 | isa = PBXNativeTarget; 431 | buildConfigurationList = 2CC33774262D221F003E8FDB /* Build configuration list for PBXNativeTarget "CreditCardReaderUITests" */; 432 | buildPhases = ( 433 | 2CC33761262D221F003E8FDB /* Sources */, 434 | 2CC33762262D221F003E8FDB /* Frameworks */, 435 | 2CC33763262D221F003E8FDB /* Resources */, 436 | ); 437 | buildRules = ( 438 | ); 439 | dependencies = ( 440 | 2CC33767262D221F003E8FDB /* PBXTargetDependency */, 441 | ); 442 | name = CreditCardReaderUITests; 443 | productName = CreditCardReaderUITests; 444 | productReference = 2CC33765262D221F003E8FDB /* CreditCardReaderUITests.xctest */; 445 | productType = "com.apple.product-type.bundle.ui-testing"; 446 | }; 447 | /* End PBXNativeTarget section */ 448 | 449 | /* Begin PBXProject section */ 450 | 2CC33741262D221C003E8FDB /* Project object */ = { 451 | isa = PBXProject; 452 | attributes = { 453 | LastSwiftUpdateCheck = 1200; 454 | LastUpgradeCheck = 1200; 455 | TargetAttributes = { 456 | 2CA26263262ECCF900B5FD6D = { 457 | CreatedOnToolsVersion = 12.0; 458 | }; 459 | 2CA26278262ECCFC00B5FD6D = { 460 | CreatedOnToolsVersion = 12.0; 461 | TestTargetID = 2CA26263262ECCF900B5FD6D; 462 | }; 463 | 2CA26283262ECCFC00B5FD6D = { 464 | CreatedOnToolsVersion = 12.0; 465 | TestTargetID = 2CA26263262ECCF900B5FD6D; 466 | }; 467 | 2CC33748262D221C003E8FDB = { 468 | CreatedOnToolsVersion = 12.0; 469 | }; 470 | 2CC33759262D221F003E8FDB = { 471 | CreatedOnToolsVersion = 12.0; 472 | TestTargetID = 2CC33748262D221C003E8FDB; 473 | }; 474 | 2CC33764262D221F003E8FDB = { 475 | CreatedOnToolsVersion = 12.0; 476 | TestTargetID = 2CC33748262D221C003E8FDB; 477 | }; 478 | }; 479 | }; 480 | buildConfigurationList = 2CC33744262D221C003E8FDB /* Build configuration list for PBXProject "CreditCardReader" */; 481 | compatibilityVersion = "Xcode 9.3"; 482 | developmentRegion = en; 483 | hasScannedForEncodings = 0; 484 | knownRegions = ( 485 | en, 486 | Base, 487 | ); 488 | mainGroup = 2CC33740262D221C003E8FDB; 489 | packageReferences = ( 490 | 2CA261DC262EAAA400B5FD6D /* XCRemoteSwiftPackageReference "AltSwiftUI" */, 491 | ); 492 | productRefGroup = 2CC3374A262D221C003E8FDB /* Products */; 493 | projectDirPath = ""; 494 | projectRoot = ""; 495 | targets = ( 496 | 2CC33748262D221C003E8FDB /* CreditCardReader */, 497 | 2CC33759262D221F003E8FDB /* CreditCardReaderTests */, 498 | 2CC33764262D221F003E8FDB /* CreditCardReaderUITests */, 499 | 2CA26263262ECCF900B5FD6D /* CreditCardReaderAlt */, 500 | 2CA26278262ECCFC00B5FD6D /* CreditCardReaderAltTests */, 501 | 2CA26283262ECCFC00B5FD6D /* CreditCardReaderAltUITests */, 502 | ); 503 | }; 504 | /* End PBXProject section */ 505 | 506 | /* Begin PBXResourcesBuildPhase section */ 507 | 2CA26262262ECCF900B5FD6D /* Resources */ = { 508 | isa = PBXResourcesBuildPhase; 509 | buildActionMask = 2147483647; 510 | files = ( 511 | 2CA26273262ECCFB00B5FD6D /* LaunchScreen.storyboard in Resources */, 512 | 2CA26270262ECCFB00B5FD6D /* Preview Assets.xcassets in Resources */, 513 | 2CA2626D262ECCFB00B5FD6D /* Assets.xcassets in Resources */, 514 | ); 515 | runOnlyForDeploymentPostprocessing = 0; 516 | }; 517 | 2CA26277262ECCFC00B5FD6D /* Resources */ = { 518 | isa = PBXResourcesBuildPhase; 519 | buildActionMask = 2147483647; 520 | files = ( 521 | ); 522 | runOnlyForDeploymentPostprocessing = 0; 523 | }; 524 | 2CA26282262ECCFC00B5FD6D /* Resources */ = { 525 | isa = PBXResourcesBuildPhase; 526 | buildActionMask = 2147483647; 527 | files = ( 528 | ); 529 | runOnlyForDeploymentPostprocessing = 0; 530 | }; 531 | 2CC33747262D221C003E8FDB /* Resources */ = { 532 | isa = PBXResourcesBuildPhase; 533 | buildActionMask = 2147483647; 534 | files = ( 535 | 2CC33754262D221E003E8FDB /* Preview Assets.xcassets in Resources */, 536 | 2CC33751262D221E003E8FDB /* Assets.xcassets in Resources */, 537 | ); 538 | runOnlyForDeploymentPostprocessing = 0; 539 | }; 540 | 2CC33758262D221F003E8FDB /* Resources */ = { 541 | isa = PBXResourcesBuildPhase; 542 | buildActionMask = 2147483647; 543 | files = ( 544 | ); 545 | runOnlyForDeploymentPostprocessing = 0; 546 | }; 547 | 2CC33763262D221F003E8FDB /* Resources */ = { 548 | isa = PBXResourcesBuildPhase; 549 | buildActionMask = 2147483647; 550 | files = ( 551 | ); 552 | runOnlyForDeploymentPostprocessing = 0; 553 | }; 554 | /* End PBXResourcesBuildPhase section */ 555 | 556 | /* Begin PBXShellScriptBuildPhase section */ 557 | 2CB2DCDC265503300052C2B4 /* SwiftLint */ = { 558 | isa = PBXShellScriptBuildPhase; 559 | buildActionMask = 2147483647; 560 | files = ( 561 | ); 562 | inputFileListPaths = ( 563 | ); 564 | inputPaths = ( 565 | ); 566 | name = SwiftLint; 567 | outputFileListPaths = ( 568 | ); 569 | outputPaths = ( 570 | ); 571 | runOnlyForDeploymentPostprocessing = 0; 572 | shellPath = /bin/sh; 573 | shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif which swiftlint >/dev/null; then\n ls\n swiftlint autocorrect\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 574 | }; 575 | 2CB2DCDD265503400052C2B4 /* SwiftLint */ = { 576 | isa = PBXShellScriptBuildPhase; 577 | buildActionMask = 2147483647; 578 | files = ( 579 | ); 580 | inputFileListPaths = ( 581 | ); 582 | inputPaths = ( 583 | ); 584 | name = SwiftLint; 585 | outputFileListPaths = ( 586 | ); 587 | outputPaths = ( 588 | ); 589 | runOnlyForDeploymentPostprocessing = 0; 590 | shellPath = /bin/sh; 591 | shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif which swiftlint >/dev/null; then\n ls\n swiftlint autocorrect\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 592 | }; 593 | /* End PBXShellScriptBuildPhase section */ 594 | 595 | /* Begin PBXSourcesBuildPhase section */ 596 | 2CA26260262ECCF900B5FD6D /* Sources */ = { 597 | isa = PBXSourcesBuildPhase; 598 | buildActionMask = 2147483647; 599 | files = ( 600 | 2CA262AB262ECD0700B5FD6D /* CardCaptureView.swift in Sources */, 601 | 2CA262A2262ECD0400B5FD6D /* CreditCard.swift in Sources */, 602 | 2CA2629A262ECD0200B5FD6D /* CreditCardReadView.swift in Sources */, 603 | 2CA26267262ECCF900B5FD6D /* AppDelegate.swift in Sources */, 604 | 2CA26269262ECCF900B5FD6D /* SceneDelegate.swift in Sources */, 605 | 2CA262A1262ECD0400B5FD6D /* CreditCardImageAnalyzer.swift in Sources */, 606 | 2CB2DCB326537CC80052C2B4 /* ViewConstants.swift in Sources */, 607 | 2CA262A3262ECD0400B5FD6D /* Error.swift in Sources */, 608 | 2CA2626B262ECCF900B5FD6D /* ContentView.swift in Sources */, 609 | 2CA262AC262ECD0700B5FD6D /* CreditCardReaderViewController.swift in Sources */, 610 | 2CA262AD262ECD0700B5FD6D /* CardCaptureController.swift in Sources */, 611 | 2CA262AA262ECD0700B5FD6D /* OverlayDefaultViewParams.swift in Sources */, 612 | ); 613 | runOnlyForDeploymentPostprocessing = 0; 614 | }; 615 | 2CA26275262ECCFC00B5FD6D /* Sources */ = { 616 | isa = PBXSourcesBuildPhase; 617 | buildActionMask = 2147483647; 618 | files = ( 619 | 2CA2627E262ECCFC00B5FD6D /* CreditCardReaderAltTests.swift in Sources */, 620 | ); 621 | runOnlyForDeploymentPostprocessing = 0; 622 | }; 623 | 2CA26280262ECCFC00B5FD6D /* Sources */ = { 624 | isa = PBXSourcesBuildPhase; 625 | buildActionMask = 2147483647; 626 | files = ( 627 | 2CA26289262ECCFC00B5FD6D /* CreditCardReaderAltUITests.swift in Sources */, 628 | ); 629 | runOnlyForDeploymentPostprocessing = 0; 630 | }; 631 | 2CC33745262D221C003E8FDB /* Sources */ = { 632 | isa = PBXSourcesBuildPhase; 633 | buildActionMask = 2147483647; 634 | files = ( 635 | 2CC3379C262D425C003E8FDB /* CreditCardReaderView.swift in Sources */, 636 | 2CC33781262D22D0003E8FDB /* OverlayDefaultViewParams.swift in Sources */, 637 | 2CA26181262E9BFA00B5FD6D /* CardCaptureController.swift in Sources */, 638 | 2CC3378B262D2309003E8FDB /* CreditCardReaderViewController.swift in Sources */, 639 | 2CC3377C262D227E003E8FDB /* CardCaptureView.swift in Sources */, 640 | 2CB2DCAC26537ACE0052C2B4 /* ViewConstants.swift in Sources */, 641 | 2CC3374F262D221C003E8FDB /* ContentView.swift in Sources */, 642 | 2CE9686F262D7752007A5045 /* CreditCard.swift in Sources */, 643 | 2CC33796262D2B0B003E8FDB /* Error.swift in Sources */, 644 | 2CC337AA262D7121003E8FDB /* CreditCardImageAnalyzer.swift in Sources */, 645 | 2CC3374D262D221C003E8FDB /* CreditCardReaderApp.swift in Sources */, 646 | ); 647 | runOnlyForDeploymentPostprocessing = 0; 648 | }; 649 | 2CC33756262D221F003E8FDB /* Sources */ = { 650 | isa = PBXSourcesBuildPhase; 651 | buildActionMask = 2147483647; 652 | files = ( 653 | 2CC3375F262D221F003E8FDB /* CreditCardReaderTests.swift in Sources */, 654 | ); 655 | runOnlyForDeploymentPostprocessing = 0; 656 | }; 657 | 2CC33761262D221F003E8FDB /* Sources */ = { 658 | isa = PBXSourcesBuildPhase; 659 | buildActionMask = 2147483647; 660 | files = ( 661 | 2CC3376A262D221F003E8FDB /* CreditCardReaderUITests.swift in Sources */, 662 | ); 663 | runOnlyForDeploymentPostprocessing = 0; 664 | }; 665 | /* End PBXSourcesBuildPhase section */ 666 | 667 | /* Begin PBXTargetDependency section */ 668 | 2CA2627B262ECCFC00B5FD6D /* PBXTargetDependency */ = { 669 | isa = PBXTargetDependency; 670 | target = 2CA26263262ECCF900B5FD6D /* CreditCardReaderAlt */; 671 | targetProxy = 2CA2627A262ECCFC00B5FD6D /* PBXContainerItemProxy */; 672 | }; 673 | 2CA26286262ECCFC00B5FD6D /* PBXTargetDependency */ = { 674 | isa = PBXTargetDependency; 675 | target = 2CA26263262ECCF900B5FD6D /* CreditCardReaderAlt */; 676 | targetProxy = 2CA26285262ECCFC00B5FD6D /* PBXContainerItemProxy */; 677 | }; 678 | 2CC3375C262D221F003E8FDB /* PBXTargetDependency */ = { 679 | isa = PBXTargetDependency; 680 | target = 2CC33748262D221C003E8FDB /* CreditCardReader */; 681 | targetProxy = 2CC3375B262D221F003E8FDB /* PBXContainerItemProxy */; 682 | }; 683 | 2CC33767262D221F003E8FDB /* PBXTargetDependency */ = { 684 | isa = PBXTargetDependency; 685 | target = 2CC33748262D221C003E8FDB /* CreditCardReader */; 686 | targetProxy = 2CC33766262D221F003E8FDB /* PBXContainerItemProxy */; 687 | }; 688 | /* End PBXTargetDependency section */ 689 | 690 | /* Begin PBXVariantGroup section */ 691 | 2CA26271262ECCFB00B5FD6D /* LaunchScreen.storyboard */ = { 692 | isa = PBXVariantGroup; 693 | children = ( 694 | 2CA26272262ECCFB00B5FD6D /* Base */, 695 | ); 696 | name = LaunchScreen.storyboard; 697 | sourceTree = ""; 698 | }; 699 | /* End PBXVariantGroup section */ 700 | 701 | /* Begin XCBuildConfiguration section */ 702 | 2CA2628C262ECCFC00B5FD6D /* Debug */ = { 703 | isa = XCBuildConfiguration; 704 | buildSettings = { 705 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 706 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 707 | CODE_SIGN_STYLE = Automatic; 708 | DEVELOPMENT_ASSET_PATHS = "\"CreditCardReaderAlt/Preview Content\""; 709 | DEVELOPMENT_TEAM = A34HD644A5; 710 | ENABLE_PREVIEWS = YES; 711 | INFOPLIST_FILE = CreditCardReaderAlt/Info.plist; 712 | LD_RUNPATH_SEARCH_PATHS = ( 713 | "$(inherited)", 714 | "@executable_path/Frameworks", 715 | ); 716 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderAlt; 717 | PRODUCT_NAME = "$(TARGET_NAME)"; 718 | SWIFT_VERSION = 5.0; 719 | TARGETED_DEVICE_FAMILY = "1,2"; 720 | }; 721 | name = Debug; 722 | }; 723 | 2CA2628D262ECCFC00B5FD6D /* Release */ = { 724 | isa = XCBuildConfiguration; 725 | buildSettings = { 726 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 727 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 728 | CODE_SIGN_STYLE = Automatic; 729 | DEVELOPMENT_ASSET_PATHS = "\"CreditCardReaderAlt/Preview Content\""; 730 | DEVELOPMENT_TEAM = A34HD644A5; 731 | ENABLE_PREVIEWS = YES; 732 | INFOPLIST_FILE = CreditCardReaderAlt/Info.plist; 733 | LD_RUNPATH_SEARCH_PATHS = ( 734 | "$(inherited)", 735 | "@executable_path/Frameworks", 736 | ); 737 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderAlt; 738 | PRODUCT_NAME = "$(TARGET_NAME)"; 739 | SWIFT_VERSION = 5.0; 740 | TARGETED_DEVICE_FAMILY = "1,2"; 741 | }; 742 | name = Release; 743 | }; 744 | 2CA2628F262ECCFC00B5FD6D /* Debug */ = { 745 | isa = XCBuildConfiguration; 746 | buildSettings = { 747 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 748 | BUNDLE_LOADER = "$(TEST_HOST)"; 749 | CODE_SIGN_STYLE = Automatic; 750 | INFOPLIST_FILE = CreditCardReaderAltTests/Info.plist; 751 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 752 | LD_RUNPATH_SEARCH_PATHS = ( 753 | "$(inherited)", 754 | "@executable_path/Frameworks", 755 | "@loader_path/Frameworks", 756 | ); 757 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderAltTests; 758 | PRODUCT_NAME = "$(TARGET_NAME)"; 759 | SWIFT_VERSION = 5.0; 760 | TARGETED_DEVICE_FAMILY = "1,2"; 761 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CreditCardReaderAlt.app/CreditCardReaderAlt"; 762 | }; 763 | name = Debug; 764 | }; 765 | 2CA26290262ECCFC00B5FD6D /* Release */ = { 766 | isa = XCBuildConfiguration; 767 | buildSettings = { 768 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 769 | BUNDLE_LOADER = "$(TEST_HOST)"; 770 | CODE_SIGN_STYLE = Automatic; 771 | INFOPLIST_FILE = CreditCardReaderAltTests/Info.plist; 772 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 773 | LD_RUNPATH_SEARCH_PATHS = ( 774 | "$(inherited)", 775 | "@executable_path/Frameworks", 776 | "@loader_path/Frameworks", 777 | ); 778 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderAltTests; 779 | PRODUCT_NAME = "$(TARGET_NAME)"; 780 | SWIFT_VERSION = 5.0; 781 | TARGETED_DEVICE_FAMILY = "1,2"; 782 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CreditCardReaderAlt.app/CreditCardReaderAlt"; 783 | }; 784 | name = Release; 785 | }; 786 | 2CA26292262ECCFC00B5FD6D /* Debug */ = { 787 | isa = XCBuildConfiguration; 788 | buildSettings = { 789 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 790 | CODE_SIGN_STYLE = Automatic; 791 | INFOPLIST_FILE = CreditCardReaderAltUITests/Info.plist; 792 | LD_RUNPATH_SEARCH_PATHS = ( 793 | "$(inherited)", 794 | "@executable_path/Frameworks", 795 | "@loader_path/Frameworks", 796 | ); 797 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderAltUITests; 798 | PRODUCT_NAME = "$(TARGET_NAME)"; 799 | SWIFT_VERSION = 5.0; 800 | TARGETED_DEVICE_FAMILY = "1,2"; 801 | TEST_TARGET_NAME = CreditCardReaderAlt; 802 | }; 803 | name = Debug; 804 | }; 805 | 2CA26293262ECCFC00B5FD6D /* Release */ = { 806 | isa = XCBuildConfiguration; 807 | buildSettings = { 808 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 809 | CODE_SIGN_STYLE = Automatic; 810 | INFOPLIST_FILE = CreditCardReaderAltUITests/Info.plist; 811 | LD_RUNPATH_SEARCH_PATHS = ( 812 | "$(inherited)", 813 | "@executable_path/Frameworks", 814 | "@loader_path/Frameworks", 815 | ); 816 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderAltUITests; 817 | PRODUCT_NAME = "$(TARGET_NAME)"; 818 | SWIFT_VERSION = 5.0; 819 | TARGETED_DEVICE_FAMILY = "1,2"; 820 | TEST_TARGET_NAME = CreditCardReaderAlt; 821 | }; 822 | name = Release; 823 | }; 824 | 2CC3376C262D221F003E8FDB /* Debug */ = { 825 | isa = XCBuildConfiguration; 826 | buildSettings = { 827 | ALWAYS_SEARCH_USER_PATHS = NO; 828 | CLANG_ANALYZER_NONNULL = YES; 829 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 830 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 831 | CLANG_CXX_LIBRARY = "libc++"; 832 | CLANG_ENABLE_MODULES = YES; 833 | CLANG_ENABLE_OBJC_ARC = YES; 834 | CLANG_ENABLE_OBJC_WEAK = YES; 835 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 836 | CLANG_WARN_BOOL_CONVERSION = YES; 837 | CLANG_WARN_COMMA = YES; 838 | CLANG_WARN_CONSTANT_CONVERSION = YES; 839 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 840 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 841 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 842 | CLANG_WARN_EMPTY_BODY = YES; 843 | CLANG_WARN_ENUM_CONVERSION = YES; 844 | CLANG_WARN_INFINITE_RECURSION = YES; 845 | CLANG_WARN_INT_CONVERSION = YES; 846 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 847 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 848 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 849 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 850 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 851 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 852 | CLANG_WARN_STRICT_PROTOTYPES = YES; 853 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 854 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 855 | CLANG_WARN_UNREACHABLE_CODE = YES; 856 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 857 | COPY_PHASE_STRIP = NO; 858 | DEBUG_INFORMATION_FORMAT = dwarf; 859 | ENABLE_STRICT_OBJC_MSGSEND = YES; 860 | ENABLE_TESTABILITY = YES; 861 | GCC_C_LANGUAGE_STANDARD = gnu11; 862 | GCC_DYNAMIC_NO_PIC = NO; 863 | GCC_NO_COMMON_BLOCKS = YES; 864 | GCC_OPTIMIZATION_LEVEL = 0; 865 | GCC_PREPROCESSOR_DEFINITIONS = ( 866 | "DEBUG=1", 867 | "$(inherited)", 868 | ); 869 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 870 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 871 | GCC_WARN_UNDECLARED_SELECTOR = YES; 872 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 873 | GCC_WARN_UNUSED_FUNCTION = YES; 874 | GCC_WARN_UNUSED_VARIABLE = YES; 875 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 876 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 877 | MTL_FAST_MATH = YES; 878 | ONLY_ACTIVE_ARCH = YES; 879 | SDKROOT = iphoneos; 880 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 881 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 882 | }; 883 | name = Debug; 884 | }; 885 | 2CC3376D262D221F003E8FDB /* Release */ = { 886 | isa = XCBuildConfiguration; 887 | buildSettings = { 888 | ALWAYS_SEARCH_USER_PATHS = NO; 889 | CLANG_ANALYZER_NONNULL = YES; 890 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 891 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 892 | CLANG_CXX_LIBRARY = "libc++"; 893 | CLANG_ENABLE_MODULES = YES; 894 | CLANG_ENABLE_OBJC_ARC = YES; 895 | CLANG_ENABLE_OBJC_WEAK = YES; 896 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 897 | CLANG_WARN_BOOL_CONVERSION = YES; 898 | CLANG_WARN_COMMA = YES; 899 | CLANG_WARN_CONSTANT_CONVERSION = YES; 900 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 901 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 902 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 903 | CLANG_WARN_EMPTY_BODY = YES; 904 | CLANG_WARN_ENUM_CONVERSION = YES; 905 | CLANG_WARN_INFINITE_RECURSION = YES; 906 | CLANG_WARN_INT_CONVERSION = YES; 907 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 908 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 909 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 910 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 911 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 912 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 913 | CLANG_WARN_STRICT_PROTOTYPES = YES; 914 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 915 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 916 | CLANG_WARN_UNREACHABLE_CODE = YES; 917 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 918 | COPY_PHASE_STRIP = NO; 919 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 920 | ENABLE_NS_ASSERTIONS = NO; 921 | ENABLE_STRICT_OBJC_MSGSEND = YES; 922 | GCC_C_LANGUAGE_STANDARD = gnu11; 923 | GCC_NO_COMMON_BLOCKS = YES; 924 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 925 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 926 | GCC_WARN_UNDECLARED_SELECTOR = YES; 927 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 928 | GCC_WARN_UNUSED_FUNCTION = YES; 929 | GCC_WARN_UNUSED_VARIABLE = YES; 930 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 931 | MTL_ENABLE_DEBUG_INFO = NO; 932 | MTL_FAST_MATH = YES; 933 | SDKROOT = iphoneos; 934 | SWIFT_COMPILATION_MODE = wholemodule; 935 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 936 | VALIDATE_PRODUCT = YES; 937 | }; 938 | name = Release; 939 | }; 940 | 2CC3376F262D221F003E8FDB /* Debug */ = { 941 | isa = XCBuildConfiguration; 942 | buildSettings = { 943 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 944 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 945 | CODE_SIGN_STYLE = Automatic; 946 | DEVELOPMENT_ASSET_PATHS = "\"CreditCardReader/Preview Content\""; 947 | DEVELOPMENT_TEAM = A34HD644A5; 948 | ENABLE_PREVIEWS = YES; 949 | INFOPLIST_FILE = CreditCardReader/Info.plist; 950 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 951 | LD_RUNPATH_SEARCH_PATHS = ( 952 | "$(inherited)", 953 | "@executable_path/Frameworks", 954 | ); 955 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReader; 956 | PRODUCT_NAME = "$(TARGET_NAME)"; 957 | SWIFT_VERSION = 5.0; 958 | TARGETED_DEVICE_FAMILY = "1,2"; 959 | }; 960 | name = Debug; 961 | }; 962 | 2CC33770262D221F003E8FDB /* Release */ = { 963 | isa = XCBuildConfiguration; 964 | buildSettings = { 965 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 966 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 967 | CODE_SIGN_STYLE = Automatic; 968 | DEVELOPMENT_ASSET_PATHS = "\"CreditCardReader/Preview Content\""; 969 | DEVELOPMENT_TEAM = A34HD644A5; 970 | ENABLE_PREVIEWS = YES; 971 | INFOPLIST_FILE = CreditCardReader/Info.plist; 972 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 973 | LD_RUNPATH_SEARCH_PATHS = ( 974 | "$(inherited)", 975 | "@executable_path/Frameworks", 976 | ); 977 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReader; 978 | PRODUCT_NAME = "$(TARGET_NAME)"; 979 | SWIFT_VERSION = 5.0; 980 | TARGETED_DEVICE_FAMILY = "1,2"; 981 | }; 982 | name = Release; 983 | }; 984 | 2CC33772262D221F003E8FDB /* Debug */ = { 985 | isa = XCBuildConfiguration; 986 | buildSettings = { 987 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 988 | BUNDLE_LOADER = "$(TEST_HOST)"; 989 | CODE_SIGN_STYLE = Automatic; 990 | INFOPLIST_FILE = CreditCardReaderTests/Info.plist; 991 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 992 | LD_RUNPATH_SEARCH_PATHS = ( 993 | "$(inherited)", 994 | "@executable_path/Frameworks", 995 | "@loader_path/Frameworks", 996 | ); 997 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderTests; 998 | PRODUCT_NAME = "$(TARGET_NAME)"; 999 | SWIFT_VERSION = 5.0; 1000 | TARGETED_DEVICE_FAMILY = "1,2"; 1001 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CreditCardReader.app/CreditCardReader"; 1002 | }; 1003 | name = Debug; 1004 | }; 1005 | 2CC33773262D221F003E8FDB /* Release */ = { 1006 | isa = XCBuildConfiguration; 1007 | buildSettings = { 1008 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1009 | BUNDLE_LOADER = "$(TEST_HOST)"; 1010 | CODE_SIGN_STYLE = Automatic; 1011 | INFOPLIST_FILE = CreditCardReaderTests/Info.plist; 1012 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 1013 | LD_RUNPATH_SEARCH_PATHS = ( 1014 | "$(inherited)", 1015 | "@executable_path/Frameworks", 1016 | "@loader_path/Frameworks", 1017 | ); 1018 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderTests; 1019 | PRODUCT_NAME = "$(TARGET_NAME)"; 1020 | SWIFT_VERSION = 5.0; 1021 | TARGETED_DEVICE_FAMILY = "1,2"; 1022 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CreditCardReader.app/CreditCardReader"; 1023 | }; 1024 | name = Release; 1025 | }; 1026 | 2CC33775262D221F003E8FDB /* Debug */ = { 1027 | isa = XCBuildConfiguration; 1028 | buildSettings = { 1029 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1030 | CODE_SIGN_STYLE = Automatic; 1031 | INFOPLIST_FILE = CreditCardReaderUITests/Info.plist; 1032 | LD_RUNPATH_SEARCH_PATHS = ( 1033 | "$(inherited)", 1034 | "@executable_path/Frameworks", 1035 | "@loader_path/Frameworks", 1036 | ); 1037 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderUITests; 1038 | PRODUCT_NAME = "$(TARGET_NAME)"; 1039 | SWIFT_VERSION = 5.0; 1040 | TARGETED_DEVICE_FAMILY = "1,2"; 1041 | TEST_TARGET_NAME = CreditCardReader; 1042 | }; 1043 | name = Debug; 1044 | }; 1045 | 2CC33776262D221F003E8FDB /* Release */ = { 1046 | isa = XCBuildConfiguration; 1047 | buildSettings = { 1048 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 1049 | CODE_SIGN_STYLE = Automatic; 1050 | INFOPLIST_FILE = CreditCardReaderUITests/Info.plist; 1051 | LD_RUNPATH_SEARCH_PATHS = ( 1052 | "$(inherited)", 1053 | "@executable_path/Frameworks", 1054 | "@loader_path/Frameworks", 1055 | ); 1056 | PRODUCT_BUNDLE_IDENTIFIER = com.travel.rakuten.CreditCardReaderUITests; 1057 | PRODUCT_NAME = "$(TARGET_NAME)"; 1058 | SWIFT_VERSION = 5.0; 1059 | TARGETED_DEVICE_FAMILY = "1,2"; 1060 | TEST_TARGET_NAME = CreditCardReader; 1061 | }; 1062 | name = Release; 1063 | }; 1064 | /* End XCBuildConfiguration section */ 1065 | 1066 | /* Begin XCConfigurationList section */ 1067 | 2CA2628B262ECCFC00B5FD6D /* Build configuration list for PBXNativeTarget "CreditCardReaderAlt" */ = { 1068 | isa = XCConfigurationList; 1069 | buildConfigurations = ( 1070 | 2CA2628C262ECCFC00B5FD6D /* Debug */, 1071 | 2CA2628D262ECCFC00B5FD6D /* Release */, 1072 | ); 1073 | defaultConfigurationIsVisible = 0; 1074 | defaultConfigurationName = Release; 1075 | }; 1076 | 2CA2628E262ECCFC00B5FD6D /* Build configuration list for PBXNativeTarget "CreditCardReaderAltTests" */ = { 1077 | isa = XCConfigurationList; 1078 | buildConfigurations = ( 1079 | 2CA2628F262ECCFC00B5FD6D /* Debug */, 1080 | 2CA26290262ECCFC00B5FD6D /* Release */, 1081 | ); 1082 | defaultConfigurationIsVisible = 0; 1083 | defaultConfigurationName = Release; 1084 | }; 1085 | 2CA26291262ECCFC00B5FD6D /* Build configuration list for PBXNativeTarget "CreditCardReaderAltUITests" */ = { 1086 | isa = XCConfigurationList; 1087 | buildConfigurations = ( 1088 | 2CA26292262ECCFC00B5FD6D /* Debug */, 1089 | 2CA26293262ECCFC00B5FD6D /* Release */, 1090 | ); 1091 | defaultConfigurationIsVisible = 0; 1092 | defaultConfigurationName = Release; 1093 | }; 1094 | 2CC33744262D221C003E8FDB /* Build configuration list for PBXProject "CreditCardReader" */ = { 1095 | isa = XCConfigurationList; 1096 | buildConfigurations = ( 1097 | 2CC3376C262D221F003E8FDB /* Debug */, 1098 | 2CC3376D262D221F003E8FDB /* Release */, 1099 | ); 1100 | defaultConfigurationIsVisible = 0; 1101 | defaultConfigurationName = Release; 1102 | }; 1103 | 2CC3376E262D221F003E8FDB /* Build configuration list for PBXNativeTarget "CreditCardReader" */ = { 1104 | isa = XCConfigurationList; 1105 | buildConfigurations = ( 1106 | 2CC3376F262D221F003E8FDB /* Debug */, 1107 | 2CC33770262D221F003E8FDB /* Release */, 1108 | ); 1109 | defaultConfigurationIsVisible = 0; 1110 | defaultConfigurationName = Release; 1111 | }; 1112 | 2CC33771262D221F003E8FDB /* Build configuration list for PBXNativeTarget "CreditCardReaderTests" */ = { 1113 | isa = XCConfigurationList; 1114 | buildConfigurations = ( 1115 | 2CC33772262D221F003E8FDB /* Debug */, 1116 | 2CC33773262D221F003E8FDB /* Release */, 1117 | ); 1118 | defaultConfigurationIsVisible = 0; 1119 | defaultConfigurationName = Release; 1120 | }; 1121 | 2CC33774262D221F003E8FDB /* Build configuration list for PBXNativeTarget "CreditCardReaderUITests" */ = { 1122 | isa = XCConfigurationList; 1123 | buildConfigurations = ( 1124 | 2CC33775262D221F003E8FDB /* Debug */, 1125 | 2CC33776262D221F003E8FDB /* Release */, 1126 | ); 1127 | defaultConfigurationIsVisible = 0; 1128 | defaultConfigurationName = Release; 1129 | }; 1130 | /* End XCConfigurationList section */ 1131 | 1132 | /* Begin XCRemoteSwiftPackageReference section */ 1133 | 2CA261DC262EAAA400B5FD6D /* XCRemoteSwiftPackageReference "AltSwiftUI" */ = { 1134 | isa = XCRemoteSwiftPackageReference; 1135 | repositoryURL = "https://github.com/rakutentech/AltSwiftUI"; 1136 | requirement = { 1137 | branch = master; 1138 | kind = branch; 1139 | }; 1140 | }; 1141 | /* End XCRemoteSwiftPackageReference section */ 1142 | 1143 | /* Begin XCSwiftPackageProductDependency section */ 1144 | 2CA262C1262ECD7B00B5FD6D /* AltSwiftUI */ = { 1145 | isa = XCSwiftPackageProductDependency; 1146 | package = 2CA261DC262EAAA400B5FD6D /* XCRemoteSwiftPackageReference "AltSwiftUI" */; 1147 | productName = AltSwiftUI; 1148 | }; 1149 | /* End XCSwiftPackageProductDependency section */ 1150 | }; 1151 | rootObject = 2CC33741262D221C003E8FDB /* Project object */; 1152 | } 1153 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "AltSwiftUI", 6 | "repositoryURL": "https://github.com/rakutentech/AltSwiftUI", 7 | "state": { 8 | "branch": "master", 9 | "revision": "18e72bd88989839f6468d9a16fae7950350faa43", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @State private var showCardReader = false 12 | @State private var showCardReaderNoRetry = false 13 | @State private var showCardReaderCustom = false 14 | @State private var showUIKitCardReader = false 15 | @State private var creditCard: CreditCard? 16 | 17 | var body: some View { 18 | VStack { 19 | Button("Read Card") { 20 | showCardReader = true 21 | } 22 | .sheet(isPresented: $showCardReader) { 23 | CreditCardReaderView { card, _ in 24 | creditCard = card 25 | } 26 | } 27 | 28 | Button("Read Card No Retry") { 29 | showCardReaderNoRetry = true 30 | } 31 | .sheet(isPresented: $showCardReaderNoRetry) { 32 | CreditCardReaderView(defaultUIControls: .init(isRetryEnabled: false)) { card, _ in 33 | creditCard = card 34 | } 35 | } 36 | 37 | Button("Read Card Custom") { 38 | showCardReaderCustom = true 39 | } 40 | .sheet(isPresented: $showCardReaderCustom) { 41 | ZStack(alignment: .top) { 42 | CreditCardReaderView( 43 | defaultNavigationBar: nil, 44 | defaultUIControls: nil) { card, _ in 45 | creditCard = card 46 | } 47 | 48 | Button("Custom Close") { 49 | showCardReaderCustom = false 50 | } 51 | } 52 | } 53 | 54 | Button("Read UIKit Card") { 55 | showUIKitCardReader = true 56 | } 57 | .sheet(isPresented: $showUIKitCardReader) { 58 | CreditCardReaderViewControllerRep { card in 59 | creditCard = card 60 | } onClose: { 61 | showUIKitCardReader = false 62 | } 63 | .edgesIgnoringSafeArea(.bottom) 64 | } 65 | 66 | if let creditCard = creditCard { 67 | Text("\(creditCard.cardNumberDisplayString)\n\(creditCard.expirationDateDisplayString ?? "")") 68 | } 69 | } 70 | } 71 | } 72 | 73 | @available(iOS 13, *) 74 | struct CreditCardReaderViewControllerRep: UIViewControllerRepresentable { 75 | var onSuccess: (CreditCard) -> Void 76 | var onClose: () -> Void 77 | 78 | func makeUIViewController(context: Self.Context) -> CreditCardReaderViewController { 79 | CreditCardReaderViewController { card, _ in 80 | onSuccess(card) 81 | } onControllerClosed: { 82 | onClose() 83 | } 84 | } 85 | func updateUIViewController(_ uiViewController: CreditCardReaderViewController, context: Context) { 86 | } 87 | } 88 | 89 | struct ContentView_Previews: PreviewProvider { 90 | static var previews: some View { 91 | ContentView() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/CreditCardReaderApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderApp.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct CreditCardReaderApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | Camera will be used to read your credit card 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UIApplicationSupportsIndirectInputEvents 31 | 32 | UILaunchScreen 33 | 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReader/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CreditCardReaderAlt 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | func application( 14 | _ application: UIApplication, 15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application( 23 | _ application: UIApplication, 24 | configurationForConnecting connectingSceneSession: UISceneSession, 25 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 26 | // Called when a new scene session is being created. 27 | // Use this method to select a configuration to create the new scene with. 28 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 29 | } 30 | 31 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 32 | // Called when the user discards a scene session. 33 | // If any sessions were discarded while the application was not running, 34 | // this will be called shortly after application:didFinishLaunchingWithOptions. 35 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CreditCardReaderAlt 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import AltSwiftUI 9 | 10 | struct ContentView: View { 11 | var viewStore = ViewValues() 12 | 13 | @State private var showCardReader = false 14 | @State private var creditCard: CreditCard? 15 | 16 | var body: View { 17 | VStack { 18 | Button("Read Card") { 19 | showCardReader = true 20 | } 21 | if let creditCard = creditCard { 22 | Text("\(creditCard.cardNumberDisplayString)\n\(creditCard.expirationDateDisplayString ?? "")") 23 | } 24 | } 25 | .sheet(isPresented: $showCardReader) { 26 | CreditCardReadView { card, _ in 27 | creditCard = card 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | Camera will be used to read your credit card 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAlt/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CreditCardReaderAlt 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import UIKit 9 | import AltSwiftUI 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func scene( 16 | _ scene: UIScene, 17 | willConnectTo session: UISceneSession, 18 | options connectionOptions: UIScene.ConnectionOptions) { 19 | // Use this method to optionally configure and attach the UIWindow ` 20 | // window` to the provided UIWindowScene `scene`. 21 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 22 | // This delegate does not imply the connecting scene or session are new 23 | // (see `application:configurationForConnectingSceneSession` instead). 24 | 25 | // Create the SwiftUI view that provides the window contents. 26 | let contentView = ContentView() 27 | 28 | // Use a UIHostingController as window root view controller. 29 | if let windowScene = scene as? UIWindowScene { 30 | let window = UIWindow(windowScene: windowScene) 31 | window.rootViewController = UIHostingController(rootView: contentView) 32 | self.window = window 33 | window.makeKeyAndVisible() 34 | } 35 | } 36 | 37 | func sceneDidDisconnect(_ scene: UIScene) { 38 | // Called as the scene is being released by the system. 39 | // This occurs shortly after the scene enters the background, or when its session is discarded. 40 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 41 | // The scene may re-connect later, as its session was not necessarily discarded 42 | // (see `application:didDiscardSceneSessions` instead). 43 | } 44 | 45 | func sceneDidBecomeActive(_ scene: UIScene) { 46 | // Called when the scene has moved from an inactive state to an active state. 47 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 48 | } 49 | 50 | func sceneWillResignActive(_ scene: UIScene) { 51 | // Called when the scene will move from an active state to an inactive state. 52 | // This may occur due to temporary interruptions (ex. an incoming phone call). 53 | } 54 | 55 | func sceneWillEnterForeground(_ scene: UIScene) { 56 | // Called as the scene transitions from the background to the foreground. 57 | // Use this method to undo the changes made on entering the background. 58 | } 59 | 60 | func sceneDidEnterBackground(_ scene: UIScene) { 61 | // Called as the scene transitions from the foreground to the background. 62 | // Use this method to save data, release shared resources, and store enough scene-specific state information 63 | // to restore the scene back to its current state. 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAltTests/CreditCardReaderAltTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderAltTests.swift 3 | // CreditCardReaderAltTests 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import XCTest 9 | @testable import CreditCardReaderAlt 10 | 11 | class CreditCardReaderAltTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAltTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAltUITests/CreditCardReaderAltUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderAltUITests.swift 3 | // CreditCardReaderAltUITests 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import XCTest 9 | 10 | class CreditCardReaderAltUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface 19 | // orientation - required for your tests before they run. The setUp 20 | // method is a good place to do this. 21 | } 22 | 23 | override func tearDownWithError() throws { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | func testExample() throws { 28 | // UI tests must launch the application that they test. 29 | let app = XCUIApplication() 30 | app.launch() 31 | 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | func testLaunchPerformance() throws { 37 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 38 | // This measures how long it takes to launch your application. 39 | measure(metrics: [XCTApplicationLaunchMetric()]) { 40 | XCUIApplication().launch() 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderAltUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderTests/CreditCardReaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderTests.swift 3 | // CreditCardReaderTests 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import XCTest 9 | @testable import CreditCardReader 10 | 11 | class CreditCardReaderTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderUITests/CreditCardReaderUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderUITests.swift 3 | // CreditCardReaderUITests 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import XCTest 9 | 10 | class CreditCardReaderUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface 19 | // orientation - required for your tests before they run. 20 | // The setUp method is a good place to do this. 21 | } 22 | 23 | override func tearDownWithError() throws { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | func testExample() throws { 28 | // UI tests must launch the application that they test. 29 | let app = XCUIApplication() 30 | app.launch() 31 | 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | func testLaunchPerformance() throws { 37 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 38 | // This measures how long it takes to launch your application. 39 | measure(metrics: [XCTApplicationLaunchMetric()]) { 40 | XCUIApplication().launch() 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CreditCardReader/CreditCardReaderUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Rakuten, Inc. (https://global.rakuten.com/corp/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "AltSwiftUI", 6 | "repositoryURL": "https://github.com/rakutentech/AltSwiftUI", 7 | "state": { 8 | "branch": null, 9 | "revision": "18e72bd88989839f6468d9a16fae7950350faa43", 10 | "version": "1.5.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CreditCardReader", 8 | platforms: [ 9 | .iOS(.v11) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "CreditCardReader-SwiftUI", 15 | targets: ["CreditCardReader-View", "CreditCardReader-Model", "CreditCardReader-SwiftUI"]), 16 | .library( 17 | name: "CreditCardReader-AltSwiftUI", 18 | targets: ["CreditCardReader-View", "CreditCardReader-Model", "CreditCardReader-AltSwiftUI"]), 19 | .library( 20 | name: "CreditCardReader-UIKit", 21 | targets: ["CreditCardReader-View", "CreditCardReader-Model", "CreditCardReader-UIKit"]) 22 | ], 23 | dependencies: [ 24 | // Dependencies declare other packages that this package depends on. 25 | // .package(url: /* package url */, from: "1.0.0"), 26 | .package(url: "https://github.com/rakutentech/AltSwiftUI", from: "1.5.0"), 27 | ], 28 | targets: [ 29 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 30 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 31 | .target( 32 | name: "CreditCardReader-View", 33 | dependencies: [], 34 | path: "Sources/Views"), 35 | .target( 36 | name: "CreditCardReader-Model", 37 | dependencies: [], 38 | path: "Sources/Model"), 39 | .target( 40 | name: "CreditCardReader-SwiftUI", 41 | dependencies: [], 42 | path: "Sources/SwiftUI"), 43 | .target( 44 | name: "CreditCardReader-AltSwiftUI", 45 | dependencies: ["AltSwiftUI"], 46 | path: "Sources/AltSwiftUI"), 47 | .target( 48 | name: "CreditCardReader-UIKit", 49 | dependencies: [], 50 | path: "Sources/UIKit") 51 | ], 52 | swiftLanguageVersions: [SwiftVersion.v5] 53 | ) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Credit Card Reader 2 | 3 | [![Build](https://github.com/rakutentech/iOS-CreditCardReader/actions/workflows/build.yml/badge.svg)](https://github.com/rakutentech/iOS-CreditCardReader/actions/workflows/build.yml) 4 | [![Pod](https://img.shields.io/cocoapods/v/CreditCardReader.svg)](https://cocoapods.org/pods/CreditCardReader) 5 | ![Swift Package Manager](https://img.shields.io/badge/Swift%20Package%20Manager-OK-brightgreen.svg) 6 | ![SwiftUI](https://img.shields.io/badge/SwiftUI-OK-brightgreen.svg) 7 | ![AltSwiftUI](https://img.shields.io/badge/AltSwiftUI-OK-brightgreen.svg) 8 | ![iOS](https://img.shields.io/badge/iOS-install_11.0+-blue.svg) 9 | ![iOS](https://img.shields.io/badge/iOS-use_13.0+-blue.svg) 10 | 11 | Camera based library for SwiftUI, AltSwiftUI and UIKit with lightweight and accurate credit card information detection, and fully customizable UI controls. 12 | 13 | ![logo](https://raw.githubusercontent.com/rakutentech/iOS-CreditCardReader/master/docResources/CreditCardReader1.png) 14 | ![logo](https://raw.githubusercontent.com/rakutentech/iOS-CreditCardReader/master/docResources/CreditCardReader2.png) 15 | 16 | - [Features](#features) 17 | - [Supported Version](#supported-version) 18 | - [Installation](#installation) 19 | - [Sample Usage](#sample-usage) 20 | - [Get Involved](#get-involved) 21 | - [License](#license) 22 | 23 | ## Features 24 | 25 | - Captures through image analysis: 26 | - Traditional credit card number 27 | - [Visa Quick Read](https://usa.visa.com/dam/VCOM/download/merchants/New_VBM_Acq_Merchant_62714_v5.pdf) card number 28 | - Card expiration date 29 | - Can discern correct information when card contains multiple dates and numbers 30 | - UI controls are completely customizable 31 | - Retry capture function before confirmation 32 | - Provides SwiftUI, AltSwiftUI and UIKit interfaces 33 | 34 | ## Supported Version 35 | 36 | - Use version: iOS 13 37 | - Min. deployment version: iOS 11. 38 | _Note_: You can import into iOS 11 projects, but can only use in iOS13+ devices. 39 | 40 | ## Installation 41 | 42 | Installation can be done by either Swift Package Manager or Cocoa Pods. Depending on the UI framework you wish to support, you must select the specific dependency. __Warning__: Installing just the base 'CreditCardReader' pod won't produce a usable library. 43 | 44 | ### SwiftUI 45 | 46 | ```ruby 47 | pod 'CreditCardReader/SwiftUI' 48 | ``` 49 | SPM: CreditCardReader-SwiftUI 50 | 51 | ### AltSwiftUI 52 | 53 | ```ruby 54 | pod 'CreditCardReader/AltSwiftUI' 55 | ``` 56 | SPM: CreditCardReader-AltSwiftUI 57 | 58 | ### UIKit 59 | 60 | ```ruby 61 | pod 'CreditCardReader/UIKit' 62 | ``` 63 | SPM: CreditCardReader-UIKit 64 | 65 | ## Sample Usage 66 | 67 | Using the credit card reader view is straightforward. You can add it directly to your view hierarchy: 68 | 69 | ```swift 70 | var body: some View { 71 | ... 72 | CreditCardReaderView { card, _ in 73 | // Do something with the card 74 | } 75 | ... 76 | } 77 | ``` 78 | 79 | Or present it in as modal: 80 | 81 | ```swift 82 | var body: some View { 83 | ... 84 | MyView() 85 | .sheet(isPresented: $showCardReader) { 86 | CreditCardReaderView { card, _ in 87 | // Do something with the card 88 | } 89 | } 90 | ... 91 | } 92 | ``` 93 | 94 | ### Customizing Navigation and UI Controls 95 | 96 | Basic Customization 97 | 98 | ```swift 99 | CreditCardReaderView( 100 | defaultNavigationBar: .init( 101 | titleText: "Read Card", 102 | closeText: "Close"), 103 | defaultUIControls: .init( 104 | instructionsText: "Align your card with the camera", 105 | isRetryEnabled: false) 106 | ) { card, _ in 107 | // Do something with the card 108 | } 109 | ``` 110 | 111 | Full Customization 112 | 113 | ```swift 114 | ZStack { 115 | CreditCardReaderView( 116 | defaultNavigationBar: nil, 117 | defaultUIControls: nil 118 | ) { card, retry in 119 | // Do something with the card 120 | // or call retry if your UI supports retry 121 | } 122 | MyOverlayView() 123 | } 124 | ``` 125 | 126 | ### AltSwiftUI 127 | 128 | In AltSwiftUI, the reader view is named `CreditCardReadView`, in order to prevent Cocoa Pods submission conflicts. 129 | 130 | ```swift 131 | CreditCardReadView { card, _ in 132 | // Do something with the card 133 | } 134 | ``` 135 | 136 | ### UIKit 137 | 138 | When using the UIKit interface, you'd instantiate the card controller this way: 139 | 140 | ```swift 141 | CreditCardReaderViewController { card, _ in 142 | // Do something with the card 143 | } onControllerClosed: { 144 | // Close controller if pushed or presented 145 | } 146 | ``` 147 | 148 | Likewise, you have access to all UI customization options through the initializer. 149 | 150 | ## Get Involved 151 | 152 | ### Code Structure 153 | 154 | Image text recognition is handled by Vision and AVFoundation frameworks. 155 | The core components for detection in dependency order are as follows: 156 | 157 | ``` 158 | CardCaptureView -> CreditCardImageAnalyzer 159 | ``` 160 | 161 | Each supported UI framework has a separate public interface with its own UI. In general, the dependency flows are the following: 162 | 163 | ``` 164 | CreditCardReaderView (SwiftUI target) -> CardCaptureView -> CreditCardImageAnalyzer 165 | ``` 166 | ``` 167 | CreditCardReaderView (AltSwiftUI target) -> CardCaptureView -> CreditCardImageAnalyzer 168 | ``` 169 | 170 | ### Contributing 171 | 172 | If you find any issues or ideas of new features/improvements, you can submit an issue in GitHub. 173 | 174 | We also welcome you to contribute by submitting a pull request. 175 | 176 | For more information, see [CONTRIBUTING](https://github.com/rakutentech/iOS-CreditCardReader/blob/master/CONTRIBUTING.md). 177 | 178 | ## License 179 | 180 | MIT license. You can read the [LICENSE](https://github.com/rakutentech/iOS-CreditCardReader/blob/master/LICENSE) for more details. 181 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/CreditCardReadView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReadView.swift 3 | // CreditCardReaderAlt 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import Foundation 9 | import AltSwiftUI 10 | 11 | /// View that contains a camera output view and reads credit card information. 12 | /// Use this to retrieve credit card information of a user selected result. 13 | @available(iOS 13, *) 14 | public struct CreditCardReadView: View { 15 | public var viewStore = ViewValues() 16 | 17 | public typealias Retry = () -> Void 18 | public typealias Success = (CreditCard, Retry?) -> Void 19 | public typealias Failure = (Error) -> Void 20 | 21 | var defaultNavigationBar: CreditCardReaderDefaultNavigationBar? 22 | var defaultUIControls: CreditCardReaderDefaultControls? 23 | var onSuccess: Success 24 | var onFailure: Failure? 25 | 26 | @Environment(\.presentationMode) private var presentationMode 27 | @State private var cardController = CardCaptureController() 28 | @State private var creditCard: CreditCard? 29 | @State private var retryCapture: Retry? 30 | 31 | /// Initializes a `CreditCardReaderView` with customizable controls. 32 | /// 33 | /// Navigation and Card Selection 34 | /// Controls can be customized by specifying `defaultNavigationBar` and 35 | /// `defaultUIControls` parameters. The customization type can be on of the 36 | /// following: 37 | /// - Completely customized: Pass `nil` 38 | /// - Content customized: Pass an instance with your configuration 39 | /// - Default: Use the default values 40 | /// 41 | /// - Parameters: 42 | /// - defaultNavigationBar: Configuration for the navigation bar 43 | /// - defaultUIControls: Configuration for Credit Card Selectino Controls 44 | /// - onSuccess: Called when a card has been selected by the user, or as soon 45 | /// as a card is recognized if `defaultUIControls` is `nil` or `defaultUIControls.isRetryEnabled` is `false.` 46 | /// - onFailure: Called when there was an error initializing the view 47 | public init( 48 | defaultNavigationBar: CreditCardReaderDefaultNavigationBar? = CreditCardReaderDefaultNavigationBar(), 49 | defaultUIControls: CreditCardReaderDefaultControls? = CreditCardReaderDefaultControls(), 50 | onSuccess: @escaping Success, 51 | onFailure: Failure? = nil) { 52 | self.defaultNavigationBar = defaultNavigationBar 53 | self.defaultUIControls = defaultUIControls 54 | self.onSuccess = onSuccess 55 | self.onFailure = onFailure 56 | } 57 | } 58 | 59 | @available(iOS 13, *) 60 | extension CreditCardReadView { 61 | public var body: View { 62 | ZStack { 63 | AltCardCaptureRepresentableView( 64 | cardController: cardController, 65 | onSuccess: { card, retry in 66 | if let defaultUIControls = defaultUIControls { 67 | retryCapture = retry 68 | creditCard = card 69 | if !defaultUIControls.isRetryEnabled { 70 | DispatchQueue.main.asyncAfter(deadline: .now() + Double.cardReaderCloseTimeDelay) { 71 | onSuccess(card, retry) 72 | if defaultUIControls.navigatesBackOnDetection { 73 | presentationMode.wrappedValue.dismiss() 74 | } 75 | } 76 | } 77 | } else { 78 | onSuccess(card, retry) 79 | } 80 | }, 81 | onFailure: onFailure) 82 | .background(Color.black) 83 | .edgesIgnoringSafeArea(.all) 84 | .onAppear { 85 | // Delay camera start to prevent frozen frames while 86 | // the view is opening 87 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 88 | cardController.startCapture() 89 | } 90 | } 91 | .onDisappear { 92 | cardController.stopCapture() 93 | } 94 | 95 | if let defaultUIControls = defaultUIControls { 96 | controlsOverlayView(defaultUIControls: defaultUIControls) 97 | } 98 | } 99 | } 100 | 101 | private func controlsOverlayView(defaultUIControls: CreditCardReaderDefaultControls) -> View { 102 | VStack(spacing: 0) { 103 | if let defaultNavigationBar = defaultNavigationBar { 104 | topControlsView(defaultNavigationBar: defaultNavigationBar) 105 | } 106 | Spacer() 107 | ZStack(alignment: .bottom) { 108 | Text(defaultUIControls.instructionsText) 109 | .foregroundColor(.white) 110 | .shadow(radius: 2) 111 | .padding(.bottom, 40) 112 | .padding(.horizontal, 16) 113 | .frame(maxWidth: .infinity) 114 | .background(Color.black) 115 | if let creditCard = creditCard { 116 | bottomControlsView(creditCard: creditCard, defaultUIControls: defaultUIControls) 117 | } 118 | } 119 | } 120 | } 121 | 122 | private func topControlsView(defaultNavigationBar: CreditCardReaderDefaultNavigationBar) -> View { 123 | HStack() { 124 | Button(defaultNavigationBar.closeText) { 125 | presentationMode.wrappedValue.dismiss() 126 | } 127 | .accentColor(.white) 128 | .frame(width: 80) 129 | 130 | Text(defaultNavigationBar.titleText) 131 | .font(.headline) 132 | .foregroundColor(.white) 133 | .lineLimit(1) 134 | .minimumScaleFactor(0.75) 135 | .frame(maxWidth: .infinity) 136 | 137 | Text("") 138 | .frame(width: 80) 139 | } 140 | .frame(height: 44) 141 | } 142 | 143 | private func bottomControlsView(creditCard: CreditCard, defaultUIControls: CreditCardReaderDefaultControls) -> View { 144 | VStack(spacing: 0) { 145 | Text(verbatim: creditCard.cardNumberDisplayString) 146 | .font(.title2) 147 | .foregroundColor(.white) 148 | 149 | if let expirationDate = creditCard.expirationDateDisplayString { 150 | Text(verbatim: expirationDate) 151 | .font(.title3) 152 | .foregroundColor(.white) 153 | .padding(.top, 4) 154 | } 155 | if defaultUIControls.isRetryEnabled { 156 | HStack(spacing: .uiControlButtonSpacing) { 157 | Button(action: { 158 | withAnimation { 159 | self.creditCard = nil 160 | } 161 | retryCapture?() 162 | retryCapture = nil 163 | }, label: { 164 | Text(defaultUIControls.retryText) 165 | .foregroundColor(.white) 166 | .padding(.horizontal, 10) 167 | .frame(height: .uiControlButtonHeight) 168 | .frame(minWidth: .uiControlButtonMinWidth) 169 | .cornerRadius(.uiControlButtonHeight / 2) 170 | .border(.white, width: 1) 171 | }) 172 | 173 | Button(action: { 174 | onSuccess(creditCard, nil) 175 | if defaultUIControls.navigatesBackOnDetection { 176 | presentationMode.wrappedValue.dismiss() 177 | } 178 | }, label: { 179 | Text(defaultUIControls.confirmText) 180 | .font(.headline) 181 | .foregroundColor(.black) 182 | .padding(.horizontal, 10) 183 | .frame(height: .uiControlButtonHeight) 184 | .frame(minWidth: .uiControlButtonMinWidth) 185 | .background(Color.white) 186 | .cornerRadius(.uiControlButtonHeight / 2) 187 | }) 188 | } 189 | .padding(.top, .uiControlButtonSpacing) 190 | } 191 | Spacer() 192 | .frame(height: .uiControlButtonSpacing) 193 | } 194 | .padding(.top, 10) 195 | .frame(maxWidth: .infinity) 196 | .padding(.horizontal, 16) 197 | .background(Color.black) 198 | .transition(AnyTransition 199 | .opacity 200 | .combined(with: .offset(y: 30)) 201 | .animation(.easeInOut(duration: 0.2))) 202 | } 203 | } 204 | 205 | @available(iOS 13, *) 206 | struct AltCardCaptureRepresentableView: UIViewRepresentable { 207 | var viewStore = ViewValues() 208 | 209 | var cardController: CardCaptureController 210 | var onSuccess: (CreditCard, @escaping CardCaptureView.Retry) -> Void 211 | var onFailure: ((Error) -> Void)? 212 | 213 | func makeUIView(context: UIContext) -> CardCaptureView { 214 | let view = CardCaptureView(onSuccess: onSuccess, onFailure: onFailure) 215 | cardController.captureView = view 216 | return view 217 | } 218 | 219 | func updateUIView(_ uiView: CardCaptureView, context: UIContext) { 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Sources/Model/CreditCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCard.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A structure containing credit card information 11 | public struct CreditCard: Hashable { 12 | /// The number of the card 13 | public var cardNumber: String 14 | 15 | /// The year of expiration of the card 16 | public var expirationYear: Int? 17 | 18 | /// The month of expiration of the card 19 | public var expirationMonth: Int? 20 | 21 | public init(cardNumber: String, 22 | expirationYear: Int?, 23 | expirationMonth: Int?) { 24 | self.cardNumber = cardNumber 25 | self.expirationYear = expirationYear 26 | self.expirationMonth = expirationMonth 27 | } 28 | 29 | /// Returns the year of expiration as a 2 digit string 30 | public var expirationYearString: String? { 31 | guard let expirationYear = expirationYear else { 32 | return nil 33 | } 34 | return String("\(expirationYear)".suffix(2)) 35 | } 36 | 37 | /// Returns the year of expiration as a 4 digit string 38 | public var expirationYearStringFull: String? { 39 | guard let expirationYear = expirationYear else { 40 | return nil 41 | } 42 | return String("\(expirationYear)") 43 | } 44 | 45 | /// Returns the month of expiration as string. This string 46 | /// always has 2 digits. If a month number has less than 2 47 | /// digits, a `0` character will be prefixed. 48 | /// 49 | /// Output examples: 50 | /// - 09 // September 51 | /// - 12 // December 52 | public var expirationMonthString: String? { 53 | guard let expirationMonth = expirationMonth else { 54 | return nil 55 | } 56 | if expirationMonth > 9 { 57 | return "\(expirationMonth)" 58 | } else { 59 | return "0\(expirationMonth)" 60 | } 61 | } 62 | 63 | /// Returns the expiration date containing month and year in 64 | /// display format. 65 | /// 66 | /// Output examples: 67 | /// - 09/25 68 | /// - 12/25 69 | public var expirationDateDisplayString: String? { 70 | guard let yearString = expirationYearString, 71 | let monthString = expirationMonthString else { 72 | return nil 73 | } 74 | return "\(monthString)/\(yearString)" 75 | } 76 | 77 | /// Returns the card number formatted for display, with a space 78 | /// character every 4 digits 79 | public var cardNumberDisplayString: String { 80 | var string = cardNumber 81 | string.insert(" ", at: string.index(string.startIndex, offsetBy: 12)) 82 | string.insert(" ", at: string.index(string.startIndex, offsetBy: 8)) 83 | string.insert(" ", at: string.index(string.startIndex, offsetBy: 4)) 84 | return string 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Model/CreditCardImageAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardImageAnalyzer.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import Foundation 9 | import Vision 10 | import UIKit 11 | 12 | @available(iOS 13, *) 13 | class CreditCardImageAnalyzer { 14 | // MARK: - Pattern matching 15 | // All regex strings have an extra \ to escape the \ symbol 16 | 17 | private let cardNumberRegex: NSRegularExpression? = try? NSRegularExpression(pattern: "(?:\\d[ ]*?){13,16}") 18 | private let visaQuickReadNumberRegex: NSRegularExpression? = try? NSRegularExpression(pattern: "^[\\/\\[\\]\\|\\\\]?(\\d{4})") 19 | private let visaQuickReadBoundaryChars: Set = ["\\", "/", "[", "]", "|"] 20 | 21 | /// Regex for month/year with 1 capture group for each component 22 | private let expirationDateRegex: NSRegularExpression? = try? NSRegularExpression(pattern: "(0[1-9]|1[0-2])\\/(\\d{4}|\\d{2})") 23 | 24 | // MARK: - Functions 25 | 26 | /// Returns a credit card by reading the provided image. 27 | func analyze(image: CGImage, onSuccess: @escaping (CreditCard) -> Void, onFailure: ((Error?) -> Void)? = nil) { 28 | let request = VNRecognizeTextRequest { [weak self] request, error in 29 | self?.analyze(request: request, onSuccess: onSuccess, onFailure: onFailure) 30 | } 31 | request.recognitionLevel = .accurate 32 | let handler = VNImageRequestHandler(cgImage: image, 33 | orientation: .up, 34 | options: [:]) 35 | do { 36 | try handler.perform([request]) 37 | } catch { 38 | onFailure?(error) 39 | } 40 | } 41 | 42 | // MARK: - Private functions 43 | 44 | private func analyze(request: VNRequest, onSuccess: (CreditCard) -> Void, onFailure: ((Error?) -> Void)?) { 45 | guard let results = request.results as? [VNRecognizedTextObservation] else { 46 | onFailure?(nil) 47 | return 48 | } 49 | 50 | var cardNumber: String? 51 | var visaQuickReadNumbers = [String]() 52 | var cardExpirationDate: (month: Int, year: Int)? 53 | for result in results { 54 | guard let candidate = result.topCandidates(1).first, 55 | candidate.confidence > 0.3 else { 56 | continue 57 | } 58 | let recognizedText = candidate.string 59 | 60 | // Always match the first found card number 61 | if cardNumber == nil, 62 | let numberMatch = cardNumberMatch(recognizedText) { 63 | cardNumber = numberMatch 64 | } else if let visaQuickReadNumber = visaQuickReadNumberMatch(recognizedText) { 65 | visaQuickReadNumbers.append(visaQuickReadNumber) 66 | } else if let expirationDateMatch = cardExpirationDateMatch(recognizedText) { 67 | if cardExpirationDate != nil { 68 | // If card detects multiple dates (e.g. from and to) separately, 69 | // we don't return expiration date as we can't determine which one 70 | // is the expiration one. 71 | cardExpirationDate = nil 72 | } else { 73 | cardExpirationDate = expirationDateMatch 74 | } 75 | } 76 | } 77 | 78 | // First priority goes to detected traditional card number 79 | if let cardNumber = cardNumber { 80 | onSuccess(CreditCard( 81 | cardNumber: cardNumber, 82 | expirationYear: cardExpirationDate?.year, 83 | expirationMonth: cardExpirationDate?.month)) 84 | // Second priority goes to VISA quick read number 85 | } else if visaQuickReadNumbers.count == 4{ 86 | onSuccess(CreditCard( 87 | cardNumber: visaQuickReadNumbers.joined(), 88 | expirationYear: cardExpirationDate?.year, 89 | expirationMonth: cardExpirationDate?.month)) 90 | } else { 91 | onFailure?(nil) 92 | } 93 | } 94 | 95 | private func cardNumberMatch(_ text: String) -> String? { 96 | guard let expression = cardNumberRegex, 97 | let match = expression.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.count)), 98 | let range = Range(match.range, in: text) 99 | else { 100 | return nil 101 | } 102 | 103 | return String(text[range]) 104 | .replacingOccurrences(of: " ", with: "") 105 | } 106 | 107 | private func visaQuickReadNumberMatch(_ text: String) -> String? { 108 | let textRange = NSRange(location: 0, length: text.count) 109 | guard let expression = visaQuickReadNumberRegex, 110 | let match = expression.matches(in: text, options: [], range: textRange).last, 111 | // Range 0 is the whole text 112 | match.numberOfRanges == 2, 113 | // Get number from capture group 114 | let range = Range(match.range(at: 1), in: text) 115 | else { 116 | return nil 117 | } 118 | 119 | if text.count == 4 { 120 | return String(text[range]) 121 | } else { 122 | let firstIndex = text.index(text.startIndex, offsetBy: 0) 123 | let fifthIndex = text.index(text.startIndex, offsetBy: 4) 124 | 125 | if visaQuickReadBoundaryChars.contains(String(text[firstIndex])) { 126 | return String(text[range]) 127 | } else if visaQuickReadBoundaryChars.contains(String(text[fifthIndex])) { 128 | return String(text[range]) 129 | } else if text.count > 5 { 130 | let sixthIndex = text.index(text.startIndex, offsetBy: 5) 131 | if visaQuickReadBoundaryChars.contains(String(text[sixthIndex])) { 132 | return String(text[range]) 133 | } 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | private func cardExpirationDateMatch(_ text: String) -> (month: Int, year: Int)? { 141 | let textRange = NSRange(location: 0, length: text.count) 142 | guard let expression = expirationDateRegex, 143 | // If there are 2 dates, usually expire date is to the right 144 | let match = expression.matches(in: text, options: [], range: textRange).last, 145 | // Range 0 is the whole text 146 | match.numberOfRanges == 3, 147 | // First capture group 148 | let monthRange = Range(match.range(at: 1), in: text), 149 | // Second capture group 150 | let yearRange = Range(match.range(at: 2), in: text) 151 | else { 152 | return nil 153 | } 154 | 155 | let monthString = String(text[monthRange]) 156 | var yearString = String(text[yearRange]) 157 | if yearString.count == 2 { 158 | yearString = "20\(yearString)" 159 | } 160 | if let monthInt = Int(monthString), 161 | let yearInt = Int(yearString) { 162 | return (month: monthInt, year: yearInt) 163 | } 164 | 165 | return nil 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/Model/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | /// When the camera fails to initialize 12 | public struct CameraInitializationError: Error {} 13 | 14 | /// When permission to camera is denied or restricted 15 | public struct CameraPermissionError: Error { 16 | let authorizationStatus: AVAuthorizationStatus 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftUI/CreditCardReaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderView.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// View that contains a camera output view and reads credit card information. 11 | /// Use this to retrieve credit card information of a user selected result. 12 | @available(iOS 13, *) 13 | public struct CreditCardReaderView: View { 14 | public typealias Retry = () -> Void 15 | public typealias Success = (CreditCard, Retry?) -> Void 16 | public typealias Failure = (Error) -> Void 17 | 18 | var defaultNavigationBar: CreditCardReaderDefaultNavigationBar? 19 | var defaultUIControls: CreditCardReaderDefaultControls? 20 | var onSuccess: Success 21 | var onFailure: Failure? 22 | 23 | @Environment(\.presentationMode) var presentationMode 24 | @State private var cardController = CardCaptureController() 25 | @State private var creditCard: CreditCard? 26 | @State private var retryCapture: Retry? 27 | 28 | /// Initializes a `CreditCardReaderView` with customizable controls. 29 | /// 30 | /// Navigation and Card Selection 31 | /// Controls can be customized by specifying `defaultNavigationBar` and 32 | /// `defaultUIControls` parameters. The customization type can be on of the 33 | /// following: 34 | /// - Completely customized: Pass `nil` 35 | /// - Content customized: Pass an instance with your configuration 36 | /// - Default: Use the default values 37 | /// 38 | /// - Parameters: 39 | /// - defaultNavigationBar: Configuration for the navigation bar 40 | /// - defaultUIControls: Configuration for Credit Card Selectino Controls 41 | /// - onSuccess: Called when a card has been selected by the user, or as soon 42 | /// as a card is recognized if `defaultUIControls` is `nil` or `defaultUIControls.isRetryEnabled` is `false.` 43 | /// - onFailure: Called when there was an error initializing the view 44 | public init( 45 | defaultNavigationBar: CreditCardReaderDefaultNavigationBar? = CreditCardReaderDefaultNavigationBar(), 46 | defaultUIControls: CreditCardReaderDefaultControls? = CreditCardReaderDefaultControls(), 47 | onSuccess: @escaping Success, 48 | onFailure: Failure? = nil) { 49 | self.defaultNavigationBar = defaultNavigationBar 50 | self.defaultUIControls = defaultUIControls 51 | self.onSuccess = onSuccess 52 | self.onFailure = onFailure 53 | } 54 | } 55 | 56 | @available(iOS 13, *) 57 | extension CreditCardReaderView { 58 | public var body: some View { 59 | ZStack { 60 | CardCaptureRepresentableView( 61 | cardController: cardController, 62 | onSuccess: { card, retry in 63 | if let defaultUIControls = defaultUIControls { 64 | retryCapture = retry 65 | withAnimation(Animation.easeInOut(duration: 0.2)) { 66 | creditCard = card 67 | } 68 | if !defaultUIControls.isRetryEnabled { 69 | DispatchQueue.main.asyncAfter(deadline: .now() + Double.cardReaderCloseTimeDelay) { 70 | onSuccess(card, retry) 71 | if defaultUIControls.navigatesBackOnDetection { 72 | presentationMode.wrappedValue.dismiss() 73 | } 74 | } 75 | } 76 | } else { 77 | onSuccess(card, retry) 78 | } 79 | }, 80 | onFailure: onFailure) 81 | .background(Color.black) 82 | .edgesIgnoringSafeArea(.all) 83 | .onAppear { 84 | // Delay camera start to prevent frozen frames while 85 | // the view is opening 86 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 87 | cardController.startCapture() 88 | } 89 | } 90 | .onDisappear { 91 | cardController.stopCapture() 92 | } 93 | 94 | if let defaultUIControls = defaultUIControls { 95 | controlsOverlayView(defaultUIControls: defaultUIControls) 96 | } 97 | } 98 | } 99 | 100 | private func controlsOverlayView(defaultUIControls: CreditCardReaderDefaultControls) -> some View { 101 | VStack(spacing: 0) { 102 | if let defaultNavigationBar = defaultNavigationBar { 103 | topControlsView(defaultNavigationBar: defaultNavigationBar) 104 | } 105 | Spacer() 106 | if let creditCard = creditCard { 107 | bottomControlsView(creditCard: creditCard, defaultUIControls: defaultUIControls) 108 | .padding(.horizontal, 16) 109 | .frame(maxWidth: .infinity) 110 | .background(Color.black) 111 | } else { 112 | Text(defaultUIControls.instructionsText) 113 | .foregroundColor(.white) 114 | .shadow(radius: 2) 115 | .padding(.bottom, 40) 116 | .padding(.horizontal, 16) 117 | } 118 | } 119 | } 120 | 121 | private func topControlsView(defaultNavigationBar: CreditCardReaderDefaultNavigationBar) -> some View { 122 | HStack() { 123 | Button(defaultNavigationBar.closeText) { 124 | presentationMode.wrappedValue.dismiss() 125 | } 126 | .accentColor(.white) 127 | .frame(width: 80) 128 | 129 | Text(defaultNavigationBar.titleText) 130 | .font(.headline) 131 | .foregroundColor(.white) 132 | .lineLimit(1) 133 | .minimumScaleFactor(0.75) 134 | .frame(maxWidth: .infinity) 135 | 136 | Text("") 137 | .frame(width: 80) 138 | } 139 | .frame(height: 44) 140 | } 141 | 142 | private func bottomControlsView(creditCard: CreditCard, defaultUIControls: CreditCardReaderDefaultControls) -> some View { 143 | VStack(spacing: 0) { 144 | Text(verbatim: creditCard.cardNumberDisplayString) 145 | .font(.title2Font) 146 | .foregroundColor(.white) 147 | 148 | if let expirationDate = creditCard.expirationDateDisplayString { 149 | Text(verbatim: expirationDate) 150 | .font(.title3Font) 151 | .foregroundColor(.white) 152 | .padding(.top, 4) 153 | } 154 | if defaultUIControls.isRetryEnabled { 155 | HStack(spacing: .uiControlButtonSpacing) { 156 | Button(action: { 157 | withAnimation(Animation.easeInOut(duration: 0.2)) { 158 | self.creditCard = nil 159 | } 160 | retryCapture?() 161 | retryCapture = nil 162 | }, label: { 163 | Text(defaultUIControls.retryText) 164 | .foregroundColor(.white) 165 | .padding(.horizontal, 10) 166 | .frame(height: .uiControlButtonHeight) 167 | .frame(minWidth: .uiControlButtonMinWidth) 168 | .overlay( 169 | RoundedRectangle(cornerRadius: .uiControlButtonHeight / 2) 170 | .stroke(Color.white, lineWidth: 1) 171 | ) 172 | }) 173 | 174 | Button(action: { 175 | onSuccess(creditCard, nil) 176 | if defaultUIControls.navigatesBackOnDetection { 177 | presentationMode.wrappedValue.dismiss() 178 | } 179 | }, label: { 180 | Text(defaultUIControls.confirmText) 181 | .font(.headline) 182 | .foregroundColor(.black) 183 | .padding(.horizontal, 10) 184 | .frame(height: .uiControlButtonHeight) 185 | .frame(minWidth: .uiControlButtonMinWidth) 186 | .background(Color.white) 187 | .cornerRadius(.uiControlButtonHeight / 2) 188 | }) 189 | } 190 | .padding(.top, 20) 191 | } 192 | Spacer() 193 | .frame(height: .uiControlButtonSpacing) 194 | } 195 | .padding(.top, 10) 196 | .frame(maxWidth: .infinity) 197 | .background(Color.black) 198 | .transition(AnyTransition.opacity.combined(with: .offset(y: 30))) 199 | } 200 | } 201 | 202 | @available(iOS 13, *) 203 | struct CardCaptureRepresentableView: UIViewRepresentable { 204 | typealias UIViewType = CardCaptureView 205 | 206 | var cardController: CardCaptureController 207 | var onSuccess: (CreditCard, @escaping CardCaptureView.Retry) -> Void 208 | var onFailure: ((Error) -> Void)? 209 | 210 | func makeUIView(context: Context) -> CardCaptureView { 211 | let view = CardCaptureView(onSuccess: onSuccess, onFailure: onFailure) 212 | cardController.captureView = view 213 | return view 214 | } 215 | 216 | func updateUIView(_ uiView: CardCaptureView, context: Context) { 217 | } 218 | } 219 | 220 | @available(iOS 13, *) 221 | extension Font { 222 | static let title2Font = Font.system(size: 22) 223 | static let title3Font = Font.system(size: 20) 224 | } 225 | -------------------------------------------------------------------------------- /Sources/UIKit/CreditCardReaderViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardReaderViewController.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import UIKit 9 | 10 | /// View Controller that contains a camera output view and reads credit card information. 11 | /// Use this to retrieve credit card information of a user selected result. 12 | @available(iOS 13, *) 13 | public class CreditCardReaderViewController: UIViewController { 14 | public typealias Retry = () -> Void 15 | public typealias Success = (CreditCard, Retry?) -> Void 16 | public typealias Failure = (Error) -> Void 17 | 18 | // MARK: Properties 19 | 20 | var cardCaptureView: CardCaptureView? 21 | var onSuccess: Success? 22 | var onFailure: Failure? 23 | var onControllerClosed: (() -> Void)? 24 | var navigationBar: CreditCardReaderDefaultNavigationBar? = .init() 25 | var uiControls: CreditCardReaderDefaultControls? = .init() 26 | 27 | private var bottomOverlayView: UIView? 28 | private let bottomViewTransitionOffset: CGFloat = 30 29 | private var detectedCard: CreditCard? 30 | private var retry: Retry? 31 | private var cardNumberLabel: UILabel? 32 | private var expirationLabel: UILabel? 33 | 34 | // MARK: Init 35 | 36 | /// Initializes a `CreditCardReaderViewController` with customizable controls. 37 | /// 38 | /// Navigation and Card Selection 39 | /// Controls can be customized by specifying `defaultNavigationBar` and 40 | /// `defaultUIControls` parameters. The customization type can be on of the 41 | /// following: 42 | /// - Completely customized: Pass `nil` 43 | /// - Content customized: Pass an instance with your configuration 44 | /// - Default: Use the default values 45 | /// 46 | /// - Parameters: 47 | /// - defaultNavigationBar: Configuration for the navigation bar 48 | /// - defaultUIControls: Configuration for Credit Card Selectino Controls 49 | /// - onSuccess: Called when a card has been selected by the user, or as soon 50 | /// as a card is recognized if `defaultUIControls` is `nil` or `defaultUIControls.isRetryEnabled` is `false.` 51 | /// - onFailure: Called when there was an error initializing the view 52 | /// - onControllerClosed: Called when this controller requests to be closed. 53 | /// For example, this can happen when the 'Close' button is pressed if defining a 54 | /// default navigation bar. 55 | public init( 56 | defaultNavigationBar: CreditCardReaderDefaultNavigationBar? = .init(), 57 | defaultUIControls: CreditCardReaderDefaultControls? = .init(), 58 | onSuccess: @escaping Success, 59 | onFailure: Failure? = nil, 60 | onControllerClosed: (() -> Void)? = nil) { 61 | self.onSuccess = onSuccess 62 | self.onFailure = onFailure 63 | self.onControllerClosed = onControllerClosed 64 | self.navigationBar = defaultNavigationBar 65 | self.uiControls = defaultUIControls 66 | super.init(nibName: nil, bundle: nil) 67 | } 68 | required init?(coder: NSCoder) { 69 | super.init(coder: coder) 70 | } 71 | 72 | // MARK: Override 73 | 74 | public override func viewDidLoad() { 75 | super.viewDidLoad() 76 | 77 | view.backgroundColor = .black 78 | 79 | let cardCaptureView = CardCaptureView { [weak self] card, retry in 80 | guard let `self` = self else { return } 81 | if let uiControls = self.uiControls { 82 | self.detectedCard = card 83 | self.retry = retry 84 | self.showBottomOverlayView() 85 | if !uiControls.isRetryEnabled { 86 | DispatchQueue.main.asyncAfter(deadline: .now() + Double.cardReaderCloseTimeDelay) { 87 | self.onSuccess?(card, retry) 88 | self.closeView() 89 | } 90 | } 91 | } else { 92 | self.onSuccess?(card, retry) 93 | } 94 | } onFailure: { [weak self] error in 95 | self?.onFailure?(error) 96 | } 97 | cardCaptureView.translatesAutoresizingMaskIntoConstraints = false 98 | view.addSubview(cardCaptureView) 99 | cardCaptureView.edgesAnchorEqualTo(destinationView: view).activate() 100 | self.cardCaptureView = cardCaptureView 101 | 102 | if let topOverlayView = topOverlayView { 103 | view.addSubview(topOverlayView) 104 | [topOverlayView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 105 | topOverlayView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), 106 | topOverlayView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor)].activate() 107 | } 108 | 109 | if let uiControls = uiControls { 110 | let instructionLabel = UILabel().noAutoresizingMask() 111 | instructionLabel.textAlignment = .center 112 | instructionLabel.textColor = .white 113 | instructionLabel.text = uiControls.instructionsText 114 | instructionLabel.numberOfLines = 0 115 | instructionLabel.shadowColor = UIColor(white: 1, alpha: 0.3) 116 | view.addSubview(instructionLabel) 117 | [instructionLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 16), 118 | instructionLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -16), 119 | instructionLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40)].activate() 120 | 121 | let bottomOverlayView = bottomOverlayDefaultView(properties: uiControls) 122 | view.addSubview(bottomOverlayView) 123 | [bottomOverlayView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), 124 | bottomOverlayView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), 125 | bottomOverlayView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor)].activate() 126 | bottomOverlayView.transform = CGAffineTransform(translationX: 0, y: bottomViewTransitionOffset) 127 | bottomOverlayView.alpha = 0 128 | self.bottomOverlayView = bottomOverlayView 129 | } 130 | } 131 | 132 | public override func viewWillAppear(_ animated: Bool) { 133 | super.viewWillAppear(animated) 134 | 135 | // Delay camera start to prevent frozen frames while 136 | // the view is opening 137 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in 138 | self?.cardCaptureView?.startCapture() 139 | } 140 | } 141 | 142 | public override func viewWillDisappear(_ animated: Bool) { 143 | super.viewWillDisappear(animated) 144 | cardCaptureView?.stopCapture() 145 | } 146 | 147 | // MARK: Overlay Views 148 | 149 | var topOverlayView: UIView? { 150 | if let navigationBar = navigationBar { 151 | return topOverlayDefaultView(properties: navigationBar) 152 | } else { 153 | return nil 154 | } 155 | } 156 | 157 | func topOverlayDefaultView(properties: CreditCardReaderDefaultNavigationBar) -> UIView { 158 | let topView = UIView(frame: .zero).noAutoresizingMask() 159 | 160 | let closeButton = UIButton(frame: .zero).noAutoresizingMask() 161 | closeButton.setTitle(properties.closeText, for: .normal) 162 | closeButton.addTarget(self, action: #selector(closeView), for: .touchUpInside) 163 | closeButton.setTitleColor(.white, for: .normal) 164 | topView.addSubview(closeButton) 165 | [closeButton.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 10), 166 | closeButton.heightAnchor.constraint(equalToConstant: 44), 167 | closeButton.topAnchor.constraint(equalTo: topView.topAnchor), 168 | closeButton.bottomAnchor.constraint(equalTo: topView.bottomAnchor)].activate() 169 | 170 | let titleLabel = UILabel(frame: .zero).noAutoresizingMask() 171 | titleLabel.text = properties.titleText 172 | titleLabel.textColor = .white 173 | titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) 174 | topView.addSubview(titleLabel) 175 | [titleLabel.centerYAnchor.constraint(equalTo: topView.centerYAnchor), 176 | titleLabel.centerXAnchor.constraint(equalTo: topView.centerXAnchor), 177 | titleLabel.leftAnchor.constraint(greaterThanOrEqualTo: closeButton.rightAnchor)].activate() 178 | 179 | return topView 180 | } 181 | 182 | func bottomOverlayDefaultView(properties: CreditCardReaderDefaultControls) -> UIView { 183 | let backgroundView = UIView().noAutoresizingMask() 184 | backgroundView.backgroundColor = .black 185 | 186 | let bottomView = UIStackView().noAutoresizingMask() 187 | bottomView.axis = .vertical 188 | bottomView.alignment = .center 189 | backgroundView.addSubview(bottomView) 190 | bottomView.edgesAnchorEqualTo(destinationView: backgroundView).activate() 191 | let spacer = UIView() 192 | bottomView.addArrangedSubview(spacer) 193 | bottomView.setCustomSpacing(4, after: spacer) 194 | 195 | // Labels 196 | 197 | let cardNumberLabel = UILabel().noAutoresizingMask() 198 | cardNumberLabel.textColor = .white 199 | cardNumberLabel.font = UIFont.preferredFont(forTextStyle: .title2) 200 | cardNumberLabel.textAlignment = .center 201 | bottomView.addArrangedSubview(cardNumberLabel) 202 | bottomView.setCustomSpacing(4, after: cardNumberLabel) 203 | self.cardNumberLabel = cardNumberLabel 204 | 205 | let expirationLabel = UILabel().noAutoresizingMask() 206 | expirationLabel.textColor = .white 207 | expirationLabel.font = UIFont.preferredFont(forTextStyle: .title3) 208 | expirationLabel.textAlignment = .center 209 | bottomView.addArrangedSubview(expirationLabel) 210 | bottomView.setCustomSpacing(.uiControlButtonSpacing, after: expirationLabel) 211 | self.expirationLabel = expirationLabel 212 | 213 | // Controls 214 | 215 | if properties.isRetryEnabled { 216 | let controlsView = UIStackView().noAutoresizingMask() 217 | controlsView.axis = .horizontal 218 | controlsView.spacing = .uiControlButtonSpacing 219 | controlsView.addArrangedSubview(UIView()) 220 | bottomView.addArrangedSubview(controlsView) 221 | bottomView.setCustomSpacing(.uiControlButtonSpacing, after: controlsView) 222 | 223 | let retryButton = UIButton().noAutoresizingMask() 224 | retryButton.setTitle(properties.retryText, for: .normal) 225 | retryButton.setTitleColor(.white, for: .normal) 226 | retryButton.layer.borderWidth = 1 227 | retryButton.layer.borderColor = UIColor.white.cgColor 228 | retryButton.layer.cornerRadius = .uiControlButtonHeight / 2 229 | retryButton.addTarget(self, action: #selector(onRetryPressed), for: .touchUpInside) 230 | controlsView.addArrangedSubview(retryButton) 231 | retryButton.uiControlDimensionConstraints().activate() 232 | 233 | let confirmButton = UIButton().noAutoresizingMask() 234 | confirmButton.setTitle(properties.confirmText, for: .normal) 235 | confirmButton.setTitleColor(.black, for: .normal) 236 | confirmButton.backgroundColor = .white 237 | confirmButton.layer.cornerRadius = .uiControlButtonHeight / 2 238 | confirmButton.addTarget(self, action: #selector(onConfirmPressed), for: .touchUpInside) 239 | controlsView.addArrangedSubview(confirmButton) 240 | confirmButton.uiControlDimensionConstraints().activate() 241 | confirmButton.widthAnchor.constraint(equalTo: retryButton.widthAnchor).isActive = true 242 | 243 | controlsView.addArrangedSubview(UIView()) 244 | } 245 | 246 | bottomView.addArrangedSubview(UIView()) 247 | 248 | return backgroundView 249 | } 250 | 251 | // MARK: Private methods 252 | 253 | private func showBottomOverlayView() { 254 | cardNumberLabel?.text = detectedCard?.cardNumberDisplayString ?? "" 255 | expirationLabel?.text = detectedCard?.expirationDateDisplayString ?? "" 256 | UIView.animate(withDuration: 0.2) { [weak self] in 257 | self?.bottomOverlayView?.alpha = 1 258 | self?.bottomOverlayView?.transform = .identity 259 | } 260 | } 261 | 262 | private func hideBottomOverlayView() { 263 | UIView.animate(withDuration: 0.2) { [weak self] in 264 | guard let `self` = self else { return } 265 | self.bottomOverlayView?.alpha = 0 266 | self.bottomOverlayView?.transform = CGAffineTransform(translationX: 0, y: self.bottomViewTransitionOffset) 267 | } 268 | } 269 | 270 | @objc func closeView() { 271 | onControllerClosed?() 272 | } 273 | 274 | @objc func onRetryPressed() { 275 | hideBottomOverlayView() 276 | retry?() 277 | } 278 | 279 | @objc func onConfirmPressed() { 280 | if let detectedCard = detectedCard, 281 | let retry = retry { 282 | onSuccess?(detectedCard, retry) 283 | closeView() 284 | } 285 | } 286 | } 287 | 288 | extension UIView { 289 | func edgesAnchorEqualTo(destinationView: UIView) -> [NSLayoutConstraint] { 290 | [leftAnchor.constraint(equalTo: destinationView.leftAnchor), 291 | rightAnchor.constraint(equalTo: destinationView.rightAnchor), 292 | topAnchor.constraint(equalTo: destinationView.topAnchor), 293 | bottomAnchor.constraint(equalTo: destinationView.bottomAnchor)] 294 | } 295 | 296 | func noAutoresizingMask() -> Self { 297 | translatesAutoresizingMaskIntoConstraints = false 298 | return self 299 | } 300 | 301 | func uiControlDimensionConstraints() -> [NSLayoutConstraint] { 302 | [widthAnchor.constraint(greaterThanOrEqualToConstant: .uiControlButtonMinWidth), 303 | heightAnchor.constraint(equalToConstant: .uiControlButtonHeight)] 304 | } 305 | } 306 | 307 | extension Array where Element: NSLayoutConstraint { 308 | @discardableResult func activate() -> Array { 309 | forEach { $0.isActive = true } 310 | return self 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /Sources/Views/CardCaptureController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardCaptureController.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/20. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 13, *) 11 | class CardCaptureController { 12 | weak var captureView: CardCaptureView? 13 | func startCapture() { 14 | captureView?.startCapture() 15 | } 16 | func stopCapture() { 17 | captureView?.stopCapture() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Views/CardCaptureView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardCaptureView.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import UIKit 9 | import AVFoundation 10 | import VideoToolbox 11 | 12 | /// View that contains a camera and analyzes text results 13 | @available(iOS 13, *) 14 | class CardCaptureView: UIView { 15 | typealias Retry = () -> Void 16 | 17 | // MARK: Properties 18 | 19 | var focusStrokeColor: UIColor 20 | var focusStrokeWidth: CGFloat 21 | var isCapturePaused = false 22 | var onSuccess: (CreditCard, @escaping Retry) -> Void 23 | var onFailure: ((Error) -> Void)? 24 | 25 | // MARK: Private properties 26 | 27 | private let session = AVCaptureSession() 28 | private var videoLayer: AVCaptureVideoPreviewLayer? { layer as? AVCaptureVideoPreviewLayer } 29 | private let bufferQueue = DispatchQueue(label: "CreditCardReader.BufferQueue", qos: .default) 30 | private let dimLayer = CALayer() 31 | private let videoFocusLayer = CAShapeLayer() 32 | private let cardWidthToHeightRatio: CGFloat = 5398 / 8560 // ISO/IEC 7810 ID-1 33 | private let cardWidthToCornerRatio: CGFloat = 0.035 // ISO/IEC 7810 ID-1 34 | private let cardWidthRatio: CGFloat = 0.9 35 | private let cardImageAnalyzer = CreditCardImageAnalyzer() 36 | private var retryCount = 0 37 | private let retryLimit = 3 38 | private var isCaptureStopped = false 39 | 40 | // MARK: Init 41 | 42 | init(focusStrokeColor: UIColor = .white, 43 | focusStrokeWidth: CGFloat = 2, 44 | onSuccess: @escaping (CreditCard, @escaping Retry) -> Void, 45 | onFailure: ((Error) -> Void)?) { 46 | self.focusStrokeColor = focusStrokeColor 47 | self.focusStrokeWidth = focusStrokeWidth 48 | self.onSuccess = onSuccess 49 | self.onFailure = onFailure 50 | super.init(frame: .zero) 51 | 52 | setupFocusAreaView() 53 | } 54 | required init?(coder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | // MARK: Override 59 | 60 | override class var layerClass: AnyClass { 61 | AVCaptureVideoPreviewLayer.self 62 | } 63 | 64 | override func layoutSubviews() { 65 | super.layoutSubviews() 66 | updateVideoFocusArea() 67 | } 68 | 69 | // MARK: Internal Methods 70 | 71 | func startCapture() { 72 | let cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video) 73 | switch cameraAuthStatus { 74 | case .authorized: 75 | startSession() 76 | case .notDetermined: // Not asked for camera permission yet 77 | AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in 78 | DispatchQueue.main.async { 79 | if granted { 80 | self?.startSession() 81 | } else { 82 | self?.onFailure?(CameraPermissionError(authorizationStatus: .denied)) 83 | } 84 | } 85 | } 86 | case .denied, .restricted: 87 | onFailure?(CameraPermissionError(authorizationStatus: cameraAuthStatus)) 88 | @unknown default: 89 | onFailure?(CameraPermissionError(authorizationStatus: cameraAuthStatus)) 90 | } 91 | } 92 | 93 | func stopCapture() { 94 | isCaptureStopped = true 95 | session.stopRunning() 96 | } 97 | 98 | // MARK: Private methods 99 | 100 | private func startSession() { 101 | guard !isCaptureStopped else { 102 | isCaptureStopped = false 103 | session.startRunning() 104 | return 105 | } 106 | 107 | guard let device = AVCaptureDevice.default(for: .video), 108 | let input = try? AVCaptureDeviceInput(device: device) else { 109 | onFailure?(CameraInitializationError()) 110 | return 111 | } 112 | 113 | let output = AVCaptureVideoDataOutput() 114 | output.alwaysDiscardsLateVideoFrames = true 115 | output.setSampleBufferDelegate(self, queue: bufferQueue) 116 | session.addInput(input) 117 | session.addOutput(output) 118 | session.sessionPreset = .photo 119 | for connection in session.connections { 120 | connection.videoOrientation = .portrait 121 | } 122 | 123 | videoLayer?.session = session 124 | videoLayer?.connection?.videoOrientation = .portrait 125 | 126 | session.startRunning() 127 | } 128 | 129 | private func setupFocusAreaView() { 130 | dimLayer.backgroundColor = UIColor(white: 0, alpha: 0.3).cgColor 131 | videoFocusLayer.strokeColor = focusStrokeColor.cgColor 132 | videoFocusLayer.fillColor = UIColor.clear.cgColor 133 | videoFocusLayer.lineWidth = focusStrokeWidth 134 | layer.addSublayer(dimLayer) 135 | layer.addSublayer(videoFocusLayer) 136 | } 137 | 138 | private func updateVideoFocusArea() { 139 | let videoFocusArea = focusArea(for: bounds) 140 | let cornerRadius = videoFocusArea.width * cardWidthToCornerRatio 141 | 142 | // Dim Layer 143 | 144 | let dimLayerMask = CAShapeLayer() 145 | let maskPath = UIBezierPath(roundedRect: videoFocusArea, cornerRadius: cornerRadius) 146 | maskPath.append(UIBezierPath(rect: bounds)) 147 | dimLayerMask.path = maskPath.cgPath 148 | dimLayerMask.fillRule = .evenOdd 149 | dimLayer.mask = dimLayerMask 150 | dimLayer.frame = bounds 151 | 152 | // Focus Stroke Layer 153 | 154 | let strokeSideLength: CGFloat = 36 155 | let firstStrokeMaskRect = CGRect( 156 | x: 0, 157 | y: videoFocusArea.minY + strokeSideLength, 158 | width: bounds.width, 159 | height: videoFocusArea.height - (strokeSideLength * 2)) 160 | let secondStrokeMaskRect = CGRect( 161 | x: videoFocusArea.minX + strokeSideLength, 162 | y: 0, 163 | width: videoFocusArea.width - (strokeSideLength * 2), 164 | height: bounds.height) 165 | 166 | let focusMaskLayer = CAShapeLayer() 167 | let focusMaskPath = UIBezierPath(rect: firstStrokeMaskRect) 168 | focusMaskPath.append(UIBezierPath(rect: secondStrokeMaskRect)) 169 | focusMaskPath.append(UIBezierPath(rect: bounds)) 170 | focusMaskLayer.path = focusMaskPath.cgPath 171 | focusMaskLayer.fillRule = .evenOdd 172 | 173 | let focusPath = UIBezierPath(roundedRect: videoFocusArea, cornerRadius: cornerRadius) 174 | videoFocusLayer.path = focusPath.cgPath 175 | videoFocusLayer.frame = bounds 176 | videoFocusLayer.mask = focusMaskLayer 177 | } 178 | 179 | private func focusArea(for frame: CGRect) -> CGRect { 180 | let areaWidth = frame.width * cardWidthRatio 181 | let areaHeight = areaWidth * cardWidthToHeightRatio 182 | let areaX = (frame.width - areaWidth) / 2 183 | let areaY = (frame.height - areaHeight) / 2 184 | 185 | return CGRect(x: areaX, y: areaY, width: areaWidth, height: areaHeight) 186 | } 187 | } 188 | 189 | @available(iOS 13, *) 190 | extension CardCaptureView: AVCaptureVideoDataOutputSampleBufferDelegate { 191 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 192 | guard !isCapturePaused, 193 | let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 194 | return 195 | } 196 | 197 | var capturedImage: CGImage? 198 | VTCreateCGImageFromCVPixelBuffer(imageBuffer, options: nil, imageOut: &capturedImage) 199 | 200 | if let capturedImage = capturedImage, 201 | let focusedImage = capturedImage.cropping(to: imageFocusRect(image: capturedImage)) { 202 | cardImageAnalyzer.analyze(image: focusedImage) { [weak self] creditCard in 203 | guard let `self` = self else { return } 204 | 205 | // Sometimes expiration capture will fail, retry to get a more 206 | // accurate result. 207 | if creditCard.expirationYear == nil && self.retryCount < self.retryLimit { 208 | self.retryCount += 1 209 | return 210 | } 211 | 212 | self.retryCount = 0 213 | self.isCapturePaused = true 214 | DispatchQueue.main.async { 215 | self.onSuccess(creditCard) { [weak self] in 216 | // Retry 217 | self?.isCapturePaused = false 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | private func imageFocusRect(image: CGImage) -> CGRect { 225 | let width = CGFloat(image.width) * cardWidthRatio 226 | let height = width * cardWidthToHeightRatio 227 | let x = (CGFloat(image.width) - width) / 2 228 | let y = (CGFloat(image.height) - height) / 2 229 | return CGRect(x: x, y: y, width: width, height: height) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Sources/Views/OverlayDefaultViewParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayDefaultViewParams.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/04/19. 6 | // 7 | 8 | import UIKit 9 | 10 | public struct CreditCardReaderDefaultNavigationBar { 11 | public let titleText: String 12 | public let closeText: String 13 | 14 | public init( 15 | titleText: String = "Read Card", 16 | closeText: String = "Close") { 17 | self.titleText = titleText 18 | self.closeText = closeText 19 | } 20 | } 21 | 22 | public struct CreditCardReaderDefaultControls { 23 | public let instructionsText: String 24 | public let retryText: String 25 | public let confirmText: String 26 | public let isRetryEnabled: Bool 27 | public let navigatesBackOnDetection: Bool 28 | 29 | public init( 30 | instructionsText: String = "Align the card with the capture area.", 31 | retryText: String = "Retry", 32 | confirmText: String = "Confirm", 33 | isRetryEnabled: Bool = true, 34 | navigatesBackOnDetection: Bool = true) { 35 | self.instructionsText = instructionsText 36 | self.retryText = retryText 37 | self.confirmText = confirmText 38 | self.isRetryEnabled = isRetryEnabled 39 | self.navigatesBackOnDetection = navigatesBackOnDetection 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Views/ViewConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewConstants.swift 3 | // CreditCardReader 4 | // 5 | // Created by Wong, Kevin a on 2021/05/18. 6 | // 7 | 8 | import UIKit 9 | 10 | extension CGFloat { 11 | static let uiControlButtonHeight: CGFloat = 36 12 | static let uiControlButtonMinWidth: CGFloat = 100 13 | static let uiControlButtonSpacing: CGFloat = 20 14 | } 15 | 16 | extension Double { 17 | static let cardReaderCloseTimeDelay: Double = 1 18 | } 19 | -------------------------------------------------------------------------------- /docResources/CreditCardReader1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/iOS-CreditCardReader/075934638009352c5bbab107b522ddda8f185aa7/docResources/CreditCardReader1.png -------------------------------------------------------------------------------- /docResources/CreditCardReader2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/iOS-CreditCardReader/075934638009352c5bbab107b522ddda8f185aa7/docResources/CreditCardReader2.png -------------------------------------------------------------------------------- /xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | --------------------------------------------------------------------------------