Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIT-2346: Add inline autofill #645

Merged
merged 5 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule was conflicting with SwiftLint, but I think SwiftLint has better handing for this since it allows certain attributes (e.g. @Environment) to be on the same line and others to be on the previous line.

SwiftFormat wanted to change this:

Before After
@available(iOS 17, *)
var credentialIdentity: (any ASCredentialIdentity)? {
passwordCredentialIdentity
}
@available(iOS 17, *) var credentialIdentity: (any ASCredentialIdentity)? {
passwordCredentialIdentity
}


# 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
Loading