Rewrite AutoFill extension
This commit is contained in:
parent
7e034d9c99
commit
40ac070232
13 changed files with 609 additions and 216 deletions
|
|
@ -9,208 +9,44 @@
|
|||
import AuthenticationServices
|
||||
import passKit
|
||||
|
||||
class CredentialProviderViewController: ASCredentialProviderViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate {
|
||||
@IBOutlet var searchBar: UISearchBar!
|
||||
@IBOutlet var tableView: UITableView!
|
||||
|
||||
private let passwordStore = PasswordStore.shared
|
||||
private let keychain = AppKeychain.shared
|
||||
|
||||
private var searchActive = false
|
||||
private var passwordsTableEntries: [PasswordTableEntry] = []
|
||||
private var filteredPasswordsTableEntries: [PasswordTableEntry] = []
|
||||
|
||||
private lazy var passcodelock: PasscodeExtensionDisplay = {
|
||||
let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext)
|
||||
return passcodelock
|
||||
}()
|
||||
|
||||
/*
|
||||
Prepare your UI to list available credentials for the user to choose from. The items in
|
||||
'serviceIdentifiers' describe the service the user is logging in to, so your extension can
|
||||
prioritize the most relevant credentials in the list.
|
||||
*/
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
||||
// clean up the search bar
|
||||
guard !serviceIdentifiers.isEmpty else {
|
||||
searchBar.text = ""
|
||||
searchBar.becomeFirstResponder()
|
||||
searchBarSearchButtonClicked(searchBar)
|
||||
return
|
||||
}
|
||||
|
||||
// get the domain
|
||||
var identifier = serviceIdentifiers[0].identifier
|
||||
if !identifier.hasPrefix("http://"), !identifier.hasPrefix("https://") {
|
||||
identifier = "http://" + identifier
|
||||
}
|
||||
let url = URL(string: identifier)?.host ?? ""
|
||||
|
||||
// "click" search
|
||||
searchBar.text = url
|
||||
searchBar.becomeFirstResponder()
|
||||
searchBarSearchButtonClicked(searchBar)
|
||||
class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
var passcodelock: PasscodeExtensionDisplay {
|
||||
PasscodeExtensionDisplay(extensionContext: self.extensionContext)
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if your extension support
|
||||
s showing credentials in the QuickType bar.
|
||||
When the user selects a credential from your app, this method will be called with the
|
||||
ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore.
|
||||
Provide the password by completing the extension request with the associated ASPasswordCredential.
|
||||
If using the credential would require showing custom UI for authenticating the user, cancel
|
||||
the request with error code ASExtensionError.userInteractionRequired.
|
||||
|
||||
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
let databaseIsUnlocked = true
|
||||
if (databaseIsUnlocked) {
|
||||
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
} else {
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
UI and call this method. Show appropriate UI for authenticating the user then provide the password
|
||||
by completing the extension request with the associated ASPasswordCredential.
|
||||
|
||||
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
}
|
||||
*/
|
||||
|
||||
@IBAction
|
||||
private func cancel(_: AnyObject?) {
|
||||
extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
self.dismiss(animated: true)
|
||||
var embeddedNavigationController: UINavigationController {
|
||||
children.first as! UINavigationController
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
passcodelock.presentPasscodeLockIfNeeded(self)
|
||||
var passwordsViewController: PasswordsViewController {
|
||||
embeddedNavigationController.viewControllers.first as! PasswordsViewController
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
passcodelock.presentPasscodeLockIfNeeded(self)
|
||||
|
||||
// prepare
|
||||
searchBar.delegate = self
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell")
|
||||
|
||||
// initialize table entries
|
||||
initPasswordsTableEntries()
|
||||
let passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false).compactMap { PasswordTableEntry($0) }
|
||||
let dataSource = PasswordsTableDataSource(entries: passwordsTableEntries)
|
||||
passwordsViewController.dataSource = dataSource
|
||||
passwordsViewController.selectionDelegate = self
|
||||
}
|
||||
|
||||
private func initPasswordsTableEntries() {
|
||||
filteredPasswordsTableEntries.removeAll()
|
||||
|
||||
let passwordEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false)
|
||||
passwordsTableEntries = passwordEntities.compactMap {
|
||||
PasswordTableEntry($0)
|
||||
}
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
||||
let url = serviceIdentifiers.first.flatMap { URL(string: $0.identifier) }
|
||||
passwordsViewController.navigationItem.prompt = url?.host
|
||||
}
|
||||
}
|
||||
|
||||
// define cell contents
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath)
|
||||
let entry = getPasswordEntry(by: indexPath)
|
||||
if entry.passwordEntity.synced {
|
||||
cell.textLabel?.text = entry.title
|
||||
} else {
|
||||
cell.textLabel?.text = "↻ \(entry.title)"
|
||||
}
|
||||
cell.accessoryType = .none
|
||||
cell.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote)
|
||||
cell.detailTextLabel?.text = entry.categoryText
|
||||
return cell
|
||||
}
|
||||
extension CredentialProviderViewController: PasswordSelectionDelegate {
|
||||
func selected(password: PasswordTableEntry) {
|
||||
let passwordEntity = password.passwordEntity
|
||||
|
||||
// select row -> extension returns (with username and password)
|
||||
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let entry = getPasswordEntry(by: indexPath)
|
||||
|
||||
guard PGPAgent.shared.isPrepared else {
|
||||
Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let passwordEntity = entry.passwordEntity
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
decryptPassword(passwordEntity: passwordEntity)
|
||||
}
|
||||
|
||||
private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) {
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
do {
|
||||
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self)
|
||||
let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
||||
|
||||
let username = decryptedPassword.getUsernameForCompletion()
|
||||
let password = decryptedPassword.password
|
||||
DispatchQueue.main.async {
|
||||
let passwordCredential = ASPasswordCredential(user: username, password: password)
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential)
|
||||
}
|
||||
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
|
||||
DispatchQueue.main.async {
|
||||
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction.cancelAndPopView(controller: self))
|
||||
let selectKey = UIAlertAction.selectKey(controller: self) { action in
|
||||
self.decryptPassword(passwordEntity: passwordEntity, keyID: action.title)
|
||||
}
|
||||
alert.addAction(selectKey)
|
||||
|
||||
self.present(alert, animated: true)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfSectionsInTableView(tableView _: UITableView) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
|
||||
if searchActive {
|
||||
return filteredPasswordsTableEntries.count
|
||||
}
|
||||
return passwordsTableEntries.count
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.text = ""
|
||||
searchActive = false
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
if let searchText = searchBar.text, searchText.isEmpty == false {
|
||||
filteredPasswordsTableEntries = passwordsTableEntries.filter { $0.match(searchText) }
|
||||
searchActive = true
|
||||
} else {
|
||||
searchActive = false
|
||||
}
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange _: String) {
|
||||
searchBarSearchButtonClicked(searchBar)
|
||||
}
|
||||
|
||||
private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry {
|
||||
if searchActive {
|
||||
return filteredPasswordsTableEntries[indexPath.row]
|
||||
} else {
|
||||
return passwordsTableEntries[indexPath.row]
|
||||
decryptPassword(in: self, with: passwordEntity) { password in
|
||||
let username = password.getUsernameForCompletion()
|
||||
let password = password.password
|
||||
let passwordCredential = ASPasswordCredential(user: username, password: password)
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import passKit
|
|||
|
||||
// cancel means cancel the extension
|
||||
class PasscodeLockViewControllerForExtension: PasscodeLockViewController {
|
||||
var originalExtensionContest: ASCredentialProviderExtensionContext?
|
||||
var originalExtensionContext: ASCredentialProviderExtensionContext!
|
||||
|
||||
convenience init(extensionContext: ASCredentialProviderExtensionContext?) {
|
||||
convenience init(extensionContext: ASCredentialProviderExtensionContext) {
|
||||
self.init()
|
||||
self.originalExtensionContest = extensionContext
|
||||
self.originalExtensionContext = extensionContext
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
|
@ -27,7 +27,7 @@ class PasscodeLockViewControllerForExtension: PasscodeLockViewController {
|
|||
|
||||
@objc
|
||||
func cancelExtension() {
|
||||
originalExtensionContest?.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
originalExtensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ class PasscodeExtensionDisplay {
|
|||
private let passcodeLockVC: PasscodeLockViewControllerForExtension
|
||||
private let extensionContext: ASCredentialProviderExtensionContext?
|
||||
|
||||
init(extensionContext: ASCredentialProviderExtensionContext?) {
|
||||
init(extensionContext: ASCredentialProviderExtensionContext) {
|
||||
self.extensionContext = extensionContext
|
||||
self.passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext)
|
||||
passcodeLockVC.dismissCompletionCallback = { [weak self] in
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// PasswordsViewController.swift
|
||||
// passAutoFillExtension
|
||||
//
|
||||
// Created by Sun, Mingshen on 12/31/20.
|
||||
// Copyright © 2020 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AuthenticationServices
|
||||
import passKit
|
||||
|
||||
class PasswordsViewController: UIViewController {
|
||||
@IBOutlet var tableView: UITableView!
|
||||
|
||||
var dataSource: PasswordsTableDataSource!
|
||||
weak var selectionDelegate: PasswordSelectionDelegate?
|
||||
|
||||
var searchController: UISearchController {
|
||||
let uiSearchController = UISearchController(searchResultsController: nil)
|
||||
uiSearchController.searchBar.isTranslucent = true
|
||||
uiSearchController.obscuresBackgroundDuringPresentation = false
|
||||
uiSearchController.searchBar.sizeToFit()
|
||||
if #available(iOS 13.0, *) {
|
||||
uiSearchController.searchBar.searchTextField.clearButtonMode = .whileEditing
|
||||
}
|
||||
return uiSearchController
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
|
||||
searchController.searchBar.delegate = self
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = dataSource
|
||||
}
|
||||
|
||||
@IBAction
|
||||
private func cancel(_: AnyObject?) {
|
||||
self.extensionContext?.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension PasswordsViewController: UISearchBarDelegate {
|
||||
func searchBar(_: UISearchBar, textDidChange searchText: String) {
|
||||
dataSource.showTableEntries(matching: searchText)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_: UISearchBar) {
|
||||
dataSource.showTableEntries(matching: "")
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension PasswordsViewController: UITableViewDelegate {
|
||||
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let entry = dataSource.filteredPasswordsTableEntries[indexPath.row]
|
||||
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
self.selectionDelegate?.selected(password: entry)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue