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"