Skip to content

Commit

Permalink
[PM-11153] Implement context menu text autofill (#1178)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Czech <[email protected]>
  • Loading branch information
fedemkr and matt-livefront authored Dec 18, 2024
1 parent 625bbd3 commit 7f150f5
Show file tree
Hide file tree
Showing 61 changed files with 3,889 additions and 201 deletions.
2 changes: 2 additions & 0 deletions BitwardenAutoFillExtension/Application/Support/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
<true/>
<key>ProvidesPasswords</key>
<true/>
<key>ProvidesTextToInsert</key>
<true/>
<key>ShowsConfigurationUI</key>
<true/>
</dict>
Expand Down
20 changes: 15 additions & 5 deletions BitwardenAutoFillExtension/CredentialProviderViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
// MARK: - iOS 18

extension CredentialProviderViewController {
@available(iOSApplicationExtension 18.0, *)
override func prepareInterfaceForUserChoosingTextToInsert() {
initializeApp(with: DefaultCredentialProviderContext(.autofillText))
}

@available(iOSApplicationExtension 18.0, *)
override func prepareOneTimeCodeCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
initializeApp(with: DefaultCredentialProviderContext(.autofillOTP(serviceIdentifiers)))
Expand Down Expand Up @@ -317,11 +322,6 @@ extension CredentialProviderViewController: AppExtensionDelegate {
extensionContext.completeRequest(withSelectedCredential: passwordCredential)
}

@available(iOSApplicationExtension 18.0, *)
func completeOTPRequest(code: String) {
extensionContext.completeOneTimeCodeRequest(using: ASOneTimeCodeCredential(code: code))
}

func didCancel() {
cancel()
}
Expand Down Expand Up @@ -421,11 +421,21 @@ extension CredentialProviderViewController: AutofillAppExtensionDelegate {
extensionContext.completeAssertionRequest(using: assertionCredential)
}

@available(iOSApplicationExtension 18.0, *)
func completeOTPRequest(code: String) {
extensionContext.completeOneTimeCodeRequest(using: ASOneTimeCodeCredential(code: code))
}

@available(iOSApplicationExtension 17.0, *)
func completeRegistrationRequest(asPasskeyRegistrationCredential: ASPasskeyRegistrationCredential) {
extensionContext.completeRegistrationRequest(using: asPasskeyRegistrationCredential)
}

@available(iOSApplicationExtension 18.0, *)
func completeTextRequest(text: String) {
extensionContext.completeRequest(withTextToInsert: text)
}

func getDidAppearPublisher() -> AsyncPublisher<AnyPublisher<Bool, Never>> {
didAppearSubject
.eraseToAnyPublisher()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/// The mode in which the autofil list presents its items.
public enum AutofillListMode {
/// The autofill list shows all ciphers for autofill.
/// This is used on autofill with text to insert.
/// Only filters deleted items.
case all
/// The autofill list only shows ciphers for password autofill.
case passwords
/// The autofill list shows both passwords and Fido2 items in the same section.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public enum AutofillExtensionMode {
/// The extension is autofilling a specific OTP credential.
case autofillOTPCredential(OneTimeCodeCredentialIdentityProxy, userInteraction: Bool)

/// The extension is called from the context menu of a field to autofill some text.
/// This is generic so we can auotfill pretty much anything the user chooses.
case autofillText

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public struct DefaultCredentialProviderContext: CredentialProviderContext {
return AppRoute.vault(.autofillList)
case .autofillOTPCredential:
return nil
case .autofillText:
return AppRoute.vault(.autofillList)
case .autofillVaultList:
return AppRoute.vault(.autofillList)
case .autofillFido2Credential:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class CredentialProviderContextTests: BitwardenTestCase {
.autofillOTPCredential(MockOneTimeCodeCredentialIdentity(), userInteraction: false)
).authCompletionRoute
)
XCTAssertEqual(
DefaultCredentialProviderContext(.autofillText).authCompletionRoute,
AppRoute.vault(.autofillList)
)
XCTAssertEqual(
DefaultCredentialProviderContext(.configureAutofill)
.authCompletionRoute,
Expand Down Expand Up @@ -67,6 +71,9 @@ class CredentialProviderContextTests: BitwardenTestCase {
DefaultCredentialProviderContext(.autofillFido2VaultList([], MockPasskeyCredentialRequestParameters()))
.configuring
)
XCTAssertFalse(
DefaultCredentialProviderContext(.autofillText).configuring
)
XCTAssertFalse(
DefaultCredentialProviderContext(
.autofillOTPCredential(
Expand Down Expand Up @@ -140,6 +147,13 @@ class CredentialProviderContextTests: BitwardenTestCase {
} else {
XCTFail("ExtensionMode doesn't match")
}

let context8 = DefaultCredentialProviderContext(.autofillText)
if case .autofillText = context8.extensionMode {
XCTAssert(true)
} else {
XCTFail("ExtensionMode doesn't match")
}
}

/// `getter:passwordCredentialIdentity` returns the identity of `autofillCredential` mode.
Expand All @@ -164,6 +178,10 @@ class CredentialProviderContextTests: BitwardenTestCase {
DefaultCredentialProviderContext(.autofillFido2VaultList([], MockPasskeyCredentialRequestParameters()))
.passwordCredentialIdentity
)
XCTAssertNil(
DefaultCredentialProviderContext(.autofillText)
.passwordCredentialIdentity
)
XCTAssertNil(
DefaultCredentialProviderContext(.configureAutofill)
.passwordCredentialIdentity
Expand Down Expand Up @@ -214,6 +232,8 @@ class CredentialProviderContextTests: BitwardenTestCase {
)
XCTAssertFalse(subjectOTPCredentialFalse.flowWithUserInteraction)

XCTAssertTrue(DefaultCredentialProviderContext(.autofillText).flowWithUserInteraction)

let subject3 = DefaultCredentialProviderContext(.configureAutofill)
XCTAssertTrue(subject3.flowWithUserInteraction)

Expand Down Expand Up @@ -270,6 +290,9 @@ class CredentialProviderContextTests: BitwardenTestCase {
)
)
XCTAssertEqual(subject5.serviceIdentifiers, expectedIdentifiers)

let subject6 = DefaultCredentialProviderContext(.autofillText)
XCTAssertEqual(subject6.serviceIdentifiers, expectedIdentifiers)
}
}

Expand Down
27 changes: 27 additions & 0 deletions BitwardenShared/Core/Platform/Services/ServiceContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// The object used by the application to retrieve information about this device.
let systemDevice: SystemDevice

/// Factory to create `TextAutofillHelper`s.
let textAutofillHelperFactory: TextAutofillHelperFactory

/// Provides the present time for TOTP Code Calculation.
let timeProvider: TimeProvider

Expand All @@ -148,6 +151,9 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// The service used by the application to generate a two step login URL.
let twoStepLoginService: TwoStepLoginService

/// A factory protocol to create `UserVerificationHelper`s.
let userVerificationHelperFactory: UserVerificationHelperFactory

/// The repository used by the application to manage vault data for the UI layer.
let vaultRepository: VaultRepository

Expand Down Expand Up @@ -202,6 +208,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// - stateService: The service used by the application to manage account state.
/// - syncService: The service used to handle syncing vault data with the API.
/// - systemDevice: The object used by the application to retrieve information about this device.
/// - textAutofillHelperFactory: Factory to create `TextAutofillHelper`s.
/// - timeProvider: Provides the present time for TOTP Code Calculation.
/// - tokenService: The service used by the application to manage account access tokens.
/// - totpExpirationManagerFactory: The factory to create TOTP expiration managers.
Expand Down Expand Up @@ -250,12 +257,14 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
stateService: StateService,
syncService: SyncService,
systemDevice: SystemDevice,
textAutofillHelperFactory: TextAutofillHelperFactory,
timeProvider: TimeProvider,
tokenService: TokenService,
totpExpirationManagerFactory: TOTPExpirationManagerFactory,
totpService: TOTPService,
trustDeviceService: TrustDeviceService,
twoStepLoginService: TwoStepLoginService,
userVerificationHelperFactory: UserVerificationHelperFactory,
vaultRepository: VaultRepository,
vaultTimeoutService: VaultTimeoutService,
watchService: WatchService
Expand Down Expand Up @@ -297,12 +306,14 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
self.stateService = stateService
self.syncService = syncService
self.systemDevice = systemDevice
self.textAutofillHelperFactory = textAutofillHelperFactory
self.timeProvider = timeProvider
self.tokenService = tokenService
self.totpExpirationManagerFactory = totpExpirationManagerFactory
self.totpService = totpService
self.trustDeviceService = trustDeviceService
self.twoStepLoginService = twoStepLoginService
self.userVerificationHelperFactory = userVerificationHelperFactory
self.vaultRepository = vaultRepository
self.vaultTimeoutService = vaultTimeoutService
self.watchService = watchService
Expand Down Expand Up @@ -646,6 +657,20 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
vaultTimeoutService: vaultTimeoutService
)

let userVerificationHelperFactory = DefaultUserVerificationHelperFactory(
authRepository: authRepository,
errorReporter: errorReporter,
localAuthService: localAuthService
)

let textAutofillHelperFactory = DefaultTextAutofillHelperFactory(
authRepository: authRepository,
errorReporter: errorReporter,
eventService: eventService,
userVerificationHelperFactory: userVerificationHelperFactory,
vaultRepository: vaultRepository
)

let authenticatorDataStore = AuthenticatorBridgeDataStore(
errorReporter: errorReporter,
groupIdentifier: Bundle.main.sharedAppGroupIdentifier,
Expand Down Expand Up @@ -719,12 +744,14 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
stateService: stateService,
syncService: syncService,
systemDevice: UIDevice.current,
textAutofillHelperFactory: textAutofillHelperFactory,
timeProvider: timeProvider,
tokenService: tokenService,
totpExpirationManagerFactory: totpExpirationManagerFactory,
totpService: totpService,
trustDeviceService: trustDeviceService,
twoStepLoginService: twoStepLoginService,
userVerificationHelperFactory: userVerificationHelperFactory,
vaultRepository: vaultRepository,
vaultTimeoutService: vaultTimeoutService,
watchService: watchService
Expand Down
16 changes: 16 additions & 0 deletions BitwardenShared/Core/Platform/Services/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ typealias Services = HasAPIService
& HasSystemDevice
& HasTOTPExpirationManagerFactory
& HasTOTPService
& HasTextAutofillHelperFactory
& HasTimeProvider
& HasTrustDeviceService
& HasTwoStepLoginService
& HasUserVerificationHelperFactory
& HasVaultRepository
& HasVaultTimeoutService
& HasWatchService
Expand Down Expand Up @@ -297,6 +299,13 @@ protocol HasSystemDevice {
var systemDevice: SystemDevice { get }
}

/// Protocol for an object that provides a `TextAutofillHelperFactory`.
///
protocol HasTextAutofillHelperFactory {
/// Helper to create `TextAutofillHelper`s`.
var textAutofillHelperFactory: TextAutofillHelperFactory { get }
}

/// Protocol for an object that provides a `TimeProvider`.
///
protocol HasTimeProvider {
Expand Down Expand Up @@ -332,6 +341,13 @@ protocol HasTwoStepLoginService {
var twoStepLoginService: TwoStepLoginService { get }
}

/// Protocol for an object that provides a `UserVerificationHelperFactory`.
///
protocol HasUserVerificationHelperFactory {
/// A factory protocol to create `UserVerificationHelper`s.
var userVerificationHelperFactory: UserVerificationHelperFactory { get }
}

/// Protocol for an object that provides a `VaultRepository`.
///
protocol HasVaultRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Networking
@testable import BitwardenShared

extension ServiceContainer {
static func withMocks(
static func withMocks( // swiftlint:disable:this function_body_length
application: Application? = nil,
appSettingsStore: AppSettingsStore = MockAppSettingsStore(),
authRepository: AuthRepository = MockAuthRepository(),
Expand Down Expand Up @@ -41,12 +41,14 @@ extension ServiceContainer {
stateService: StateService = MockStateService(),
syncService: SyncService = MockSyncService(),
systemDevice: SystemDevice = MockSystemDevice(),
textAutofillHelperFactory: TextAutofillHelperFactory = MockTextAutofillHelperFactory(),
timeProvider: TimeProvider = MockTimeProvider(.currentTime),
trustDeviceService: TrustDeviceService = MockTrustDeviceService(),
tokenService: TokenService = MockTokenService(),
totpExpirationManagerFactory: TOTPExpirationManagerFactory = MockTOTPExpirationManagerFactory(),
totpService: TOTPService = MockTOTPService(),
twoStepLoginService: TwoStepLoginService = MockTwoStepLoginService(),
userVerificationHelperFactory: UserVerificationHelperFactory = MockUserVerificationHelperFactory(),
vaultRepository: VaultRepository = MockVaultRepository(),
vaultTimeoutService: VaultTimeoutService = MockVaultTimeoutService(),
watchService: WatchService = MockWatchService()
Expand Down Expand Up @@ -92,12 +94,14 @@ extension ServiceContainer {
stateService: stateService,
syncService: syncService,
systemDevice: systemDevice,
textAutofillHelperFactory: textAutofillHelperFactory,
timeProvider: timeProvider,
tokenService: tokenService,
totpExpirationManagerFactory: totpExpirationManagerFactory,
totpService: totpService,
trustDeviceService: trustDeviceService,
twoStepLoginService: twoStepLoginService,
userVerificationHelperFactory: userVerificationHelperFactory,
vaultRepository: vaultRepository,
vaultTimeoutService: vaultTimeoutService,
watchService: watchService
Expand Down
Loading

0 comments on commit 7f150f5

Please sign in to comment.