diff --git a/web/src/admin/applications/wizard/ContextIdentity.ts b/web/src/admin/applications/wizard/ContextIdentity.ts
index 7a0b85b400a0..e821bc94c1ee 100644
--- a/web/src/admin/applications/wizard/ContextIdentity.ts
+++ b/web/src/admin/applications/wizard/ContextIdentity.ts
@@ -3,5 +3,5 @@ import { LocalTypeCreate } from "./steps/ProviderChoices.js";
import { createContext } from "@lit/context";
export const applicationWizardProvidersContext = createContext(
- Symbol.for("ak-application-wizard-providers-context"),
+ Symbol("ak-application-wizard-providers-context"),
);
diff --git a/web/src/common/api/middleware.ts b/web/src/common/api/middleware.ts
index a43e032a7193..50f35048f111 100644
--- a/web/src/common/api/middleware.ts
+++ b/web/src/common/api/middleware.ts
@@ -1,5 +1,5 @@
import { AKRequestPostEvent, APIRequestInfo } from "#common/api/events";
-import { autoDetectLanguage } from "#common/ui/locale/utils";
+import { formatAcceptLanguageHeader } from "#common/ui/locale/utils";
import { getCookie } from "#common/utils";
import { ConsoleLogger, Logger } from "#logger/browser";
@@ -74,11 +74,11 @@ export class LocaleMiddleware implements Middleware, Disposable {
return;
}
- this.#locale = event.detail.readyLocale;
+ this.#locale = formatAcceptLanguageHeader(event.detail.readyLocale);
};
- constructor(localeHint?: string) {
- this.#locale = autoDetectLanguage(localeHint);
+ constructor(languageTagHint: Intl.UnicodeBCP47LocaleIdentifier) {
+ this.#locale = formatAcceptLanguageHeader(languageTagHint);
window.addEventListener(LOCALE_STATUS_EVENT, this.#localeStatusListener);
}
diff --git a/web/src/common/ui/locale/format.ts b/web/src/common/ui/locale/format.ts
index 7fc9cfd73be8..5e2ab4460853 100644
--- a/web/src/common/ui/locale/format.ts
+++ b/web/src/common/ui/locale/format.ts
@@ -9,8 +9,6 @@ import {
import { safeParseLocale } from "#common/ui/locale/utils";
import { msg, str } from "@lit/localize";
-import { html } from "lit";
-import { repeat } from "lit/directives/repeat.js";
/**
* Safely get a minimized locale ID, with fallback for older browsers.
@@ -198,34 +196,41 @@ export function formatLocaleDisplayNames(
return entries.sort(createIntlCollator(activeLocaleTag, collatorOptions));
}
-export function renderLocaleDisplayNames(
- entries: LocaleDisplay[],
- activeLocaleTag: TargetLanguageTag | null,
+export function formatRelativeLocaleDisplayName(
+ languageTag: TargetLanguageTag,
+ localizedDisplayName: string,
+ relativeDisplayName: string,
) {
- return repeat(
- entries,
- ([languageTag]) => languageTag,
- ([languageTag, localizedDisplayName, relativeDisplayName]) => {
- const pseudo = languageTag === PseudoLanguageTag;
-
- const same =
- relativeDisplayName &&
- normalizeDisplayName(relativeDisplayName) ===
- normalizeDisplayName(localizedDisplayName);
-
- let localizedMessage = localizedDisplayName;
-
- if (!same && !pseudo) {
- localizedMessage = msg(str`${relativeDisplayName} (${localizedDisplayName})`, {
- id: "locale-option-localized-label",
- desc: "Locale option label showing the localized language name along with the native language name in parentheses.",
- });
- }
+ const pseudo = languageTag === PseudoLanguageTag;
+
+ const same =
+ relativeDisplayName &&
+ normalizeDisplayName(relativeDisplayName) === normalizeDisplayName(localizedDisplayName);
+
+ if (same || pseudo) {
+ return localizedDisplayName;
+ }
+
+ return msg(str`${relativeDisplayName} (${localizedDisplayName})`, {
+ id: "locale-option-localized-label",
+ desc: "Locale option label showing the localized language name along with the native language name in parentheses.",
+ });
+}
+
+/**
+ * Format the display name for the auto-detect locale option.
+ *
+ * @param detectedLocale The detected locale display, if any.
+ */
+export function formatAutoDetectLocaleDisplayName(detectedLocale?: LocaleDisplay | null) {
+ const prefix = msg("Auto-detect", {
+ id: "locale-auto-detect-option",
+ desc: "Label for the auto-detect locale option in language selection dropdown",
+ });
+
+ if (!detectedLocale) {
+ return prefix;
+ }
- return html`${pseudo ? html`
` : null}
- `;
- },
- );
+ return `${prefix} (${formatRelativeLocaleDisplayName(...detectedLocale)})`;
}
diff --git a/web/src/common/ui/locale/utils.ts b/web/src/common/ui/locale/utils.ts
index 66c41fbe4724..0cba58aef26e 100644
--- a/web/src/common/ui/locale/utils.ts
+++ b/web/src/common/ui/locale/utils.ts
@@ -157,8 +157,8 @@ export function getSessionLocale(): string | null {
/**
* Auto-detect the best locale to use from several sources.
*
- * @param localeHint An optional locale code hint.
- * @param fallbackLocaleCode An optional fallback locale code.
+ * @param languageTagHint An optional locale code hint.
+ * @param fallbackLanguageTag An optional fallback locale code.
* @returns The best-matching supported locale code.
*
* @remarks
@@ -172,8 +172,8 @@ export function getSessionLocale(): string | null {
* 6. The source locale (English)
*/
export function autoDetectLanguage(
- localeHint?: string,
- fallbackLocaleCode?: string,
+ languageTagHint?: Intl.UnicodeBCP47LocaleIdentifier,
+ fallbackLanguageTag?: Intl.UnicodeBCP47LocaleIdentifier,
): TargetLanguageTag {
let localeParam: string | null = null;
@@ -188,9 +188,9 @@ export function autoDetectLanguage(
const candidates = [
localeParam,
sessionLocale,
- localeHint,
- self.navigator?.language,
- fallbackLocaleCode,
+ languageTagHint,
+ ...(self.navigator?.languages || []),
+ fallbackLanguageTag,
].filter((item): item is string => !!item);
const firstSupportedLocale = findSupportedLocale(candidates);
@@ -198,8 +198,8 @@ export function autoDetectLanguage(
if (!firstSupportedLocale) {
console.debug(`authentik/locale: Falling back to source locale`, {
SourceLanguageTag,
- localeHint,
- fallbackLocaleCode,
+ languageTagHint,
+ fallbackLanguageTag,
candidates,
});
@@ -208,3 +208,28 @@ export function autoDetectLanguage(
return firstSupportedLocale;
}
+
+/**
+ * Given a locale code, format it for use in an `Accept-Language` header.
+ */
+export function formatAcceptLanguageHeader(languageTag: Intl.UnicodeBCP47LocaleIdentifier): string {
+ const [preferredLanguageTag, ...languageTags] = new Set([
+ languageTag,
+ ...(self.navigator?.languages || []),
+ SourceLanguageTag,
+ "*",
+ ]);
+
+ const fallbackCount = languageTags.length;
+
+ return [
+ preferredLanguageTag,
+ ...languageTags.map((tag, idx) => {
+ const weight = ((fallbackCount - idx) / (fallbackCount + 1)).toFixed(
+ fallbackCount > 9 ? 2 : 1,
+ );
+
+ return `${tag};q=${weight}`;
+ }),
+ ].join(", ");
+}
diff --git a/web/src/components/ak-wizard/WizardContexts.ts b/web/src/components/ak-wizard/WizardContexts.ts
index 2b9c2e3f7919..51ae845d8663 100644
--- a/web/src/components/ak-wizard/WizardContexts.ts
+++ b/web/src/components/ak-wizard/WizardContexts.ts
@@ -3,5 +3,5 @@ import type { WizardStepState } from "./types.js";
import { createContext } from "@lit/context";
export const wizardStepContext = createContext(
- Symbol.for("authentik-wizard-step-labels"),
+ Symbol("authentik-wizard-step-labels"),
);
diff --git a/web/src/elements/AuthenticatedInterface.ts b/web/src/elements/AuthenticatedInterface.ts
index e6a9ded7f746..9eba16b70863 100644
--- a/web/src/elements/AuthenticatedInterface.ts
+++ b/web/src/elements/AuthenticatedInterface.ts
@@ -3,16 +3,18 @@ import { NotificationsContextController } from "#elements/controllers/Notificati
import { SessionContextController } from "#elements/controllers/SessionContextController";
import { VersionContextController } from "#elements/controllers/VersionContextController";
import { Interface } from "#elements/Interface";
+import { LicenseContext } from "#elements/mixins/license";
import { NotificationsContext } from "#elements/mixins/notifications";
import { SessionContext } from "#elements/mixins/session";
+import { VersionContext } from "#elements/mixins/version";
export class AuthenticatedInterface extends Interface {
constructor() {
super();
- this.addController(new LicenseContextController(this));
+ this.addController(new LicenseContextController(this), LicenseContext);
this.addController(new SessionContextController(this), SessionContext);
- this.addController(new VersionContextController(this));
+ this.addController(new VersionContextController(this), VersionContext);
this.addController(new NotificationsContextController(this), NotificationsContext);
}
}
diff --git a/web/src/elements/Interface.ts b/web/src/elements/Interface.ts
index 21eaca63189d..fcf5f176860e 100644
--- a/web/src/elements/Interface.ts
+++ b/web/src/elements/Interface.ts
@@ -7,7 +7,9 @@ import { ConfigContextController } from "#elements/controllers/ConfigContextCont
import { ContextControllerRegistry } from "#elements/controllers/ContextControllerRegistry";
import { LocaleContextController } from "#elements/controllers/LocaleContextController";
import { ModalOrchestrationController } from "#elements/controllers/ModalOrchestrationController";
-import { ReactiveContextController } from "#elements/types";
+import { ReactiveContextController } from "#elements/controllers/ReactiveContextController";
+import { BrandingContext } from "#elements/mixins/branding";
+import { AuthentikConfigContext } from "#elements/mixins/config";
import { Context, ContextType } from "@lit/context";
import { ReactiveController } from "lit";
@@ -16,6 +18,14 @@ import { ReactiveController } from "lit";
* The base interface element for the application.
*/
export abstract class Interface extends AKElement {
+ /**
+ * Private map of controllers to their registry keys.
+ *
+ * This is used to track which controllers have been registered,
+ * and to unregister them when removed.
+ */
+ #registryKeys = new WeakMap>>();
+
constructor() {
super();
@@ -24,24 +34,44 @@ export abstract class Interface extends AKElement {
createUIThemeEffect(applyDocumentTheme);
this.addController(new LocaleContextController(this, locale));
- this.addController(new ConfigContextController(this, config));
- this.addController(new BrandingContextController(this, brand));
+ this.addController(new ConfigContextController(this, config), AuthentikConfigContext);
+ this.addController(new BrandingContextController(this, brand), BrandingContext);
this.addController(new ModalOrchestrationController());
+
+ this.dataset.testId = "interface-root";
}
public override addController(
controller: ReactiveController,
registryKey?: ContextType>,
): void {
- super.addController(controller);
+ if (controller instanceof ReactiveContextController) {
+ if (!registryKey) {
+ throw new TypeError(
+ `ReactiveContextController (${controller.constructor.name}) requires a registry key.`,
+ );
+ }
- if (registryKey) {
- ContextControllerRegistry.set(registryKey, controller as ReactiveContextController);
+ if (this.#registryKeys.has(controller)) {
+ throw new Error(
+ `Controller (${controller.constructor.name}) is already registered.`,
+ );
+ }
+
+ this.#registryKeys.set(controller, registryKey);
+ ContextControllerRegistry.set(registryKey, controller);
}
+ super.addController(controller);
}
- public connectedCallback(): void {
- super.connectedCallback();
- this.dataset.testId = "interface-root";
+ public override removeController(controller: ReactiveController): void {
+ super.removeController(controller);
+
+ const registryKey = this.#registryKeys.get(controller);
+
+ if (registryKey) {
+ ContextControllerRegistry.delete(registryKey);
+ this.#registryKeys.delete(controller);
+ }
}
}
diff --git a/web/src/elements/controllers/ContextControllerRegistry.ts b/web/src/elements/controllers/ContextControllerRegistry.ts
index bccd43e9aec1..9d48d3158be4 100644
--- a/web/src/elements/controllers/ContextControllerRegistry.ts
+++ b/web/src/elements/controllers/ContextControllerRegistry.ts
@@ -1,5 +1,34 @@
import { type ContextControllerRegistryMap } from "#elements/types";
+/**
+ * Check if the environment supports Symbol-keyed WeakMaps.
+ *
+ * @see {@link https://caniuse.com/mdn-javascript_builtins_weakmap_symbol_as_keys | Can I use}
+ *
+ * @todo Re-evaluate browser coverage after 2027-01-01
+ */
+function supportsSymbolKeyedWeakMap(): boolean {
+ const testKey = Symbol("test");
+ const wm = new WeakMap();
+
+ try {
+ wm.set(testKey, "value");
+ return wm.has(testKey);
+ } catch (_error) {
+ return false;
+ }
+}
+
+/**
+ * A constructor for either WeakMap or Map, depending on environment support.
+ *
+ * @remarks
+ *
+ * A preference for `WeakMap` is optional at the moment.
+ * However, if we ever support short-lived context controllers, such as
+ */
+const ContextControllerConstructor = supportsSymbolKeyedWeakMap() ? WeakMap : Map;
+
/**
* A registry of context controllers added to the Interface.
*
@@ -9,4 +38,5 @@ import { type ContextControllerRegistryMap } from "#elements/types";
*
* This is exported separately to avoid circular dependencies.
*/
-export const ContextControllerRegistry = new WeakMap() as ContextControllerRegistryMap;
+export const ContextControllerRegistry =
+ new ContextControllerConstructor() as ContextControllerRegistryMap;
diff --git a/web/src/elements/locale/ak-locale-select.ts b/web/src/elements/locale/ak-locale-select.ts
index 636a58a93128..f79c94a33e4e 100644
--- a/web/src/elements/locale/ak-locale-select.ts
+++ b/web/src/elements/locale/ak-locale-select.ts
@@ -1,9 +1,10 @@
import { TargetLanguageTag } from "#common/ui/locale/definitions";
-import { formatLocaleDisplayNames, renderLocaleDisplayNames } from "#common/ui/locale/format";
+import { formatLocaleDisplayNames } from "#common/ui/locale/format";
import { setSessionLocale } from "#common/ui/locale/utils";
import { AKElement } from "#elements/Base";
import Styles from "#elements/locale/ak-locale-select.css";
+import { LocaleOptions } from "#elements/locale/utils";
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { WithLocale } from "#elements/mixins/locale";
@@ -159,7 +160,7 @@ export class AKLocaleSelect extends WithLocale(WithCapabilitiesConfig(AKElement)
class="pf-c-form-control ak-m-capitalize"
name="locale"
>
- ${renderLocaleDisplayNames(entries, activeLocaleTag)}
+ ${LocaleOptions({ entries, activeLocaleTag })}
`;
});
}
diff --git a/web/src/elements/locale/utils.ts b/web/src/elements/locale/utils.ts
new file mode 100644
index 000000000000..1636a019f07b
--- /dev/null
+++ b/web/src/elements/locale/utils.ts
@@ -0,0 +1,36 @@
+import { PseudoLanguageTag, TargetLanguageTag } from "#common/ui/locale/definitions";
+import { formatRelativeLocaleDisplayName, LocaleDisplay } from "#common/ui/locale/format";
+
+import type { LitFC, SlottedTemplateResult } from "#elements/types";
+
+import { html } from "lit";
+import { repeat } from "lit/directives/repeat.js";
+
+export interface LocaleOptionsProps {
+ entries: Iterable;
+ activeLocaleTag: TargetLanguageTag | null;
+}
+
+/**
+ * Render locale display name options for a select element.
+ */
+export const LocaleOptions: LitFC = ({ entries, activeLocaleTag }) => {
+ return repeat(
+ entries,
+ ([languageTag]) => languageTag,
+ ([languageTag, localizedDisplayName, relativeDisplayName]) => {
+ const pseudo = languageTag === PseudoLanguageTag;
+
+ const localizedMessage = formatRelativeLocaleDisplayName(
+ languageTag,
+ localizedDisplayName,
+ relativeDisplayName,
+ );
+
+ return html`${pseudo ? html`
` : null}
+ `;
+ },
+ ) as SlottedTemplateResult;
+};
diff --git a/web/src/elements/mixins/branding.ts b/web/src/elements/mixins/branding.ts
index cd3fddeb4d6e..b33f5a33b37d 100644
--- a/web/src/elements/mixins/branding.ts
+++ b/web/src/elements/mixins/branding.ts
@@ -13,9 +13,7 @@ import { consume, Context, createContext } from "@lit/context";
* @see {@linkcode BrandingMixin}
* @see {@linkcode WithBrandConfig}
*/
-export const BrandingContext = createContext(
- Symbol.for("authentik-branding-context"),
-);
+export const BrandingContext = createContext(Symbol("authentik-branding-context"));
export type BrandingContext = Context;
diff --git a/web/src/elements/mixins/config.ts b/web/src/elements/mixins/config.ts
index 550b9945cfd0..7f89265c3d05 100644
--- a/web/src/elements/mixins/config.ts
+++ b/web/src/elements/mixins/config.ts
@@ -13,7 +13,7 @@ export const kAKConfig = Symbol("kAKConfig");
* @see {@linkcode AKConfigMixin}
* @see {@linkcode WithAuthentikConfig}
*/
-export const AuthentikConfigContext = createContext(Symbol.for("authentik-config-context"));
+export const AuthentikConfigContext = createContext(Symbol("authentik-config-context"));
export type AuthentikConfigContext = Context;
diff --git a/web/src/elements/mixins/license.ts b/web/src/elements/mixins/license.ts
index b5d125f7809f..104645b41fa0 100644
--- a/web/src/elements/mixins/license.ts
+++ b/web/src/elements/mixins/license.ts
@@ -4,9 +4,7 @@ import { type LicenseSummary, LicenseSummaryStatusEnum } from "@goauthentik/api"
import { consume, Context, createContext } from "@lit/context";
-export const LicenseContext = createContext(
- Symbol.for("authentik-license-context"),
-);
+export const LicenseContext = createContext(Symbol("authentik-license-context"));
export type LicenseContext = Context;
diff --git a/web/src/elements/mixins/locale.ts b/web/src/elements/mixins/locale.ts
index 4af490be472e..099c707f2336 100644
--- a/web/src/elements/mixins/locale.ts
+++ b/web/src/elements/mixins/locale.ts
@@ -16,9 +16,7 @@ export const kAKLocale = Symbol("kAKLocale");
* @see {@linkcode LocaleMixin}
* @see {@linkcode WithLocale}
*/
-export const LocaleContext = createContext(
- Symbol.for("authentik-locale-context"),
-);
+export const LocaleContext = createContext(Symbol("authentik-locale-context"));
export type LocaleContext = typeof LocaleContext;
diff --git a/web/src/elements/mixins/version.ts b/web/src/elements/mixins/version.ts
index 4cb707452f78..60652b8941ca 100644
--- a/web/src/elements/mixins/version.ts
+++ b/web/src/elements/mixins/version.ts
@@ -13,9 +13,7 @@ import { property } from "lit/decorators.js";
* @see {@linkcode WithVersion}
*/
-export const VersionContext = createContext(
- Symbol.for("authentik-version-context"),
-);
+export const VersionContext = createContext(Symbol("authentik-version-context"));
export type VersionContext = typeof VersionContext;
diff --git a/web/src/elements/types.ts b/web/src/elements/types.ts
index 23b95d04fbff..3be662a1e01e 100644
--- a/web/src/elements/types.ts
+++ b/web/src/elements/types.ts
@@ -100,6 +100,7 @@ export interface ContextControllerRegistryMap {
key: ContextType,
controller: ReactiveContextController,
): void;
+ delete>(key: ContextType): void;
}
export interface ReactiveControllerHostRegistry extends ReactiveControllerHost {
diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts
index 8fcbd814643b..dac9c3bea1a0 100644
--- a/web/src/flow/FlowExecutor.ts
+++ b/web/src/flow/FlowExecutor.ts
@@ -84,20 +84,26 @@ export class FlowExecutor
@property({ type: String, attribute: "slug", useDefault: true })
public flowSlug: string = window.location.pathname.split("/")[3];
- #challenge?: ChallengeTypes;
+ #challenge: ChallengeTypes | null = null;
@property({ attribute: false })
- public set challenge(value: ChallengeTypes | undefined) {
+ public set challenge(value: ChallengeTypes | null) {
+ const previousValue = this.#challenge;
+ const previousTitle = previousValue?.flowInfo?.title;
+ const nextTitle = value?.flowInfo?.title;
+
this.#challenge = value;
- if (value?.flowInfo?.title) {
- document.title = `${value.flowInfo?.title} - ${this.brandingTitle}`;
- } else {
+
+ if (!nextTitle) {
document.title = this.brandingTitle;
+ } else if (nextTitle !== previousTitle) {
+ document.title = `${nextTitle} - ${this.brandingTitle}`;
}
- this.requestUpdate();
+
+ this.requestUpdate("challenge", previousValue);
}
- public get challenge(): ChallengeTypes | undefined {
+ public get challenge(): ChallengeTypes | null {
return this.#challenge;
}
@@ -116,7 +122,7 @@ export class FlowExecutor
@property({ type: Boolean })
public inspectorAvailable?: boolean;
- @property({ type: String, attribute: "data-layout", useDefault: true })
+ @property({ type: String, attribute: "data-layout", useDefault: true, reflect: true })
public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout;
@state()
@@ -158,6 +164,8 @@ export class FlowExecutor
});
}
+ //#region Listeners
+
@listen(AKSessionAuthenticatedEvent)
protected sessionAuthenticatedListener = () => {
if (!document.hidden) {
@@ -174,11 +182,7 @@ export class FlowExecutor
WebsocketClient.close();
}
- public async firstUpdated(): Promise {
- if (this.can(CapabilitiesEnum.CanDebug)) {
- this.inspectorAvailable = true;
- }
-
+ protected refresh = () => {
this.loading = true;
return new FlowsApi(DEFAULT_CONFIG)
@@ -186,11 +190,7 @@ export class FlowExecutor
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
})
- .then((challenge: ChallengeTypes) => {
- if (this.inspectorOpen) {
- window.dispatchEvent(new AKFlowAdvanceEvent());
- }
-
+ .then((challenge) => {
this.challenge = challenge;
if (this.challenge.flowInfo) {
@@ -209,6 +209,20 @@ export class FlowExecutor
.finally(() => {
this.loading = false;
});
+ };
+
+ public async firstUpdated(changed: PropertyValues): Promise {
+ super.firstUpdated(changed);
+
+ if (this.can(CapabilitiesEnum.CanDebug)) {
+ this.inspectorAvailable = true;
+ }
+
+ this.refresh().then(() => {
+ if (this.inspectorOpen) {
+ window.dispatchEvent(new AKFlowAdvanceEvent());
+ }
+ });
}
// DOM post-processing has to happen after the render.
diff --git a/web/src/flow/stages/base.ts b/web/src/flow/stages/base.ts
index 565974a8738e..49d6fba61a15 100644
--- a/web/src/flow/stages/base.ts
+++ b/web/src/flow/stages/base.ts
@@ -7,6 +7,8 @@ import { FocusTarget } from "#elements/utils/focus";
import { FlowUserDetails } from "#flow/FormStatic";
+import { ConsoleLogger } from "#logger/browser";
+
import { ContextualFlowInfo, CurrentBrand, ErrorDetail } from "@goauthentik/api";
import { html, LitElement, nothing, PropertyValues } from "lit";
@@ -63,6 +65,8 @@ export abstract class BaseStage<
delegatesFocus: true,
};
+ protected logger = ConsoleLogger.prefix(`flow:${this.tagName.toLowerCase()}`);
+
// TODO: Should have a property but this needs some refactoring first.
// @property({ attribute: false })
public host!: StageHost;
@@ -135,18 +139,18 @@ export abstract class BaseStage<
}
}
- return this.host?.submit(payload).then((successful) => {
+ return this.host?.submit(payload).then(async (successful) => {
if (successful) {
- this.onSubmitSuccess();
+ await this.onSubmitSuccess?.(payload);
} else {
- this.onSubmitFailure();
+ await this.onSubmitFailure?.(payload);
}
return successful;
});
};
- renderNonFieldErrors() {
+ protected renderNonFieldErrors() {
const nonFieldErrors = this.challenge?.responseErrors?.non_field_errors;
if (!nonFieldErrors) {
@@ -171,7 +175,7 @@ export abstract class BaseStage<
`;
}
- renderUserInfo() {
+ protected renderUserInfo() {
if (!this.challenge.pendingUser || !this.challenge.pendingUserAvatar) {
return nothing;
}
@@ -186,12 +190,17 @@ export abstract class BaseStage<
`;
}
- onSubmitSuccess(): void {
- // Method that can be overridden by stages
- return;
- }
- onSubmitFailure(): void {
- // Method that can be overridden by stages
- return;
- }
+ /**
+ * Callback method for successful form submission.
+ *
+ * @abstract
+ */
+ protected onSubmitSuccess?(payload: Record): void | Promise;
+
+ /**
+ * Callback method for failed form submission.
+ *
+ * @abstract
+ */
+ protected onSubmitFailure?(payload: Record): void | Promise;
}
diff --git a/web/src/flow/stages/identification/IdentificationStage.ts b/web/src/flow/stages/identification/IdentificationStage.ts
index c017022f2c2d..c5d3a89d7328 100644
--- a/web/src/flow/stages/identification/IdentificationStage.ts
+++ b/web/src/flow/stages/identification/IdentificationStage.ts
@@ -307,11 +307,11 @@ export class IdentificationStage extends BaseStage<
//#endregion
- onSubmitSuccess(): void {
+ protected override onSubmitSuccess(): void {
this.#form?.remove();
}
- onSubmitFailure(): void {
+ protected override onSubmitFailure(): void {
const captchaInput = this.#captchaInputRef.value;
if (captchaInput) {
diff --git a/web/src/flow/stages/prompt/PromptStage.ts b/web/src/flow/stages/prompt/PromptStage.ts
index 6559274fddda..5f45e6acea78 100644
--- a/web/src/flow/stages/prompt/PromptStage.ts
+++ b/web/src/flow/stages/prompt/PromptStage.ts
@@ -1,17 +1,17 @@
import "#elements/Divider";
import "#flow/components/ak-flow-card";
-import { formatLocaleDisplayNames, renderLocaleDisplayNames } from "#common/ui/locale/format";
-import { getBestMatchLocale } from "#common/ui/locale/utils";
-
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
+import { SlottedTemplateResult } from "#elements/types";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseStage } from "#flow/stages/base";
+import { LocalePrompt } from "#flow/stages/prompt/components/locale";
import {
+ CapabilitiesEnum,
PromptChallenge,
PromptChallengeResponseRequest,
PromptTypeEnum,
@@ -19,7 +19,7 @@ import {
} from "@goauthentik/api";
import { msg } from "@lit/localize";
-import { css, CSSResult, html, nothing, TemplateResult } from "lit";
+import { css, CSSResult, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
@@ -33,9 +33,6 @@ import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
-// Fixes horizontal rule
warning in select dropdowns.
-/* eslint-disable lit/no-invalid-html */
-
@customElement("ak-stage-prompt")
export class PromptStage extends WithCapabilitiesConfig(
BaseStage,
@@ -59,7 +56,7 @@ export class PromptStage extends WithCapabilitiesConfig(
`,
];
- renderPromptInner(prompt: StagePrompt): TemplateResult {
+ protected renderPromptInner(prompt: StagePrompt): SlottedTemplateResult {
const fieldId = `field-${prompt.fieldKey}`;
switch (prompt.type) {
@@ -219,37 +216,19 @@ ${prompt.initialValue} `;
})}`;
case PromptTypeEnum.AkLocale: {
- const entries = formatLocaleDisplayNames(this.activeLanguageTag);
-
- const currentLanguageTag = prompt.initialValue
- ? getBestMatchLocale(prompt.initialValue)
- : null;
-
- return html``;
+ return LocalePrompt({
+ activeLanguageTag: this.activeLanguageTag,
+ prompt,
+ fieldId,
+ debug: this.can(CapabilitiesEnum.CanDebug),
+ });
}
default:
return html`invalid type '${prompt.type}'
`;
}
}
- renderPromptHelpText(prompt: StagePrompt) {
+ protected renderPromptHelpText(prompt: StagePrompt) {
if (!prompt.subText) {
return nothing;
}
@@ -257,7 +236,7 @@ ${prompt.initialValue}${unsafeHTML(prompt.subText)}
`;
}
- shouldRenderInWrapper(prompt: StagePrompt): boolean {
+ protected shouldRenderInWrapper(prompt: StagePrompt): boolean {
// Special types that aren't rendered in a wrapper
return !(
prompt.type === PromptTypeEnum.Static ||
@@ -266,7 +245,7 @@ ${prompt.initialValue}
@@ -303,7 +282,7 @@ ${prompt.initialValue}