[HybridInAutofill] Make PasswordSuggestionGenerator reusable in Autofill
This CL refactors PasswordSuggestionGenerator to create the hybrid
passkey suggestion in a dedicated method. This allows to reuse the exact
same item in Autofill dropdowns that is used in Password dropdowns.
Prototype Screenshot: http://screen/7RacahewbUDhBVc
Prototype: crrev.com/c/6836380
Feature: AutofillReintroduceHybridPasskeyDropdownItem
Bug: 399124614
Change-Id: I257f077cc16ecb725db85e75e1148f17173bbb02
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6860605
Reviewed-by: Mohamed Amir Yosef <mamir@chromium.org>
Commit-Queue: Friedrich Hauser <friedrichh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1503976}
diff --git a/components/password_manager/core/browser/password_suggestion_generator.cc b/components/password_manager/core/browser/password_suggestion_generator.cc
index 1be8a3b2..e65264eb 100644
--- a/components/password_manager/core/browser/password_suggestion_generator.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator.cc
@@ -388,6 +388,24 @@
}
#endif
+
+#if !BUILDFLAG(IS_ANDROID)
+bool ShowPasskeysFromAnotherDeviceInAutofill() {
+#if BUILDFLAG(IS_IOS)
+ return true;
+#else
+ // Show the hybrid passkey item if the context menu experiment (which moves
+ // this option) is not enabled, or if the feature to reintroduce it to the
+ // dropdown is explicitly enabled.
+ return !base::FeatureList::IsEnabled(
+ password_manager::features::
+ kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu) ||
+ base::FeatureList::IsEnabled(
+ password_manager::features::
+ kAutofillReintroduceHybridPasskeyDropdownItem);
+#endif // BUILDFLAG(IS_IOS)
+}
+#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
PasswordSuggestionGenerator::PasswordSuggestionGenerator(
@@ -470,20 +488,8 @@
#if !BUILDFLAG(IS_ANDROID)
// Add "Use a passkey" or "Use a different passkey" button.
- if (uses_passkeys && delegate->IsSecurityKeyOrHybridFlowAvailable()) {
-#if !BUILDFLAG(IS_IOS)
- const bool passkey_from_another_device_in_autofill =
- !(base::FeatureList::IsEnabled(
- features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu));
-#else
- const bool passkey_from_another_device_in_autofill = true;
-#endif //! BUILDFLAG(IS_IOS)
- if (passkey_from_another_device_in_autofill) {
- bool listed_passkeys = delegate->GetPasskeys().has_value() &&
- delegate->GetPasskeys().value()->size() > 0;
- suggestions.emplace_back(
- CreatePasskeyFromAnotherDeviceEntry(listed_passkeys));
- }
+ if (auto hybrid_suggestion = GetWebauthnSignInWithAnotherDeviceSuggestion()) {
+ suggestions.push_back(std::move(hybrid_suggestion.value()));
}
#endif // !BUILDFLAG(IS_ANDROID)
@@ -631,4 +637,23 @@
return suggestions;
}
+std::optional<autofill::Suggestion>
+PasswordSuggestionGenerator::GetWebauthnSignInWithAnotherDeviceSuggestion()
+ const {
+#if BUILDFLAG(IS_ANDROID)
+ return std::nullopt;
+#else // BUILDFLAG(IS_ANDROID)
+ WebAuthnCredentialsDelegate* delegate =
+ password_client_->GetWebAuthnCredentialsDelegateForDriver(
+ password_manager_driver_);
+ if (!delegate || !delegate->GetPasskeys().has_value() ||
+ !delegate->IsSecurityKeyOrHybridFlowAvailable() ||
+ !ShowPasskeysFromAnotherDeviceInAutofill()) {
+ return std::nullopt;
+ }
+ return CreatePasskeyFromAnotherDeviceEntry(
+ /*listed_passkeys=*/delegate->GetPasskeys().value()->size() > 0);
+#endif // BUILDFLAG(IS_ANDROID)
+}
+
} // namespace password_manager
diff --git a/components/password_manager/core/browser/password_suggestion_generator.h b/components/password_manager/core/browser/password_suggestion_generator.h
index cedbbd6b..a191207 100644
--- a/components/password_manager/core/browser/password_suggestion_generator.h
+++ b/components/password_manager/core/browser/password_suggestion_generator.h
@@ -107,6 +107,11 @@
base::span<const CredentialUIEntry> all_credentials,
IsTriggeredOnPasswordForm on_password_form) const;
+ // Returns a `kWebauthnSignInWithAnotherDevice` suggestion if it should be
+ // shown for the current context.
+ std::optional<autofill::Suggestion>
+ GetWebauthnSignInWithAnotherDeviceSuggestion() const;
+
private:
const raw_ptr<PasswordManagerDriver> password_manager_driver_;
const raw_ptr<PasswordManagerClient> password_client_;
diff --git a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
index d4d4eee..9906694 100644
--- a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
@@ -1756,4 +1756,128 @@
EqualsManagePasswordsSuggestion()));
}
+#if !BUILDFLAG(IS_ANDROID)
+TEST_F(PasswordSuggestionGeneratorTest,
+ GetWebauthnSignInWithAnotherDeviceSuggestion) {
+#if !BUILDFLAG(IS_IOS)
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu);
+#endif // !BUILDFLAG(IS_IOS)
+ const std::vector<PasskeyCredential> passkeys;
+ ON_CALL(credentials_delegate(), GetPasskeys)
+ .WillByDefault(Return(base::ok(&passkeys)));
+ ON_CALL(credentials_delegate(), IsSecurityKeyOrHybridFlowAvailable)
+ .WillByDefault(Return(true));
+
+ std::optional<Suggestion> suggestion =
+ generator().GetWebauthnSignInWithAnotherDeviceSuggestion();
+ ASSERT_TRUE(suggestion.has_value());
+ EXPECT_THAT(*suggestion,
+ EqualsSuggestion(
+ SuggestionType::kWebauthnSignInWithAnotherDevice,
+ l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_USE_PASSKEY),
+ Suggestion::Icon::kDevice));
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
+ GetWebauthnSignInWithAnotherDeviceSuggestionWithListedPasskeys) {
+#if !BUILDFLAG(IS_IOS)
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu);
+#endif // !BUILDFLAG(IS_IOS)
+ const std::vector<PasskeyCredential> passkeys = {
+ passkey_credential(PasskeyCredential::Source::kWindowsHello, "username")};
+ ON_CALL(credentials_delegate(), GetPasskeys)
+ .WillByDefault(Return(base::ok(&passkeys)));
+ ON_CALL(credentials_delegate(), IsSecurityKeyOrHybridFlowAvailable)
+ .WillByDefault(Return(true));
+
+ std::optional<Suggestion> suggestion =
+ generator().GetWebauthnSignInWithAnotherDeviceSuggestion();
+ ASSERT_TRUE(suggestion.has_value());
+ EXPECT_THAT(*suggestion,
+ EqualsSuggestion(SuggestionType::kWebauthnSignInWithAnotherDevice,
+ l10n_util::GetStringUTF16(
+ IDS_PASSWORD_MANAGER_USE_DIFFERENT_PASSKEY),
+ Suggestion::Icon::kDevice));
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
+ GetWebauthnSignInWithAnotherDeviceSuggestionWhenHybridFlagIsReenabled) {
+#if !BUILDFLAG(IS_IOS)
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitWithFeatures(
+ {features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu,
+ features::kAutofillReintroduceHybridPasskeyDropdownItem},
+ {});
+#endif // !BUILDFLAG(IS_IOS)
+ const std::vector<PasskeyCredential> passkeys;
+ ON_CALL(credentials_delegate(), GetPasskeys)
+ .WillByDefault(Return(base::ok(&passkeys)));
+ ON_CALL(credentials_delegate(), IsSecurityKeyOrHybridFlowAvailable)
+ .WillByDefault(Return(true));
+
+ std::optional<Suggestion> suggestion =
+ generator().GetWebauthnSignInWithAnotherDeviceSuggestion();
+ ASSERT_TRUE(suggestion.has_value());
+}
+
+#if !BUILDFLAG(IS_IOS)
+TEST_F(PasswordSuggestionGeneratorTest,
+ NoWebauthnSignInWithAnotherDeviceSuggestionWhenHybridIsOff) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu);
+ const std::vector<PasskeyCredential> passkeys;
+ ON_CALL(credentials_delegate(), GetPasskeys)
+ .WillByDefault(Return(base::ok(&passkeys)));
+ ON_CALL(credentials_delegate(), IsSecurityKeyOrHybridFlowAvailable)
+ .WillByDefault(Return(true));
+
+ std::optional<Suggestion> suggestion =
+ generator().GetWebauthnSignInWithAnotherDeviceSuggestion();
+ EXPECT_FALSE(suggestion.has_value());
+}
+#endif // !BUILDFLAG(IS_IOS)
+
+TEST_F(PasswordSuggestionGeneratorTest,
+ NoWebauthnSignInWithAnotherDeviceSuggestionWhenNoPasskeys) {
+#if !BUILDFLAG(IS_IOS)
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu);
+#endif // !BUILDFLAG(IS_IOS)
+ ON_CALL(credentials_delegate(), GetPasskeys)
+ .WillByDefault(Return(
+ base::unexpected(WebAuthnCredentialsDelegate::
+ PasskeysUnavailableReason::kNotReceived)));
+ ON_CALL(credentials_delegate(), IsSecurityKeyOrHybridFlowAvailable)
+ .WillByDefault(Return(true));
+
+ std::optional<Suggestion> suggestion =
+ generator().GetWebauthnSignInWithAnotherDeviceSuggestion();
+ EXPECT_FALSE(suggestion.has_value());
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
+ NoWebauthnSignInWithAnotherDeviceSuggestionWhenHybridFlowUnavailable) {
+#if !BUILDFLAG(IS_IOS)
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu);
+#endif // !BUILDFLAG(IS_IOS)
+ const std::vector<PasskeyCredential> passkeys;
+ ON_CALL(credentials_delegate(), GetPasskeys)
+ .WillByDefault(Return(base::ok(&passkeys)));
+ ON_CALL(credentials_delegate(), IsSecurityKeyOrHybridFlowAvailable)
+ .WillByDefault(Return(false));
+
+ std::optional<Suggestion> suggestion =
+ generator().GetWebauthnSignInWithAnotherDeviceSuggestion();
+ EXPECT_FALSE(suggestion.has_value());
+}
+#endif // !BUILDFLAG(IS_ANDROID)
+
} // namespace password_manager