diff --git a/app/assets/javascripts/discourse/app/components/modal/login.hbs b/app/assets/javascripts/discourse/app/components/modal/login.hbs
index acb9696aea8..83e42cc2b89 100644
--- a/app/assets/javascripts/discourse/app/components/modal/login.hbs
+++ b/app/assets/javascripts/discourse/app/components/modal/login.hbs
@@ -15,7 +15,10 @@
@createAccount={{this.createAccount}}
/>
{{#if this.showLoginButtons}}
-
+
{{/if}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/modal/login/login-test.js b/app/assets/javascripts/discourse/tests/acceptance/modal/login/login-test.js
index 85ce8213ad3..d1b07457627 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/modal/login/login-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/modal/login/login-test.js
@@ -1,5 +1,6 @@
import { click, fillIn, tab, visit } from "@ember/test-helpers";
import { test } from "qunit";
+import sinon from "sinon";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
@@ -82,3 +83,37 @@ acceptance("Modal - Login - With Passkeys disabled", function (needs) {
assert.dom("#login-account-name").hasAttribute("autocomplete", "username");
});
});
+
+acceptance("Modal - Login - Passkeys on mobile", function (needs) {
+ needs.mobileView();
+ needs.settings({
+ experimental_passkeys: true,
+ });
+
+ needs.pretender((server, helper) => {
+ server.get(`/session/passkey/challenge.json`, () =>
+ helper.response({
+ challenge: "some-challenge",
+ })
+ );
+ });
+
+ test("Includes passkeys button and conditional UI", async function (assert) {
+ await visit("/");
+ await click("header .login-button");
+
+ sinon.stub(navigator.credentials, "get").callsFake(function () {
+ return Promise.reject(new Error("credentials.get got called"));
+ });
+
+ assert
+ .dom("#login-account-name")
+ .hasAttribute("autocomplete", "username webauthn");
+
+ await click(".passkey-login-button");
+
+ // clicking the button triggers credentials.get
+ // but we can't really test that in frontend so an error is returned
+ assert.dom(".dialog-body").exists();
+ });
+});
diff --git a/spec/system/user_page/user_preferences_security_spec.rb b/spec/system/user_page/user_preferences_security_spec.rb
index ccd7a10e9c8..2177ccd8664 100644
--- a/spec/system/user_page/user_preferences_security_spec.rb
+++ b/spec/system/user_page/user_preferences_security_spec.rb
@@ -15,7 +15,7 @@ describe "User preferences for Security", type: :system do
DiscourseWebauthn.stubs(:origin).returns(current_host + ":" + Capybara.server_port.to_s)
end
- describe "Security keys" do
+ shared_examples "security keys" do
it "adds a 2FA security key and logs in with it" do
options = ::Selenium::WebDriver::VirtualAuthenticatorOptions.new
authenticator = page.driver.browser.add_virtual_authenticator(options)
@@ -48,7 +48,7 @@ describe "User preferences for Security", type: :system do
end
end
- describe "Passkeys" do
+ shared_examples "passkeys" do
before { SiteSetting.experimental_passkeys = true }
it "adds a passkey and logs in with it" do
@@ -88,8 +88,9 @@ describe "User preferences for Security", type: :system do
user_menu.sign_out
# login with the key we just created
+ # this triggers the conditional UI for passkeys
+ # which uses the virtual authenticator
find(".d-header .login-button").click
- find(".passkey-login-button").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
@@ -97,4 +98,14 @@ describe "User preferences for Security", type: :system do
authenticator.remove!
end
end
+
+ context "when desktop" do
+ include_examples "security keys"
+ include_examples "passkeys"
+ end
+
+ context "when mobile", mobile: true do
+ include_examples "security keys"
+ include_examples "passkeys"
+ end
end