diff --git a/apps/kyc-demo/CHANGELOG.md b/apps/kyc-demo/CHANGELOG.md
index a549fdb7..54ee326d 100644
--- a/apps/kyc-demo/CHANGELOG.md
+++ b/apps/kyc-demo/CHANGELOG.md
@@ -1,5 +1,12 @@
# kyc-demo
+## 0.1.82
+
+### Patch Changes
+
+- Updated dependencies [6b21fed]
+ - @slashid/react@1.36.0
+
## 0.1.81
### Patch Changes
diff --git a/apps/kyc-demo/package.json b/apps/kyc-demo/package.json
index 5709e1fd..f6ba148a 100644
--- a/apps/kyc-demo/package.json
+++ b/apps/kyc-demo/package.json
@@ -1,7 +1,7 @@
{
"name": "kyc-demo",
"private": true,
- "version": "0.1.81",
+ "version": "0.1.82",
"type": "module",
"scripts": {
"start": "vite",
diff --git a/apps/react-nextjs/CHANGELOG.md b/apps/react-nextjs/CHANGELOG.md
index 740f3285..63abe584 100644
--- a/apps/react-nextjs/CHANGELOG.md
+++ b/apps/react-nextjs/CHANGELOG.md
@@ -1,5 +1,13 @@
# react-nextjs
+## 0.1.96
+
+### Patch Changes
+
+- Updated dependencies [6b21fed]
+ - @slashid/react@1.36.0
+ - demo-form@0.0.95
+
## 0.1.95
### Patch Changes
diff --git a/apps/react-nextjs/package.json b/apps/react-nextjs/package.json
index 19f2c591..6322354e 100644
--- a/apps/react-nextjs/package.json
+++ b/apps/react-nextjs/package.json
@@ -1,6 +1,6 @@
{
"name": "react-nextjs",
- "version": "0.1.95",
+ "version": "0.1.96",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/apps/react-remix/CHANGELOG.md b/apps/react-remix/CHANGELOG.md
index 12e0746a..22a32f43 100644
--- a/apps/react-remix/CHANGELOG.md
+++ b/apps/react-remix/CHANGELOG.md
@@ -1,5 +1,13 @@
# react-remix
+## 0.1.82
+
+### Patch Changes
+
+- Updated dependencies [6b21fed]
+ - @slashid/react@1.36.0
+ - demo-form@0.0.95
+
## 0.1.81
### Patch Changes
diff --git a/apps/react-remix/package.json b/apps/react-remix/package.json
index 2cc11fd8..07217cee 100644
--- a/apps/react-remix/package.json
+++ b/apps/react-remix/package.json
@@ -1,6 +1,6 @@
{
"name": "react-remix",
- "version": "0.1.81",
+ "version": "0.1.82",
"private": true,
"sideEffects": false,
"scripts": {
diff --git a/apps/react-vite/CHANGELOG.md b/apps/react-vite/CHANGELOG.md
index d0fc173b..4d280401 100644
--- a/apps/react-vite/CHANGELOG.md
+++ b/apps/react-vite/CHANGELOG.md
@@ -1,5 +1,13 @@
# react-vite
+## 0.0.102
+
+### Patch Changes
+
+- Updated dependencies [6b21fed]
+ - @slashid/react@1.36.0
+ - demo-form@0.0.95
+
## 0.0.101
### Patch Changes
diff --git a/apps/react-vite/package.json b/apps/react-vite/package.json
index 330cf11c..62b13782 100644
--- a/apps/react-vite/package.json
+++ b/apps/react-vite/package.json
@@ -1,7 +1,7 @@
{
"name": "react-vite",
"private": true,
- "version": "0.0.101",
+ "version": "0.0.102",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/apps/slashid-remix-impl/CHANGELOG.md b/apps/slashid-remix-impl/CHANGELOG.md
index 5e11ce01..6e4ca0f4 100644
--- a/apps/slashid-remix-impl/CHANGELOG.md
+++ b/apps/slashid-remix-impl/CHANGELOG.md
@@ -1,5 +1,11 @@
# my-remix-app
+## 0.0.39
+
+### Patch Changes
+
+- @slashid/remix@0.2.35
+
## 0.0.38
### Patch Changes
diff --git a/apps/slashid-remix-impl/package.json b/apps/slashid-remix-impl/package.json
index 59dca5d1..732ed1a4 100644
--- a/apps/slashid-remix-impl/package.json
+++ b/apps/slashid-remix-impl/package.json
@@ -1,7 +1,7 @@
{
"name": "slashid-remix-impl",
"private": true,
- "version": "0.0.38",
+ "version": "0.0.39",
"sideEffects": false,
"type": "module",
"scripts": {
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000..f70ea5c3
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,2 @@
+specs/
+plans/
\ No newline at end of file
diff --git a/packages/demo-form/CHANGELOG.md b/packages/demo-form/CHANGELOG.md
index 7fb4c033..788cbc6d 100644
--- a/packages/demo-form/CHANGELOG.md
+++ b/packages/demo-form/CHANGELOG.md
@@ -1,5 +1,12 @@
# demo-form
+## 0.0.95
+
+### Patch Changes
+
+- Updated dependencies [6b21fed]
+ - @slashid/react@1.36.0
+
## 0.0.94
### Patch Changes
diff --git a/packages/demo-form/package.json b/packages/demo-form/package.json
index 46ef28dc..e7e73d50 100644
--- a/packages/demo-form/package.json
+++ b/packages/demo-form/package.json
@@ -1,6 +1,6 @@
{
"name": "demo-form",
- "version": "0.0.94",
+ "version": "0.0.95",
"private": true,
"type": "module",
"module": "./dist/demo-form.es.js",
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index e1ad66f3..e97dd6f8 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,11 @@
# @slashid/react
+## 1.36.0
+
+### Minor Changes
+
+- 6b21fed: Add `provider.*` text keys so OIDC and SAML provider display names can be localized. The authenticating screen title now shows the same name as the initial screen button, including the factor `label`. The default name for `azuread` is now "Microsoft".
+
## 1.35.0
### Minor Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 88f25fa6..49212262 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@slashid/react",
- "version": "1.35.0",
+ "version": "1.36.0",
"private": false,
"publishConfig": {
"access": "public"
diff --git a/packages/react/src/components/form/authenticating/authenticating.css.ts b/packages/react/src/components/form/authenticating/authenticating.css.ts
index 54917d83..de41b3d7 100644
--- a/packages/react/src/components/form/authenticating/authenticating.css.ts
+++ b/packages/react/src/components/form/authenticating/authenticating.css.ts
@@ -17,10 +17,6 @@ export const promptResend = style({
gap: "4px",
});
-export const oidcTitle = style({
- textTransform: "capitalize",
-});
-
export const otpForm = style({
margin: "16px 0",
});
diff --git a/packages/react/src/components/form/authenticating/index.tsx b/packages/react/src/components/form/authenticating/index.tsx
index 19313b34..38100627 100644
--- a/packages/react/src/components/form/authenticating/index.tsx
+++ b/packages/react/src/components/form/authenticating/index.tsx
@@ -1,9 +1,12 @@
import {
isFactorOTP,
isFactorPassword,
+ isFactorSSO,
isFactorTOTP,
} from "../../../domain/handles";
import { Text } from "../../text";
+import { getProviderName } from "../../text/provider-name";
+import { useConfiguration } from "../../../hooks/use-configuration";
import { getAuthenticatingMessage } from "./messages";
import { OTPState } from "./otp";
@@ -35,6 +38,7 @@ const DELAY_BEFORE_RETRY = TIME_MS.second * 30;
const LoadingState = ({ flowState }: Props) => {
const { factor, handle } = flowState.context.config;
+ const { text } = useConfiguration();
const { title, message, tokens } = getAuthenticatingMessage(factor, handle);
const [showPrompt, setShowPrompt] = useState(true);
@@ -53,13 +57,8 @@ const LoadingState = ({ flowState }: Props) => {
t={title}
variant={{ size: "2xl-title", weight: "bold" }}
>
- {factor.method === "oidc" ? (
-
- {factor.options?.provider as unknown as string}
-
- ) : undefined}
- {factor.method === "saml" ? (
- SSO
+ {isFactorSSO(factor) ? (
+ {getProviderName(text, factor)}
) : undefined}
diff --git a/packages/react/src/components/form/initial/sso.test.tsx b/packages/react/src/components/form/initial/sso.test.tsx
new file mode 100644
index 00000000..dacc3e50
--- /dev/null
+++ b/packages/react/src/components/form/initial/sso.test.tsx
@@ -0,0 +1,146 @@
+import {
+ AuthnContextUpdateChallengeReceivedEvent,
+ User,
+} from "@slashid/slashid";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect } from "vitest";
+import { Form } from "..";
+import { TextConfig } from "../../text/constants";
+import { createTestUser, MockSlashID } from "../../test-utils";
+import { TestSlashIDProvider } from "../../../context/test-providers";
+import { ConfigurationProvider } from "../../../context/config-context";
+import { FactorConfiguration } from "../../../domain/types";
+
+function renderForm(
+ factors: FactorConfiguration[],
+ text?: Partial
+) {
+ let resolveLogin: ((u: User) => void) | undefined;
+ const sid = new MockSlashID({ oid: "oid", analyticsEnabled: false });
+ const logIn = vi.fn(() => {
+ sid.mockPublish("authnContextUpdateChallengeReceivedEvent", {
+ targetOrgId: "oid",
+ factor: factors[0] as AuthnContextUpdateChallengeReceivedEvent["factor"],
+ });
+ return new Promise((resolve) => {
+ resolveLogin = resolve;
+ });
+ });
+
+ render(
+
+
+
+
+
+ );
+
+ return { finishLogin: () => resolveLogin?.(createTestUser()) };
+}
+
+async function clickAndReadAuthenticatingTitle(buttonName: string) {
+ const user = userEvent.setup();
+ await user.click(screen.getByRole("button", { name: buttonName }));
+ return screen.findByRole("heading", { level: 1 });
+}
+
+const github: FactorConfiguration = {
+ method: "oidc",
+ options: { provider: "github", client_id: "github" },
+};
+
+const saml: FactorConfiguration = {
+ method: "saml",
+ options: { provider_credentials_id: "saml-1" },
+};
+
+describe("SSO provider names", () => {
+ test("renders the default OIDC provider name on the initial button and the authenticating title", async () => {
+ const { finishLogin } = renderForm([github]);
+
+ expect(
+ screen.getByRole("button", { name: "Sign in with GitHub" })
+ ).toBeInTheDocument();
+
+ const title = await clickAndReadAuthenticatingTitle("Sign in with GitHub");
+ expect(title).toHaveTextContent("Sign in with GitHub");
+
+ finishLogin();
+ });
+
+ test("renders an overridden OIDC provider name in both places", async () => {
+ const { finishLogin } = renderForm([github], {
+ "provider.github": "Octocat",
+ });
+
+ expect(
+ screen.getByRole("button", { name: "Sign in with Octocat" })
+ ).toBeInTheDocument();
+
+ const title = await clickAndReadAuthenticatingTitle("Sign in with Octocat");
+ expect(title).toHaveTextContent("Sign in with Octocat");
+
+ finishLogin();
+ });
+
+ test("prefers the factor label over the overridden provider name", async () => {
+ const { finishLogin } = renderForm([{ ...github, label: "Work GitHub" }], {
+ "provider.github": "Octocat",
+ });
+
+ expect(
+ screen.getByRole("button", { name: "Sign in with Work GitHub" })
+ ).toBeInTheDocument();
+
+ const title = await clickAndReadAuthenticatingTitle(
+ "Sign in with Work GitHub"
+ );
+ expect(title).toHaveTextContent("Sign in with Work GitHub");
+
+ finishLogin();
+ });
+
+ test("renders the default SAML name in both places", async () => {
+ const { finishLogin } = renderForm([saml]);
+
+ expect(
+ screen.getByRole("button", { name: "Sign in with SAML" })
+ ).toBeInTheDocument();
+
+ const title = await clickAndReadAuthenticatingTitle("Sign in with SAML");
+ expect(title).toHaveTextContent("Sign in with SAML");
+
+ finishLogin();
+ });
+
+ test("renders an overridden SAML name in both places", async () => {
+ const { finishLogin } = renderForm([saml], { "provider.saml": "Corp SSO" });
+
+ expect(
+ screen.getByRole("button", { name: "Sign in with Corp SSO" })
+ ).toBeInTheDocument();
+
+ const title = await clickAndReadAuthenticatingTitle(
+ "Sign in with Corp SSO"
+ );
+ expect(title).toHaveTextContent("Sign in with Corp SSO");
+
+ finishLogin();
+ });
+
+ test("prefers the SAML factor label over the overridden SAML name", async () => {
+ const { finishLogin } = renderForm([{ ...saml, label: "Acme" }], {
+ "provider.saml": "Corp SSO",
+ });
+
+ expect(
+ screen.getByRole("button", { name: "Sign in with Acme" })
+ ).toBeInTheDocument();
+
+ const title = await clickAndReadAuthenticatingTitle("Sign in with Acme");
+ expect(title).toHaveTextContent("Sign in with Acme");
+
+ finishLogin();
+ });
+});
diff --git a/packages/react/src/components/form/initial/sso.tsx b/packages/react/src/components/form/initial/sso.tsx
index d08fc145..fdbd1442 100644
--- a/packages/react/src/components/form/initial/sso.tsx
+++ b/packages/react/src/components/form/initial/sso.tsx
@@ -19,6 +19,7 @@ import {
FactorLabeledOIDC,
} from "../../../domain/types";
import { useConfiguration } from "../../../hooks/use-configuration";
+import { getProviderName } from "../../text/provider-name";
import * as styles from "./initial.css";
@@ -34,18 +35,6 @@ const PROVIDER_TO_ICON: Record = {
apple: ,
};
-const PROVIDER_TO_PRETTY_NAME: Record = {
- google: "Google",
- facebook: "Facebook",
- github: "GitHub",
- gitlab: "GitLab",
- line: "LINE",
- bitbucket: "Bitbucket",
- azuread: "Azure AD",
- okta: "Okta",
- apple: "Apple",
-};
-
export type SAMLProviderProps = {
provider: FactorCustomizableSAML;
handleClick: (factor: Factor) => void;
@@ -66,7 +55,7 @@ export function SAMLProvider({ provider: p, handleClick }: SAMLProviderProps) {
className={clsx("sid-saml--button")}
>
{text["initial.sso"]}
- {p.label || "SAML"}
+ {getProviderName(text, p)}
);
}
@@ -118,9 +107,7 @@ export function OIDCProvider({ provider: p, handleClick }: OIDCProviderProps) {
className={clsx("sid-oidc--button")}
>
{text["initial.oidc"]}
-
- {p.label || PROVIDER_TO_PRETTY_NAME[p.options?.provider]}
-
+ {getProviderName(text, p)}
);
}
diff --git a/packages/react/src/components/text/constants.ts b/packages/react/src/components/text/constants.ts
index 697fb87d..f7676436 100644
--- a/packages/react/src/components/text/constants.ts
+++ b/packages/react/src/components/text/constants.ts
@@ -179,6 +179,16 @@ export const TEXT = {
"factor.emailLink": "Email link",
"factor.smsLink": "SMS link",
"factor.password": "Password",
+ "provider.google": "Google",
+ "provider.facebook": "Facebook",
+ "provider.github": "GitHub",
+ "provider.gitlab": "GitLab",
+ "provider.line": "LINE",
+ "provider.bitbucket": "Bitbucket",
+ "provider.azuread": "Microsoft",
+ "provider.okta": "Okta",
+ "provider.apple": "Apple",
+ "provider.saml": "SAML",
"validationError.otp": "Please enter the OTP code.",
"validationError.email": "Please enter your email address.",
"validationError.phoneNumber": "Please enter your phone number.",
diff --git a/packages/react/src/components/text/provider-name.test.ts b/packages/react/src/components/text/provider-name.test.ts
new file mode 100644
index 00000000..d65c467f
--- /dev/null
+++ b/packages/react/src/components/text/provider-name.test.ts
@@ -0,0 +1,43 @@
+import { describe, test, expect } from "vitest";
+import { TEXT } from "./constants";
+import { getProviderName } from "./provider-name";
+
+describe("getProviderName", () => {
+ test("uses the factor label when present", () => {
+ expect(
+ getProviderName(TEXT, {
+ method: "oidc",
+ options: { provider: "github", client_id: "id" },
+ label: "Work GitHub",
+ })
+ ).toBe("Work GitHub");
+ });
+
+ test("uses the provider text key when there is no label", () => {
+ expect(
+ getProviderName(
+ { ...TEXT, "provider.github": "Octocat" },
+ { method: "oidc", options: { provider: "github", client_id: "id" } }
+ )
+ ).toBe("Octocat");
+ });
+
+ test("falls back to the raw provider id for an unknown provider", () => {
+ expect(
+ getProviderName(TEXT, {
+ method: "oidc",
+ // @ts-expect-error unknown provider id
+ options: { provider: "unknown-idp", client_id: "id" },
+ })
+ ).toBe("unknown-idp");
+ });
+
+ test("uses the saml text key for saml factors", () => {
+ expect(
+ getProviderName(
+ { ...TEXT, "provider.saml": "Corp SSO" },
+ { method: "saml", options: { provider_credentials_id: "id" } }
+ )
+ ).toBe("Corp SSO");
+ });
+});
diff --git a/packages/react/src/components/text/provider-name.ts b/packages/react/src/components/text/provider-name.ts
new file mode 100644
index 00000000..4007f4dc
--- /dev/null
+++ b/packages/react/src/components/text/provider-name.ts
@@ -0,0 +1,26 @@
+import { OAuthProvider } from "@slashid/slashid";
+import { FactorCustomizableSAML, FactorLabeledOIDC } from "../../domain/types";
+import { TEXT, TextConfig } from "./constants";
+
+type ProviderTextKey = `provider.${OAuthProvider}` | "provider.saml";
+
+// Fails to compile when the core SDK adds a provider without a default in TEXT.
+TEXT satisfies Record;
+
+export function getProviderTextKey(provider: OAuthProvider): ProviderTextKey {
+ return `provider.${provider}`;
+}
+
+export function getProviderName(
+ text: TextConfig,
+ factor: FactorLabeledOIDC | FactorCustomizableSAML
+): string {
+ if (factor.label) return factor.label;
+
+ if (factor.method === "saml") return text["provider.saml"];
+
+ const provider = factor.options?.provider;
+ if (!provider) return "";
+
+ return text[getProviderTextKey(provider)] ?? provider;
+}
diff --git a/packages/react/src/domain/types.ts b/packages/react/src/domain/types.ts
index 46857cbe..4b710d2c 100644
--- a/packages/react/src/domain/types.ts
+++ b/packages/react/src/domain/types.ts
@@ -91,7 +91,7 @@ export type FactorWithAllowedHandleTypes = TF & {
/**
* This makes it possible to add a label to the configured OIDC factors.
- * This is useful when you want to change the default display (capitalized provider name).
+ * This is useful when you want to change the default display (the matching "provider.*" text key).
*/
export type FactorLabeledOIDC = FactorOIDC & { label?: string };
diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md
index 85d0b89b..2b18aeb2 100644
--- a/packages/remix/CHANGELOG.md
+++ b/packages/remix/CHANGELOG.md
@@ -1,5 +1,12 @@
# @slashid/remix
+## 0.2.35
+
+### Patch Changes
+
+- Updated dependencies [6b21fed]
+ - @slashid/react@1.36.0
+
## 0.2.34
### Patch Changes
diff --git a/packages/remix/package.json b/packages/remix/package.json
index d2859488..75496f3a 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -1,6 +1,6 @@
{
"name": "@slashid/remix",
- "version": "0.2.34",
+ "version": "0.2.35",
"private": false,
"publishConfig": {
"access": "public"