Skip to content

Commit

Permalink
BIT-2346: Add inline autofill (#645)
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-livefront authored Jun 4, 2024
1 parent 6eee319 commit 7710cbb
Show file tree
Hide file tree
Showing 29 changed files with 1,280 additions and 187 deletions.
2 changes: 1 addition & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
--header strip
--funcattributes prev-line
--typeattributes prev-line
--varattributes same-line
--varattributes preserve

# file options
--exclude **/Generated,build,vendor/bundle
Expand Down
2 changes: 1 addition & 1 deletion BitwardenActionExtension/ActionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class ActionViewController: UIViewController {
// MARK: - AppExtensionDelegate

extension ActionViewController: AppExtensionDelegate {
var authCompletionRoute: AppRoute {
var authCompletionRoute: AppRoute? {
actionExtensionHelper.authCompletionRoute
}

Expand Down
165 changes: 118 additions & 47 deletions BitwardenAutoFillExtension/CredentialProviderViewController.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import AuthenticationServices
import BitwardenShared
import OSLog

/// An `ASCredentialProviderViewController` that implements credential autofill.
///
class CredentialProviderViewController: ASCredentialProviderViewController {
// MARK: Types

/// An enumeration that describes how the extension is being used.
///
enum ExtensionMode {
/// The extension is autofilling a specific credential.
case autofillCredential(ASPasswordCredentialIdentity)

/// The extension is displaying a list of items in the vault that match a service identifier.
case autofillVaultList([ASCredentialServiceIdentifier])

/// The extension is being configured to set up autofill.
case configureAutofill
}

// MARK: Properties

/// The app's theme.
Expand All @@ -12,60 +28,44 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
/// The processor that manages application level logic.
private var appProcessor: AppProcessor?

/// Whether the extension was opened to configure the extension after it was enabled.
private var isConfiguring = false

/// A list of service identifiers used to filter credentials for autofill.
private var serviceIdentifiers = [ASCredentialServiceIdentifier]()
/// The mode that describes how the extension is being used.
private var extensionMode = ExtensionMode.configureAutofill

// MARK: ASCredentialProviderViewController

override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
self.serviceIdentifiers = serviceIdentifiers
initializeApp()
initializeApp(extensionMode: .autofillVaultList(serviceIdentifiers))
}

// Implement this method if your extension supports 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) {
// }

override func prepareInterfaceForExtensionConfiguration() {
isConfiguring = true
initializeApp()
initializeApp(extensionMode: .configureAutofill)
}

override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
initializeApp(extensionMode: .autofillCredential(credentialIdentity))
}

override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
guard let recordIdentifier = credentialIdentity.recordIdentifier else {
cancel(error: ASExtensionError(.credentialIdentityNotFound))
return
}

initializeApp(extensionMode: .autofillCredential(credentialIdentity), userInteraction: false)
provideCredential(for: recordIdentifier)
}

// MARK: Private

/// Cancels the extension request and dismisses the extension's view controller.
///
private func cancel() {
if isConfiguring {
/// - Parameter error: An optional error describing why the request failed.
///
private func cancel(error: Error? = nil) {
if case .configureAutofill = extensionMode {
extensionContext.completeExtensionConfigurationRequest()
} else if let error {
extensionContext.cancelRequest(withError: error)
} else {
extensionContext.cancelRequest(
withError: NSError(
Expand All @@ -78,27 +78,70 @@ class CredentialProviderViewController: ASCredentialProviderViewController {

/// Sets up and initializes the app and UI.
///
private func initializeApp() {
/// - Parameters:
/// - extensionMode: The mode that describes how the extension is being used.
/// - userInteraction: Whether user interaction is allowed or if the app needs to
/// start without user interaction.
///
private func initializeApp(extensionMode: ExtensionMode, userInteraction: Bool = true) {
self.extensionMode = extensionMode

let errorReporter = OSLogErrorReporter()
let services = ServiceContainer(errorReporter: errorReporter)
let appModule = DefaultAppModule(appExtensionDelegate: self, services: services)
let appProcessor = AppProcessor(appModule: appModule, services: services)
self.appProcessor = appProcessor

if userInteraction {
Task {
await appProcessor.start(appContext: .appExtension, navigator: self, window: nil)
}
}
}

/// Attempts to provide the credential with the specified ID to the extension context to handle
/// autofill.
///
/// - Parameters:
/// - id: The identifier of the user-requested credential to return.
/// - repromptPasswordValidated: `true` if master password reprompt was required for the
/// cipher and the user's master password was validated.
///
private func provideCredential(
for id: String,
repromptPasswordValidated: Bool = false
) {
guard let appProcessor else {
cancel(error: ASExtensionError(.failed))
return
}

Task {
await appProcessor.start(appContext: .appExtension, navigator: self, window: nil)
do {
let credential = try await appProcessor.provideCredential(
for: id,
repromptPasswordValidated: repromptPasswordValidated
)
extensionContext.completeRequest(withSelectedCredential: credential)
} catch {
Logger.appExtension.error("Error providing credential without user interaction: \(error)")
cancel(error: error)
}
}
}
}

// MARK: - AppExtensionDelegate

extension CredentialProviderViewController: AppExtensionDelegate {
var authCompletionRoute: AppRoute {
if isConfiguring {
AppRoute.extensionSetup(.extensionActivation(type: .autofillExtension))
} else {
var authCompletionRoute: AppRoute? {
switch extensionMode {
case .autofillCredential:
nil
case .autofillVaultList:
AppRoute.vault(.autofillList)
case .configureAutofill:
AppRoute.extensionSetup(.extensionActivation(type: .autofillExtension))
}
}

Expand All @@ -107,7 +150,10 @@ extension CredentialProviderViewController: AppExtensionDelegate {
var isInAppExtension: Bool { true }

var uri: String? {
guard let serviceIdentifier = serviceIdentifiers.first else { return nil }
guard case let .autofillVaultList(serviceIdentifiers) = extensionMode,
let serviceIdentifier = serviceIdentifiers.first
else { return nil }

return switch serviceIdentifier.type {
case .domain:
"https://" + serviceIdentifier.identifier
Expand All @@ -126,6 +172,31 @@ extension CredentialProviderViewController: AppExtensionDelegate {
func didCancel() {
cancel()
}

func didCompleteAuth() {
guard case let .autofillCredential(credential) = extensionMode else { return }

guard let appProcessor, let recordIdentifier = credential.recordIdentifier else {
cancel(error: ASExtensionError(.failed))
return
}

Task {
do {
try await appProcessor.repromptForCredentialIfNecessary(
for: recordIdentifier
) { repromptPasswordValidated in
self.provideCredential(
for: recordIdentifier,
repromptPasswordValidated: repromptPasswordValidated
)
}
} catch {
Logger.appExtension.error("Error providing credential: \(error)")
cancel(error: error)
}
}
}
}

// MARK: - RootNavigator
Expand Down
2 changes: 1 addition & 1 deletion BitwardenShareExtension/ShareViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ShareViewController: UIViewController {
/// The app's theme.
var appTheme: AppTheme = .default

var authCompletionRoute: AppRoute = .sendItem(.add(content: nil, hasPremium: false))
var authCompletionRoute: AppRoute? = .sendItem(.add(content: nil, hasPremium: false))

/// The processor that manages application level logic.
private var appProcessor: AppProcessor?
Expand Down
Loading

0 comments on commit 7710cbb

Please sign in to comment.