diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index e3de27b..782a66a 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ A27424D91E7C35960093F436 /* NotificationNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27424D81E7C35960093F436 /* NotificationNames.swift */; }; A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2802BF71E70813A00879216 /* SliderTableViewCell.swift */; }; A2802BFA1E70813A00879216 /* SliderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2802BF81E70813A00879216 /* SliderTableViewCell.xib */; }; + A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */; }; A2A89D691E954698003FB2D3 /* UITextFieldExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A89D681E954698003FB2D3 /* UITextFieldExtension.swift */; }; DC037CA61E4B883900609409 /* OpenSourceComponentsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */; }; DC037CA81E4B898100609409 /* BasicStaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA71E4B898100609409 /* BasicStaticTableViewController.swift */; }; @@ -85,6 +86,7 @@ A27424D81E7C35960093F436 /* NotificationNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationNames.swift; sourceTree = ""; }; A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = ""; }; A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = ""; }; + A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; A2A89D681E954698003FB2D3 /* UITextFieldExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextFieldExtension.swift; sourceTree = ""; }; ADCE7A5C3CCC67D7D21BB3C4 /* libPods-pass.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-pass.a"; sourceTree = BUILT_PRODUCTS_DIR; }; AEAD6B31EAF5D061447A68CC /* Pods-pass.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.release.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.release.xcconfig"; sourceTree = ""; }; @@ -217,6 +219,7 @@ DC037CA91E4B8EAE00609409 /* SpecialThanksTableViewController.swift */, DC8963BF1E38EEB900828B09 /* SSHKeySettingTableViewController.swift */, DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */, + A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */, ); path = Controllers; sourceTree = ""; @@ -381,8 +384,8 @@ }; DC917BD21E2E8231000FDF54 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 4WDM8E95VU; - ProvisioningStyle = Manual; + DevelopmentTeam = FVE7LGR4V3; + ProvisioningStyle = Automatic; }; }; }; @@ -530,6 +533,7 @@ DCFB77A71E502DF9008DE471 /* EditPasswordTableViewController.swift in Sources */, DCA0499A1E335CC800522E8F /* GitServerSettingTableViewController.swift in Sources */, DCDDEAB31E4896BF00F68193 /* PasswordDetailTitleTableViewCell.swift in Sources */, + A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */, DCC277D21E30D6EA00402246 /* pass.xcdatamodeld in Sources */, DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */, DC962CDF1E4B62C10033B5D8 /* AboutTableViewController.swift in Sources */, @@ -719,7 +723,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = NO; - DEVELOPMENT_TEAM = 4WDM8E95VU; + DEVELOPMENT_TEAM = FVE7LGR4V3; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -734,10 +738,10 @@ ); INFOPLIST_FILE = pass/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = me.mssun.passforios; + PRODUCT_BUNDLE_IDENTIFIER = rocks.dango.passforios; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "893c10b3-79b1-46f7-914a-e625bf10d665"; - PROVISIONING_PROFILE_SPECIFIER = "match Development me.mssun.passforios"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/pass/Helpers/Objective-CBridgingHeader.h"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -750,10 +754,10 @@ baseConfigurationReference = AEAD6B31EAF5D061447A68CC /* Pods-pass.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = NO; - DEVELOPMENT_TEAM = 4WDM8E95VU; + DEVELOPMENT_TEAM = FVE7LGR4V3; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -768,10 +772,10 @@ ); INFOPLIST_FILE = pass/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = me.mssun.passforios; + PRODUCT_BUNDLE_IDENTIFIER = rocks.dango.passforios; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "aefeea85-1194-4db2-9ce4-fb9995e2fdff"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.mssun.passforios"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/pass/Helpers/Objective-CBridgingHeader.h"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/pass/Base.lproj/Main.storyboard b/pass/Base.lproj/Main.storyboard index 52b9647..d045738 100644 --- a/pass/Base.lproj/Main.storyboard +++ b/pass/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -89,11 +89,11 @@ - + diff --git a/pass/Controllers/AddPasswordTableViewController.swift b/pass/Controllers/AddPasswordTableViewController.swift index bb47726..dea0558 100644 --- a/pass/Controllers/AddPasswordTableViewController.swift +++ b/pass/Controllers/AddPasswordTableViewController.swift @@ -19,6 +19,7 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password"], [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]], [[.type: PasswordEditorCellType.textViewCell, .title: "additions"]], + [[.type: PasswordEditorCellType.scanQRCodeCell]] ] super.viewDidLoad() } @@ -54,13 +55,15 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "saveAddPasswordSegue" {let cells = tableView.visibleCells + super.prepare(for: segue, sender: sender) + if segue.identifier == "saveAddPasswordSegue" { + let cells = tableView.visibleCells var cellContents = [String: String]() for cell in cells { - let indexPath = tableView.indexPath(for: cell)! - let contentCell = cell as! ContentTableViewCell - let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String - if let cellContent = contentCell.getContent() { + if let indexPath = tableView.indexPath(for: cell), + let contentCell = cell as? ContentTableViewCell, + let cellTitle = tableData[indexPath.section][indexPath.row][.title] as? String, + let cellContent = contentCell.getContent() { cellContents[cellTitle] = cellContent } } diff --git a/pass/Controllers/EditPasswordTableViewController.swift b/pass/Controllers/EditPasswordTableViewController.swift index 71b6146..0caf2f8 100644 --- a/pass/Controllers/EditPasswordTableViewController.swift +++ b/pass/Controllers/EditPasswordTableViewController.swift @@ -15,7 +15,8 @@ class EditPasswordTableViewController: PasswordEditorTableViewController { [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password], [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]], [[.type: PasswordEditorCellType.textViewCell, .title: "additions", .content: password!.getAdditionsPlainText()]], - [[.type: PasswordEditorCellType.deletePasswordCell]], + [[.type: PasswordEditorCellType.scanQRCodeCell], + [.type: PasswordEditorCellType.deletePasswordCell]] ] super.viewDidLoad() } @@ -37,16 +38,16 @@ class EditPasswordTableViewController: PasswordEditorTableViewController { } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) if segue.identifier == "saveEditPasswordSegue" { let cells = tableView.visibleCells var cellContents = [String: String]() for cell in cells { - let indexPath = tableView.indexPath(for: cell)! - if let contentCell = cell as? ContentTableViewCell { - let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String - if let cellContent = contentCell.getContent() { - cellContents[cellTitle] = cellContent - } + if let indexPath = tableView.indexPath(for: cell), + let contentCell = cell as? ContentTableViewCell, + let cellTitle = tableData[indexPath.section][indexPath.row][.title] as? String, + let cellContent = contentCell.getContent() { + cellContents[cellTitle] = cellContent } } var plainText = "" diff --git a/pass/Controllers/PasswordEditorTableViewController.swift b/pass/Controllers/PasswordEditorTableViewController.swift index 334546a..b67f8ec 100644 --- a/pass/Controllers/PasswordEditorTableViewController.swift +++ b/pass/Controllers/PasswordEditorTableViewController.swift @@ -8,16 +8,17 @@ import UIKit import SwiftyUserDefaults +import OneTimePassword enum PasswordEditorCellType { - case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell + case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell, scanQRCodeCell } enum PasswordEditorCellKey { case type, title, content, placeholders } -class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, UIGestureRecognizerDelegate { +class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, QRScannerControllerDelegate { var tableData = [ [Dictionary] @@ -29,11 +30,13 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()} private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""] private let passwordSection = 1 + private let additionsSection = 2 private var hidePasswordSettings = true private var fillPasswordCell: FillPasswordTableViewCell? private var passwordLengthCell: SliderTableViewCell? private var deletePasswordCell: UITableViewCell? + private var scanQRCodeCell: UITableViewCell? override func loadView() { super.loadView() @@ -42,6 +45,14 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl deletePasswordCell!.textLabel?.text = "Delete Password" deletePasswordCell!.textLabel?.textColor = Globals.red deletePasswordCell?.selectionStyle = .default + + scanQRCodeCell = UITableViewCell(style: .default, reuseIdentifier: "default") + scanQRCodeCell?.textLabel?.text = "Add One-Time Password" + scanQRCodeCell?.textLabel?.textColor = Globals.blue + scanQRCodeCell?.selectionStyle = .default +// scanQRCodeCell?.imageView?.image = #imageLiteral(resourceName: "Camera").withRenderingMode(.alwaysTemplate) +// scanQRCodeCell?.imageView?.tintColor = Globals.blue +// scanQRCodeCell?.imageView?.contentMode = .scaleAspectFit } override func viewDidLoad() { @@ -86,6 +97,8 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl return passwordLengthCell! case .deletePasswordCell: return deletePasswordCell! + case .scanQRCodeCell: + return scanQRCodeCell! default: let cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String) @@ -119,13 +132,16 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if tableView.cellForRow(at: indexPath) == deletePasswordCell { + let selectedCell = tableView.cellForRow(at: indexPath) + if selectedCell == deletePasswordCell { let alert = UIAlertController(title: "Delete Password?", message: nil, preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "Delete", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in self.performSegue(withIdentifier: "deletePasswordSegue", sender: self) })) alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler:nil)) self.present(alert, animated: true, completion: nil) + } else if selectedCell == scanQRCodeCell { + self.performSegue(withIdentifier: "showQRScannerSegue", sender: self) } tableView.deselectRow(at: indexPath, animated: true) } @@ -168,4 +184,37 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl hidePasswordSettings = !hidePasswordSettings tableView.reloadSections([passwordSection], with: .fade) } + + func insertScannedOTPFields(_ otpauth: String) { + // update tableData + if let additionsPlainText = (tableData[additionsSection][0][PasswordEditorCellKey.content] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), additionsPlainText != "" { + tableData[additionsSection][0][PasswordEditorCellKey.content] = additionsPlainText + "\n" + otpauth + } else { + tableData[additionsSection][0][PasswordEditorCellKey.content] = otpauth + } + // reload + tableView.reloadSections([additionsSection], with: .none) + } + + // MARK: - QRScannerControllerDelegate Methods + func checkScannedOutput(line: String) -> (accept: Bool, message: String) { + if let url = URL(string: line), let _ = Token(url: url) { + return (accept: true, message: "Valid token URL") + } else { + return (accept: false, message: "Invalid token URL") + } + } + + // MARK: - QRScannerControllerDelegate Methods + func handleScannedOutput(line: String) { + insertScannedOTPFields(line) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showQRScannerSegue" { + if let viewController = segue.destination as? QRScannerController { + viewController.delegate = self + } + } + } } diff --git a/pass/Controllers/QRScannerController.swift b/pass/Controllers/QRScannerController.swift new file mode 100644 index 0000000..13e40da --- /dev/null +++ b/pass/Controllers/QRScannerController.swift @@ -0,0 +1,128 @@ +// +// QRScannerController.swift +// pass +// +// Created by Yishi Lin on 7/4/17. +// Copyright © 2017 Yishi Lin. All rights reserved. +// + +import UIKit +import AVFoundation +import OneTimePassword +import SVProgressHUD + +protocol QRScannerControllerDelegate { + func checkScannedOutput(line: String) -> (accept: Bool, message: String) + func handleScannedOutput(line: String) +} + +class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { + + @IBOutlet weak var scannerOutput: UILabel! + + var captureSession: AVCaptureSession? + var videoPreviewLayer: AVCaptureVideoPreviewLayer? + var qrCodeFrameView: UIView? + + let supportedCodeTypes = [AVMetadataObjectTypeQRCode] + + var delegate: QRScannerControllerDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + // Get an instance of the AVCaptureDevice class to initialize a device object and provide the video as the media type parameter. + let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) + + do { + // Get an instance of the AVCaptureDeviceInput class using the previous device object. + let input = try AVCaptureDeviceInput(device: captureDevice) + + // Initialize the captureSession object. + captureSession = AVCaptureSession() + + // Set the input device on the capture session. + captureSession?.addInput(input) + + // Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. + let captureMetadataOutput = AVCaptureMetadataOutput() + captureSession?.addOutput(captureMetadataOutput) + + // Set delegate and use the default dispatch queue to execute the call back + captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + captureMetadataOutput.metadataObjectTypes = supportedCodeTypes + + // Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer. + videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill + videoPreviewLayer?.frame = view.layer.bounds + view.layer.addSublayer(videoPreviewLayer!) + + // Start video capture. + captureSession?.startRunning() + + // Move the message label to the front + scannerOutput.layer.cornerRadius = 10 + scannerOutput.text = "No QR code detected" + view.bringSubview(toFront: scannerOutput) + + // Initialize QR Code Frame to highlight the QR code + qrCodeFrameView = UIView() + + if let qrCodeFrameView = qrCodeFrameView { + qrCodeFrameView.layer.borderColor = UIColor.green.cgColor + qrCodeFrameView.layer.borderWidth = 2 + view.addSubview(qrCodeFrameView) + view.bringSubview(toFront: qrCodeFrameView) + } + + } catch { + print(error) + return + } + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + // MARK: - AVCaptureMetadataOutputObjectsDelegate Methods + + func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { + + if let metadataObj = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + supportedCodeTypes.contains(metadataObj.type), + let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj) { + + // draw a bounds on the found QR code + qrCodeFrameView?.frame = barCodeObject.bounds + + // check whether it is a valid result + if let scanned = metadataObj.stringValue { + if let (accept, message) = delegate?.checkScannedOutput(line: scanned) { + scannerOutput.text = message + if accept == true { + captureSession?.stopRunning() + delegate?.handleScannedOutput(line: scanned) + DispatchQueue.main.async { + SVProgressHUD.showSuccess(withStatus: "Done") + SVProgressHUD.dismiss(withDelay: 1) + self.navigationController?.popViewController(animated: true) + } + } + } else { + // no delegate, show the scanned result + scannerOutput.text = scanned + } + } else { + scannerOutput.text = "No string value" + } + + } else { + qrCodeFrameView?.frame = CGRect.zero + scannerOutput.text = "No QR code detected" + } + } +} diff --git a/pass/Info.plist b/pass/Info.plist index 11cab7f..4121fc4 100644 --- a/pass/Info.plist +++ b/pass/Info.plist @@ -24,6 +24,8 @@ LSRequiresIPhoneOS + NSCameraUsageDescription + We need to access your camera for scanning QR code. UIApplicationShortcutItems