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

[PM-11153] Implement context menu text autofill #1178

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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: 2 additions & 0 deletions BitwardenAutoFillExtension/Application/Support/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
<true/>
<key>ProvidesPasswords</key>
<true/>
<key>ProvidesTextToInsert</key>
<true/>
<key>ShowsConfigurationUI</key>
<true/>
</dict>
Expand Down
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,9 @@ class CredentialProviderContextTests: BitwardenTestCase {
DefaultCredentialProviderContext(.autofillFido2VaultList([], MockPasskeyCredentialRequestParameters()))
.passwordCredentialIdentity
)
XCTAssertNil(
DefaultCredentialProviderContext(.autofillText)
)
XCTAssertNil(
DefaultCredentialProviderContext(.configureAutofill)
.passwordCredentialIdentity
Expand Down Expand Up @@ -214,6 +231,8 @@ class CredentialProviderContextTests: BitwardenTestCase {
)
XCTAssertFalse(subjectOTPCredentialFalse.flowWithUserInteraction)

XCTAssertTrue(DefaultCredentialProviderContext(.autofillText).flowWithUserInteraction)

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

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

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

Expand Down
24 changes: 24 additions & 0 deletions BitwardenShared/Core/Platform/Services/ServiceContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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

/// Helper to autofill text from any cipher type.
let textAutofillHelper: TextAutofillHelper

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

Expand Down Expand Up @@ -198,6 +201,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.
/// - textAutofillHelper: Helper to autofill text from any cipher type.
/// - 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 @@ -245,6 +249,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
stateService: StateService,
syncService: SyncService,
systemDevice: SystemDevice,
textAutofillHelper: TextAutofillHelper,
timeProvider: TimeProvider,
tokenService: TokenService,
totpExpirationManagerFactory: TOTPExpirationManagerFactory,
Expand Down Expand Up @@ -291,6 +296,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
self.stateService = stateService
self.syncService = syncService
self.systemDevice = systemDevice
self.textAutofillHelper = textAutofillHelper
self.timeProvider = timeProvider
self.tokenService = tokenService
self.totpExpirationManagerFactory = totpExpirationManagerFactory
Expand Down Expand Up @@ -635,6 +641,23 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
vaultTimeoutService: vaultTimeoutService
)

let textAutofillHelper: TextAutofillHelper
if #available(iOS 18.0, *) {
textAutofillHelper = DefaultTextAutofillHelper(
authRepository: authRepository,
errorReporter: errorReporter,
eventService: eventService,
userVerificationHelper: DefaultUserVerificationHelper(
authRepository: authRepository,
errorReporter: errorReporter,
localAuthService: localAuthService
),
vaultRepository: vaultRepository
)
} else {
textAutofillHelper = NoOpTextAutofillHelper()
}

let authenticatorDataStore = AuthenticatorBridgeDataStore(
errorReporter: errorReporter,
groupIdentifier: Bundle.main.sharedAppGroupIdentifier,
Expand Down Expand Up @@ -707,6 +730,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
stateService: stateService,
syncService: syncService,
systemDevice: UIDevice.current,
textAutofillHelper: textAutofillHelper,
timeProvider: timeProvider,
tokenService: tokenService,
totpExpirationManagerFactory: totpExpirationManagerFactory,
Expand Down
8 changes: 8 additions & 0 deletions BitwardenShared/Core/Platform/Services/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ typealias Services = HasAPIService
& HasSystemDevice
& HasTOTPExpirationManagerFactory
& HasTOTPService
& HasTextAutofillHelper
& HasTimeProvider
& HasTrustDeviceService
& HasTwoStepLoginService
Expand Down Expand Up @@ -290,6 +291,13 @@ protocol HasSystemDevice {
var systemDevice: SystemDevice { get }
}

/// Protocol for an object that provides a `TextAutofillHelper`.
///
protocol HasTextAutofillHelper {
/// Helper to autofill text from any cipher type.
var textAutofillHelper: TextAutofillHelper { get }
}

/// Protocol for an object that provides a `TimeProvider`.
///
protocol HasTimeProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ extension ServiceContainer {
stateService: StateService = MockStateService(),
syncService: SyncService = MockSyncService(),
systemDevice: SystemDevice = MockSystemDevice(),
textAutofillHelper: TextAutofillHelper = MockTextAutofillHelper(),
timeProvider: TimeProvider = MockTimeProvider(.currentTime),
trustDeviceService: TrustDeviceService = MockTrustDeviceService(),
tokenService: TokenService = MockTokenService(),
Expand Down Expand Up @@ -90,6 +91,7 @@ extension ServiceContainer {
stateService: stateService,
syncService: syncService,
systemDevice: systemDevice,
textAutofillHelper: textAutofillHelper,
timeProvider: timeProvider,
tokenService: tokenService,
totpExpirationManagerFactory: totpExpirationManagerFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,8 @@ extension DefaultVaultRepository: VaultRepository {
searchText: String?
) async throws -> [VaultListSection] {
switch mode {
case .all:
return []
case .combinedMultipleSections, .passwords:
var sections = [VaultListSection]()
if #available(iOSApplicationExtension 17.0, *),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public protocol AutofillAppExtensionDelegate: AppExtensionDelegate {
@available(iOSApplicationExtension 17.0, *)
func completeRegistrationRequest(asPasskeyRegistrationCredential: ASPasskeyRegistrationCredential)

/// Completes the text request with some text to insert.
@available(iOSApplicationExtension 18.0, *)
func completeTextRequest(text: String)

/// Gets a publisher for when `didAppear` happens.
func getDidAppearPublisher() -> AsyncPublisher<AnyPublisher<Bool, Never>>

Expand All @@ -41,6 +45,8 @@ extension AutofillAppExtensionDelegate {
.combinedMultipleSections
case .autofillOTP:
.totp
case .autofillText:
.all
case .registerFido2Credential:
.combinedSingleSection
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class MockAutofillAppExtensionDelegate: MockAppExtensionDelegate, AutofillAppExt
var completeAssertionRequestMocker = InvocationMocker<ASPasskeyAssertionCredential>()
var completeOTPRequestCodeCalled: String?
var completeRegistrationRequestMocker = InvocationMocker<ASPasskeyRegistrationCredential>()
var completeTextRequestTextToInsert: String?
var extensionMode: AutofillExtensionMode = .configureAutofill
var didAppearPublisher = CurrentValueSubject<Bool, Never>(false)
var setUserInteractionRequiredCalled = false
Expand All @@ -27,6 +28,11 @@ class MockAutofillAppExtensionDelegate: MockAppExtensionDelegate, AutofillAppExt
completeRegistrationRequestMocker.invoke(param: asPasskeyRegistrationCredential)
}

@available(iOSApplicationExtension 18.0, *)
func completeTextRequest(text: String) {
completeTextRequestTextToInsert = text
}

func getDidAppearPublisher() -> AsyncPublisher<AnyPublisher<Bool, Never>> {
didAppearPublisher
.eraseToAnyPublisher()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BitwardenSdk

@testable import BitwardenShared

class MockTextAutofillHelper: TextAutofillHelper {

Check failure on line 5 in BitwardenShared/UI/Vault/Helpers/TestHelpers/MockTextAutofillHelper.swift

View workflow job for this annotation

GitHub Actions / Test

type 'MockTextAutofillHelper' does not conform to protocol 'TextAutofillHelper'
var handleCipherForAutofillCalledWithCipher: CipherView?
var textAutofillHelperDelegate: (any TextAutofillHelperDelegate)?

func handleCipherForAutofill(cipherView: CipherView) async {
handleCipherForAutofillCalledWithCipher = cipherView
}
}
Loading
Loading