From 59211cf1df79d2761cd7352836297fad27713393 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 30 Jul 2026 17:37:08 +0530 Subject: [PATCH] Add "ThunderID" attribution badge support --- .../browser/src/branding/attributionDesign.ts | 150 ++++++++++++++++++ .../src/branding/resolveShowAttribution.ts | 49 ++++++ packages/browser/src/index.ts | 15 ++ packages/browser/src/theme/elevatedSurface.ts | 66 ++++++++ packages/javascript/src/i18n/models/i18n.ts | 1 + .../javascript/src/i18n/translations/en-US.ts | 1 + .../javascript/src/i18n/translations/fr-FR.ts | 1 + .../javascript/src/i18n/translations/hi-IN.ts | 1 + .../javascript/src/i18n/translations/ja-JP.ts | 1 + .../javascript/src/i18n/translations/pt-BR.ts | 1 + .../javascript/src/i18n/translations/pt-PT.ts | 1 + .../javascript/src/i18n/translations/si-LK.ts | 1 + .../javascript/src/i18n/translations/ta-IN.ts | 1 + .../javascript/src/i18n/translations/te-IN.ts | 1 + packages/javascript/src/models/config.ts | 8 + .../UserDropdown/BaseUserDropdown.styles.ts | 15 +- .../UserProfile/BaseUserProfile.styles.ts | 11 +- .../auth/Recovery/BaseRecovery.tsx | 28 +++- .../auth/SignIn/BaseSignIn.styles.ts | 11 +- .../presentation/auth/SignIn/BaseSignIn.tsx | 31 +++- .../auth/SignUp/BaseSignUp.styles.ts | 11 +- .../presentation/auth/SignUp/BaseSignUp.tsx | 29 +++- .../primitives/Attribution/Attribution.tsx | 81 ++++++++++ .../primitives/Attribution/index.ts | 20 +++ .../contexts/ThunderID/ThunderIDContext.ts | 7 + .../contexts/ThunderID/ThunderIDProvider.tsx | 3 + packages/react/src/hooks/useAttribution.ts | 38 +++++ packages/react/src/index.ts | 5 + 28 files changed, 561 insertions(+), 27 deletions(-) create mode 100644 packages/browser/src/branding/attributionDesign.ts create mode 100644 packages/browser/src/branding/resolveShowAttribution.ts create mode 100644 packages/browser/src/theme/elevatedSurface.ts create mode 100644 packages/react/src/components/primitives/Attribution/Attribution.tsx create mode 100644 packages/react/src/components/primitives/Attribution/index.ts create mode 100644 packages/react/src/hooks/useAttribution.ts diff --git a/packages/browser/src/branding/attributionDesign.ts b/packages/browser/src/branding/attributionDesign.ts new file mode 100644 index 0000000..4e8c7d8 --- /dev/null +++ b/packages/browser/src/branding/attributionDesign.ts @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {bem, withVendorCSSClassPrefix} from '@thunderid/javascript'; + +/** + * Design tokens for the "Powered by ThunderID" attribution badge. + * + * This is intentionally a fixed brand mark, not a themeable/white-labelable surface: + * the badge always renders in ThunderID's own colors regardless of the host + * application's theme, the same way "Secured by Clerk" or "Powered by Vercel" badges + * stay on-brand for the vendor rather than the tenant. Only the two seam-blending + * values (border + shadow) borrow from the surrounding card's theme. + * + * Framework packages (`@thunderid/react`, `@thunderid/vue`, ...) should render from + * these shared tokens instead of re-deriving the badge's colors, geometry, or mark — + * this file is the single source of truth so the badge stays pixel- and color-identical + * across every framework SDK. + */ +export const ATTRIBUTION_BADGE_COLORS: {background: string; text: string} = { + background: 'rgba(9, 20, 44, 0.98)', + text: 'rgba(139, 249, 250, 0.85)', +}; + +/** + * The official ThunderID logo mark (icon + wordmark), inverted-for-dark-backgrounds variant. + * The badge's chip is always the fixed dark navy in `ATTRIBUTION_BADGE_COLORS.background` + * regardless of the host theme, so the inverted (white text) logo variant is always correct here. + */ +export const ATTRIBUTION_LOGO_SVG_MARKUP = ` + + + + + + + + + + + + +`; + +/** + * The logo mark's SVG `viewBox` dimensions, used to derive its rendered width from a + * fixed height (see `buildAttributionCss`) without hardcoding a separately-maintained + * pixel value that could drift from the artwork above. + */ +export const ATTRIBUTION_LOGO_VIEWBOX: {width: number; height: number} = { + width: 1187, + height: 257, +}; + +/** + * Fixed layout constants for the badge — shared so every framework wrapper attaches + * the badge to its host card identically. + */ +export const ATTRIBUTION_LAYOUT = { + borderRadius: '0 10px 10px 0', + fontSize: '9px', + gap: '6px', + letterSpacing: '0.06em', + logoHeight: '11px', + padding: '10px 8px', + topOffset: '28px', +} as const; + +export const ATTRIBUTION_BLOCK: string = withVendorCSSClassPrefix(bem('attribution')); +export const ATTRIBUTION_CONTENT_CLASS: string = withVendorCSSClassPrefix(bem('attribution', 'content')); +export const ATTRIBUTION_LABEL_CLASS: string = withVendorCSSClassPrefix(bem('attribution', 'label')); +export const ATTRIBUTION_LOGO_CLASS: string = withVendorCSSClassPrefix(bem('attribution', 'logo')); + +/** + * Builds the shared attribution badge stylesheet as plain CSS text. + * + * The badge's own colors are fixed brand values; `borderColor` and `boxShadow` are the + * only values threaded through from the resolved `Theme` so the badge blends into + * whichever card it's attached to. + * + * @param borderColor - Resolved `theme.vars.colors.border` value of the host theme. + * @param boxShadow - Resolved `theme.vars.shadows.medium` value of the host theme. + * @returns A CSS stylesheet string framework wrappers can inject once and share. + */ +export const buildAttributionCss = (borderColor: string, boxShadow: string): string => ` +.${ATTRIBUTION_BLOCK} { + position: absolute; + top: ${ATTRIBUTION_LAYOUT.topOffset}; + right: -1px; + transform: translateX(100%); + display: flex; + align-items: center; + justify-content: center; + padding: ${ATTRIBUTION_LAYOUT.padding}; + background: ${ATTRIBUTION_BADGE_COLORS.background}; + border: 1px solid ${borderColor}; + border-left: none; + border-radius: ${ATTRIBUTION_LAYOUT.borderRadius}; + box-shadow: ${boxShadow}; + text-decoration: none; + z-index: 1; +} + +.${ATTRIBUTION_CONTENT_CLASS} { + display: flex; + align-items: center; + gap: ${ATTRIBUTION_LAYOUT.gap}; + writing-mode: vertical-rl; + white-space: nowrap; +} + +.${ATTRIBUTION_LABEL_CLASS} { + font-size: ${ATTRIBUTION_LAYOUT.fontSize}; + font-weight: 600; + letter-spacing: ${ATTRIBUTION_LAYOUT.letterSpacing}; + color: ${ATTRIBUTION_BADGE_COLORS.text}; + white-space: nowrap; +} + +.${ATTRIBUTION_LOGO_CLASS} { + display: flex; + align-items: center; + justify-content: center; + width: ${ATTRIBUTION_LAYOUT.logoHeight}; + height: calc(${ATTRIBUTION_LAYOUT.logoHeight} * ${ATTRIBUTION_LOGO_VIEWBOX.width} / ${ATTRIBUTION_LOGO_VIEWBOX.height}); + flex-shrink: 0; +} + +.${ATTRIBUTION_LOGO_CLASS} svg { + height: ${ATTRIBUTION_LAYOUT.logoHeight}; + width: auto; + transform: rotate(-90deg); + display: block; +} +`; diff --git a/packages/browser/src/branding/resolveShowAttribution.ts b/packages/browser/src/branding/resolveShowAttribution.ts new file mode 100644 index 0000000..a7e8e47 --- /dev/null +++ b/packages/browser/src/branding/resolveShowAttribution.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Resolves whether the "Powered by ThunderID" attribution badge should be shown, given the + * root-level (`ThunderIDProvider`'s `showAttribution` config field) and an optional + * component-level `showAttribution` prop override. + * + * Precedence: the component-level value wins when defined, otherwise the root-level value is + * used, otherwise the badge defaults to shown. + * + * Shared across framework SDKs so every wrapper (`@thunderid/react`, `@thunderid/vue`, ...) + * applies the exact same precedence instead of re-implementing it. + * + * @param rootShowAttribution - `showAttribution` supplied to the root provider (e.g. `ThunderIDProvider`). + * @param componentShowAttribution - Optional component-level `showAttribution` prop override. + * @returns Whether the attribution badge should render. + */ +const resolveShowAttribution = ( + rootShowAttribution?: boolean, + componentShowAttribution?: boolean, +): boolean => { + if (componentShowAttribution !== undefined) { + return componentShowAttribution; + } + + if (rootShowAttribution !== undefined) { + return rootShowAttribution; + } + + return true; +}; + +export default resolveShowAttribution; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 5739883..3fb2cc8 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -77,6 +77,21 @@ export {detectThemeMode, createClassObserver, createMediaQueryListener} from './ export type {BrowserThemeDetection} from './theme/themeDetection'; export {default as getActiveTheme} from './theme/getActiveTheme'; export {default as normalizeBorderRadius} from './theme/normalizeBorderRadius'; +export {getElevatedSurfaceTokens} from './theme/elevatedSurface'; +export type {ElevatedSurfaceTokens} from './theme/elevatedSurface'; + +// Branding — shared design system tokens/logic for framework SDKs (react, vue, ...) +export { + ATTRIBUTION_BADGE_COLORS, + ATTRIBUTION_BLOCK, + ATTRIBUTION_CONTENT_CLASS, + ATTRIBUTION_LABEL_CLASS, + ATTRIBUTION_LAYOUT, + ATTRIBUTION_LOGO_CLASS, + ATTRIBUTION_LOGO_SVG_MARKUP, + buildAttributionCss, +} from './branding/attributionDesign'; +export {default as resolveShowAttribution} from './branding/resolveShowAttribution'; // Re-export everything from the JavaScript SDK export * from '@thunderid/javascript'; diff --git a/packages/browser/src/theme/elevatedSurface.ts b/packages/browser/src/theme/elevatedSurface.ts new file mode 100644 index 0000000..51b3da1 --- /dev/null +++ b/packages/browser/src/theme/elevatedSurface.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {Theme} from '@thunderid/javascript'; + +export interface ElevatedSurfaceTokens { + /** + * Themed surface background. + */ + background: string; + /** + * Themed 1px border shorthand (`1px solid `). + */ + border: string; + /** + * Large corner radius, shared by every top-level card surface (auth cards, dropdowns, + * profile/organization panels). + */ + borderRadius: string; + /** + * Elevated drop shadow used to lift a card off the page background. + */ + boxShadow: string; + /** + * Comfortable card padding (vertical, horizontal). + */ + padding: string; +} + +/** + * Resolves the shared "elevated surface" look — the rounded, bordered, drop-shadowed card + * treatment used across every top-level ThunderID surface (`SignIn`, `SignUp`, `Recovery`, + * `UserDropdown`, `UserProfile`, and friends). + * + * Centralizing this recipe here means every framework SDK composes the exact same combination + * of theme tokens instead of each component picking its own subset (which is how, prior to + * this, some cards had a shadow and others didn't). Consumers spread the result into their own + * CSS-in-JS or stylesheet rules alongside layout-specific properties (`gap`, `min-width`, ...). + * + * @param theme - The resolved theme object. + * @returns The shared elevated-surface CSS token values. + */ +export const getElevatedSurfaceTokens = (theme: Theme): ElevatedSurfaceTokens => ({ + background: theme.vars.colors.background.surface, + border: `1px solid ${theme.vars.colors.border}`, + borderRadius: theme.vars.borderRadius.large, + boxShadow: theme.vars.shadows.large, + padding: `calc(${theme.vars.spacing.unit} * 4) calc(${theme.vars.spacing.unit} * 3.5)`, +}); + +export default getElevatedSurfaceTokens; diff --git a/packages/javascript/src/i18n/models/i18n.ts b/packages/javascript/src/i18n/models/i18n.ts index a0d0b4b..4798a2b 100644 --- a/packages/javascript/src/i18n/models/i18n.ts +++ b/packages/javascript/src/i18n/models/i18n.ts @@ -42,6 +42,7 @@ export interface I18nTranslations { 'elements.display.divider.or_separator': string; 'elements.display.copyable_text.copy': string; 'elements.display.copyable_text.copied': string; + 'elements.display.attribution.powered_by': string; /* Fields */ 'elements.fields.generic.placeholder': string; diff --git a/packages/javascript/src/i18n/translations/en-US.ts b/packages/javascript/src/i18n/translations/en-US.ts index 69428ff..2c939ac 100644 --- a/packages/javascript/src/i18n/translations/en-US.ts +++ b/packages/javascript/src/i18n/translations/en-US.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'OR', 'elements.display.copyable_text.copy': 'Copy', 'elements.display.copyable_text.copied': 'Copied!', + 'elements.display.attribution.powered_by': 'Powered by', /* Fields */ 'elements.fields.generic.placeholder': 'Enter your {field}', diff --git a/packages/javascript/src/i18n/translations/fr-FR.ts b/packages/javascript/src/i18n/translations/fr-FR.ts index 636bf89..caeaaa5 100644 --- a/packages/javascript/src/i18n/translations/fr-FR.ts +++ b/packages/javascript/src/i18n/translations/fr-FR.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'OU', 'elements.display.copyable_text.copy': 'Copie', 'elements.display.copyable_text.copied': 'Copié!', + 'elements.display.attribution.powered_by': 'Propulsé par', /* Fields */ 'elements.fields.generic.placeholder': 'Entrez votre {field}', diff --git a/packages/javascript/src/i18n/translations/hi-IN.ts b/packages/javascript/src/i18n/translations/hi-IN.ts index 1a57eaa..eb261cc 100644 --- a/packages/javascript/src/i18n/translations/hi-IN.ts +++ b/packages/javascript/src/i18n/translations/hi-IN.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'या', 'elements.display.copyable_text.copy': 'प्रतिलिपि', 'elements.display.copyable_text.copied': 'नकल की गई!', + 'elements.display.attribution.powered_by': 'द्वारा संचालित', /* Fields */ 'elements.fields.generic.placeholder': '{field} दर्ज करें', diff --git a/packages/javascript/src/i18n/translations/ja-JP.ts b/packages/javascript/src/i18n/translations/ja-JP.ts index 58dcef2..53df7a8 100644 --- a/packages/javascript/src/i18n/translations/ja-JP.ts +++ b/packages/javascript/src/i18n/translations/ja-JP.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'または', 'elements.display.copyable_text.copy': 'コピー', 'elements.display.copyable_text.copied': 'コピーしました!', + 'elements.display.attribution.powered_by': '提供', /* Fields */ 'elements.fields.generic.placeholder': '{field}を入力してください', diff --git a/packages/javascript/src/i18n/translations/pt-BR.ts b/packages/javascript/src/i18n/translations/pt-BR.ts index f7217a5..910e3c4 100644 --- a/packages/javascript/src/i18n/translations/pt-BR.ts +++ b/packages/javascript/src/i18n/translations/pt-BR.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'OU', 'elements.display.copyable_text.copy': 'Cópia', 'elements.display.copyable_text.copied': 'Copiado!', + 'elements.display.attribution.powered_by': 'Desenvolvido por', /* Fields */ 'elements.fields.generic.placeholder': 'Digite seu {field}', diff --git a/packages/javascript/src/i18n/translations/pt-PT.ts b/packages/javascript/src/i18n/translations/pt-PT.ts index 7dff401..32f9162 100644 --- a/packages/javascript/src/i18n/translations/pt-PT.ts +++ b/packages/javascript/src/i18n/translations/pt-PT.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'OU', 'elements.display.copyable_text.copy': 'Cópia', 'elements.display.copyable_text.copied': 'Copiado!', + 'elements.display.attribution.powered_by': 'Desenvolvido por', /* Fields */ 'elements.fields.generic.placeholder': 'Introduza o seu {field}', diff --git a/packages/javascript/src/i18n/translations/si-LK.ts b/packages/javascript/src/i18n/translations/si-LK.ts index 9957a55..f23925d 100644 --- a/packages/javascript/src/i18n/translations/si-LK.ts +++ b/packages/javascript/src/i18n/translations/si-LK.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'හෝ', 'elements.display.copyable_text.copy': 'පිටපත් කරන්න', 'elements.display.copyable_text.copied': 'පිටපත් කළා!', + 'elements.display.attribution.powered_by': 'මගින් බලගැන්වේ', /* Fields */ 'elements.fields.generic.placeholder': 'ඔබේ {field} ඇතුලත් කරන්න', diff --git a/packages/javascript/src/i18n/translations/ta-IN.ts b/packages/javascript/src/i18n/translations/ta-IN.ts index ccc552d..6c113b9 100644 --- a/packages/javascript/src/i18n/translations/ta-IN.ts +++ b/packages/javascript/src/i18n/translations/ta-IN.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'அல்லது', 'elements.display.copyable_text.copy': 'நகலெடுக்கவும்', 'elements.display.copyable_text.copied': 'நகலெடுக்கப்பட்டது!', + 'elements.display.attribution.powered_by': 'மூலம் இயக்கப்படுகிறது', /* Fields */ 'elements.fields.generic.placeholder': '{field} உள்ளிடவும்', diff --git a/packages/javascript/src/i18n/translations/te-IN.ts b/packages/javascript/src/i18n/translations/te-IN.ts index aba4eff..7ca80ab 100644 --- a/packages/javascript/src/i18n/translations/te-IN.ts +++ b/packages/javascript/src/i18n/translations/te-IN.ts @@ -42,6 +42,7 @@ const translations: I18nTranslations = { 'elements.display.divider.or_separator': 'లేదా', 'elements.display.copyable_text.copy': 'కాపీ చేయండి', 'elements.display.copyable_text.copied': 'కాపీ చేయబడింది!', + 'elements.display.attribution.powered_by': 'ద్వారా అందించబడింది', /* Fields */ 'elements.fields.generic.placeholder': 'మీ {field} ను నమోదు చేయండి', diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 7a9af6e..68e543b 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -163,6 +163,14 @@ export interface BaseConfig extends WithPreferences, WithExtensions */ applicationId?: string | undefined; + /** + * Whether to show the "Powered by ThunderID" attribution badge on pre-login authentication + * surfaces (sign in, sign up, and account recovery). Individual components accept their own + * `showAttribution` prop to override this on a per-instance basis. + * @default true + */ + showAttribution?: boolean; + /** * Vendor/brand namespace used to prefix storage keys, cookie names, and CSS class names * generated by the SDK. Override this when white-labeling the SDK under a different brand. diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts index 2bc55c9..4517d0c 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts @@ -17,7 +17,7 @@ */ import {css} from '@emotion/css'; -import {Theme} from '@thunderid/browser'; +import {ElevatedSurfaceTokens, getElevatedSurfaceTokens, Theme} from '@thunderid/browser'; import {useMemo} from 'react'; /** @@ -28,6 +28,8 @@ import {useMemo} from 'react'; */ const useStyles = (theme: Theme, colorScheme: string): Record => useMemo(() => { + const surface: ElevatedSurfaceTokens = getElevatedSurfaceTokens(theme); + const trigger: string = css` display: inline-flex; align-items: center; @@ -70,12 +72,10 @@ const useStyles = (theme: Theme, colorScheme: string): Record => `; const dropdownContent: string = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - border: 1px solid ${theme.vars.colors.border}; + background: ${surface.background}; + border-radius: ${surface.borderRadius}; + box-shadow: ${surface.boxShadow}; + border: ${surface.border}; font-family: ${theme.vars.typography.fontFamily}; min-width: 250px; max-width: 600px; @@ -222,6 +222,7 @@ const useStyles = (theme: Theme, colorScheme: string): Record => theme.vars.colors.action?.hover, theme.vars.borderRadius.medium, theme.vars.borderRadius.large, + theme.vars.shadows.large, theme.vars.spacing.unit, theme.vars.typography.fontFamily, colorScheme, diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts index fc9293c..4a4ab8d 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts @@ -17,7 +17,7 @@ */ import {css} from '@emotion/css'; -import {Theme, withVendorCSSClassPrefix} from '@thunderid/browser'; +import {ElevatedSurfaceTokens, getElevatedSurfaceTokens, Theme, withVendorCSSClassPrefix} from '@thunderid/browser'; import {useMemo} from 'react'; /** @@ -78,6 +78,8 @@ const useStyles = (theme: Theme, colorScheme: string): Record => `; return useMemo(() => { + const surface: ElevatedSurfaceTokens = getElevatedSurfaceTokens(theme); + const root: string = css` padding: calc(${theme.vars.spacing.unit} * 4); min-width: 600px; @@ -86,8 +88,10 @@ const useStyles = (theme: Theme, colorScheme: string): Record => `; const card: string = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; + background: ${surface.background}; + border-radius: ${surface.borderRadius}; + box-shadow: ${surface.boxShadow}; + border: ${surface.border}; `; const header: string = css` @@ -243,6 +247,7 @@ const useStyles = (theme: Theme, colorScheme: string): Record => theme.vars.colors.border, theme.vars.borderRadius.large, theme.vars.borderRadius.medium, + theme.vars.shadows.large, theme.vars.spacing.unit, theme.vars.typography.fontFamily, colorScheme, diff --git a/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx b/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx index 872ad12..4b8f134 100644 --- a/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx +++ b/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx @@ -37,12 +37,14 @@ import useFlow from '../../../../contexts/Flow/useFlow'; import ComponentPreferencesContext from '../../../../contexts/I18n/ComponentPreferencesContext'; import useTheme from '../../../../contexts/Theme/useTheme'; import useThunderID from '../../../../contexts/ThunderID/useThunderID'; +import useAttribution from '../../../../hooks/useAttribution'; import {useForm, FormField} from '../../../../hooks/useForm'; import useTranslation from '../../../../hooks/useTranslation'; import composeAffixedInputs from '../../../../utils/composeAffixedInputs'; import {normalizeFlowResponse, extractErrorMessage} from '../../../../utils/flowTransformer'; import getAuthComponentHeadings from '../../../../utils/getAuthComponentHeadings'; import AlertPrimitive from '../../../primitives/Alert/Alert'; +import Attribution from '../../../primitives/Attribution/Attribution'; import CardPrimitive, {CardProps} from '../../../primitives/Card/Card'; import Logo from '../../../primitives/Logo/Logo'; import Spinner from '../../../primitives/Spinner/Spinner'; @@ -105,6 +107,12 @@ export interface BaseRecoveryProps { showLogo?: boolean; showSubtitle?: boolean; showTitle?: boolean; + /** + * Whether to show the "Powered by ThunderID" attribution badge. + * Overrides the root `ThunderIDProvider`'s `showAttribution` config option for this instance. + * @default true + */ + showAttribution?: boolean; size?: 'small' | 'medium' | 'large'; variant?: CardProps['variant']; } @@ -629,6 +637,22 @@ const BaseRecoveryContent: FC = ({ * BaseRecovery component for ThunderIDV2 that provides an embedded account/password recovery flow. * Accepts API functions as props to maintain framework independence. */ +const BaseRecoveryWithAttribution: FC = ({ + showAttribution, + ...rest +}: BaseRecoveryProps): ReactElement => { + const resolvedShowAttribution: boolean = useAttribution(showAttribution); + + return ( +
+ + + + {resolvedShowAttribution && } +
+ ); +}; + const BaseRecovery: FC = ({ preferences, showLogo = true, @@ -644,9 +668,7 @@ const BaseRecovery: FC = ({ )} - - - + ); diff --git a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.styles.ts b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.styles.ts index 391ce33..1b7dab9 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.styles.ts +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.styles.ts @@ -17,7 +17,7 @@ */ import {css} from '@emotion/css'; -import {Theme} from '@thunderid/browser'; +import {ElevatedSurfaceTokens, getElevatedSurfaceTokens, Theme} from '@thunderid/browser'; import {useMemo} from 'react'; /** @@ -28,6 +28,8 @@ import {useMemo} from 'react'; */ const useStyles = (theme: Theme, colorScheme: string): Record => useMemo(() => { + const surface: ElevatedSurfaceTokens = getElevatedSurfaceTokens(theme); + const signIn: string = css` min-width: 420px; margin: 0 auto; @@ -35,10 +37,12 @@ const useStyles = (theme: Theme, colorScheme: string): Record => `; const card: string = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; + background: ${surface.background}; + border-radius: ${surface.borderRadius}; + box-shadow: ${surface.boxShadow}; gap: calc(${theme.vars.spacing.unit} * 2); min-width: 420px; + padding: ${surface.padding}; `; const logoContainer: string = css` @@ -185,6 +189,7 @@ const useStyles = (theme: Theme, colorScheme: string): Record => theme.vars.colors.text.primary, theme.vars.colors.text.secondary, theme.vars.borderRadius.large, + theme.vars.shadows.large, theme.vars.spacing.unit, theme.vars.typography.fontFamily, colorScheme, diff --git a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx index 5e14a01..0c172cb 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx @@ -36,11 +36,13 @@ import useFlow from '../../../../contexts/Flow/useFlow'; import ComponentPreferencesContext from '../../../../contexts/I18n/ComponentPreferencesContext'; import useTheme from '../../../../contexts/Theme/useTheme'; import useThunderID from '../../../../contexts/ThunderID/useThunderID'; +import useAttribution from '../../../../hooks/useAttribution'; import {FormField, useForm} from '../../../../hooks/useForm'; import useTranslation from '../../../../hooks/useTranslation'; import composeAffixedInputs from '../../../../utils/composeAffixedInputs'; import {extractErrorMessage} from '../../../../utils/flowTransformer'; import AlertPrimitive from '../../../primitives/Alert/Alert'; +import Attribution from '../../../primitives/Attribution/Attribution'; // eslint-disable-next-line import/no-named-as-default import CardPrimitive, {CardProps} from '../../../primitives/Card/Card'; import Spinner from '../../../primitives/Spinner/Spinner'; @@ -239,6 +241,13 @@ export interface BaseSignInProps { */ size?: 'small' | 'medium' | 'large'; + /** + * Whether to show the "Powered by ThunderID" attribution badge. + * Overrides the root `ThunderIDProvider`'s `showAttribution` config option for this instance. + * @default true + */ + showAttribution?: boolean; + /** * Theme variant for the component. */ @@ -719,12 +728,24 @@ const BaseSignInContent: FC = ({ * * ``` */ -const BaseSignIn: FC = ({preferences, ...rest}: BaseSignInProps): ReactElement => { - const content: ReactElement = ( - - - +const BaseSignInWithAttribution: FC = ({ + showAttribution, + ...rest +}: BaseSignInProps): ReactElement => { + const resolvedShowAttribution: boolean = useAttribution(showAttribution); + + return ( +
+ + + + {resolvedShowAttribution && } +
); +}; + +const BaseSignIn: FC = ({preferences, ...rest}: BaseSignInProps): ReactElement => { + const content: ReactElement = ; if (!preferences) return content; diff --git a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.styles.ts b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.styles.ts index 87bbbd4..532faba 100644 --- a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.styles.ts +++ b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.styles.ts @@ -17,7 +17,7 @@ */ import {css} from '@emotion/css'; -import {Theme} from '@thunderid/browser'; +import {ElevatedSurfaceTokens, getElevatedSurfaceTokens, Theme} from '@thunderid/browser'; import {useMemo} from 'react'; /** @@ -28,6 +28,8 @@ import {useMemo} from 'react'; */ const useStyles = (theme: Theme, colorScheme: string): Record => useMemo(() => { + const surface: ElevatedSurfaceTokens = getElevatedSurfaceTokens(theme); + const signUp: string = css` min-width: 420px; margin: 0 auto; @@ -35,10 +37,12 @@ const useStyles = (theme: Theme, colorScheme: string): Record => `; const card: string = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; + background: ${surface.background}; + border-radius: ${surface.borderRadius}; + box-shadow: ${surface.boxShadow}; gap: calc(${theme.vars.spacing.unit} * 2); min-width: 420px; + padding: ${surface.padding}; `; const logoContainer: string = css` @@ -180,6 +184,7 @@ const useStyles = (theme: Theme, colorScheme: string): Record => theme.vars.colors.text.primary, theme.vars.colors.text.secondary, theme.vars.borderRadius.large, + theme.vars.shadows.large, theme.vars.spacing.unit, theme.vars.typography.fontFamily, colorScheme, diff --git a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx index 1e66b1a..87ad48e 100644 --- a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx @@ -39,6 +39,7 @@ import useFlow from '../../../../contexts/Flow/useFlow'; import ComponentPreferencesContext from '../../../../contexts/I18n/ComponentPreferencesContext'; import useTheme from '../../../../contexts/Theme/useTheme'; import useThunderID from '../../../../contexts/ThunderID/useThunderID'; +import useAttribution from '../../../../hooks/useAttribution'; import {useForm, FormField} from '../../../../hooks/useForm'; import useTranslation from '../../../../hooks/useTranslation'; import composeAffixedInputs from '../../../../utils/composeAffixedInputs'; @@ -46,6 +47,7 @@ import {normalizeFlowResponse, extractErrorMessage} from '../../../../utils/flow import getAuthComponentHeadings from '../../../../utils/getAuthComponentHeadings'; import {handlePasskeyRegistration} from '../../../../utils/passkey'; import AlertPrimitive from '../../../primitives/Alert/Alert'; +import Attribution from '../../../primitives/Attribution/Attribution'; // eslint-disable-next-line import/no-named-as-default import CardPrimitive, {CardProps} from '../../../primitives/Card/Card'; import Logo from '../../../primitives/Logo/Logo'; @@ -256,6 +258,13 @@ export interface BaseSignUpProps { */ showTitle?: boolean; + /** + * Whether to show the "Powered by ThunderID" attribution badge. + * Overrides the root `ThunderIDProvider`'s `showAttribution` config option for this instance. + * @default true + */ + showAttribution?: boolean; + /** * Size variant for the component. */ @@ -1224,6 +1233,22 @@ const BaseSignUpContent: FC = ({ * This component handles both the presentation layer and sign-up flow logic. * It accepts API functions as props to maintain framework independence. */ +const BaseSignUpWithAttribution: FC = ({ + showAttribution, + ...rest +}: BaseSignUpProps): ReactElement => { + const resolvedShowAttribution: boolean = useAttribution(showAttribution); + + return ( +
+ + + + {resolvedShowAttribution && } +
+ ); +}; + const BaseSignUp: FC = ({preferences, showLogo = true, ...rest}: BaseSignUpProps): ReactElement => { const {theme, colorScheme} = useTheme(); const styles: any = useStyles(theme, colorScheme); @@ -1235,9 +1260,7 @@ const BaseSignUp: FC = ({preferences, showLogo = true, ...rest} )} - - - + ); diff --git a/packages/react/src/components/primitives/Attribution/Attribution.tsx b/packages/react/src/components/primitives/Attribution/Attribution.tsx new file mode 100644 index 0000000..6cc6e74 --- /dev/null +++ b/packages/react/src/components/primitives/Attribution/Attribution.tsx @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {cx} from '@emotion/css'; +import { + ATTRIBUTION_BLOCK, + ATTRIBUTION_CONTENT_CLASS, + ATTRIBUTION_LABEL_CLASS, + ATTRIBUTION_LOGO_CLASS, + ATTRIBUTION_LOGO_SVG_MARKUP, + buildAttributionCss, +} from '@thunderid/browser'; +import {FC, ReactElement, useMemo} from 'react'; +import useTheme from '../../../contexts/Theme/useTheme'; +import useTranslation from '../../../hooks/useTranslation'; + +export interface AttributionProps { + /** + * Custom CSS class name for the attribution badge container. + */ + className?: string; +} + +/** + * The "Powered by ThunderID" attribution badge. + * + * Renders as a vertical tab attached to the trailing edge of the nearest positioned + * ancestor (the host must set `position: relative` and allow horizontal overflow, since + * the badge is drawn outside its own box). This is a fixed brand mark shared across every + * ThunderID framework SDK via `@thunderid/browser` — see `buildAttributionCss` — so it + * always renders in ThunderID's own colors, and the official ThunderID logo mark, rather + * than the host theme or plain text. + * + * Shown by default on pre-login surfaces (sign in, sign up, account recovery). Consumers + * can hide it globally via `ThunderIDProvider`'s `showAttribution` config option, or + * per-component via that same component's `showAttribution` prop. + */ +const Attribution: FC = ({className = ''}: AttributionProps): ReactElement => { + const {theme} = useTheme(); + const {t} = useTranslation(); + + const css: string = useMemo( + () => buildAttributionCss(theme.vars.colors.border, theme.vars.shadows.medium), + [theme.vars.colors.border, theme.vars.shadows.medium], + ); + + return ( + <> + +
+
+ {t('elements.display.attribution.powered_by')} + +
+
+ + ); +}; + +export default Attribution; diff --git a/packages/react/src/components/primitives/Attribution/index.ts b/packages/react/src/components/primitives/Attribution/index.ts new file mode 100644 index 0000000..86c1084 --- /dev/null +++ b/packages/react/src/components/primitives/Attribution/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export {default} from './Attribution'; +export type {AttributionProps} from './Attribution'; diff --git a/packages/react/src/contexts/ThunderID/ThunderIDContext.ts b/packages/react/src/contexts/ThunderID/ThunderIDContext.ts index e3ec459..2b59912 100644 --- a/packages/react/src/contexts/ThunderID/ThunderIDContext.ts +++ b/packages/react/src/contexts/ThunderID/ThunderIDContext.ts @@ -209,6 +209,12 @@ export type ThunderIDContextProps = { signUpUrl: string | undefined; + /** + * Whether to show the "Powered by ThunderID" attribution badge on pre-login authentication + * surfaces. Resolved from the `showAttribution` config option, defaulting to `true`. + */ + showAttribution?: boolean; + user: any; /** @@ -257,6 +263,7 @@ const ThunderIDContext: Context = createContext Promise.resolve({} as any), signUp: () => Promise.resolve({} as any), signUpUrl: undefined, + showAttribution: undefined, user: null, vendor: VendorConstants.VENDOR_PREFIX, }); diff --git a/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx b/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx index ea7260f..5540813 100644 --- a/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx +++ b/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx @@ -70,6 +70,7 @@ const ThunderIDProvider: FC> = ({ syncSession, instanceId = 0, organizationChain, + showAttribution, ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); @@ -480,12 +481,14 @@ const ThunderIDProvider: FC> = ({ signOut, signUp, signUpUrl, + showAttribution, syncSession, user, vendor: getVendorPrefix(config.vendor), }), [ applicationId, + showAttribution, config?.organizationHandle, config.vendor, config.afterSignInUrl, diff --git a/packages/react/src/hooks/useAttribution.ts b/packages/react/src/hooks/useAttribution.ts new file mode 100644 index 0000000..27a8764 --- /dev/null +++ b/packages/react/src/hooks/useAttribution.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {resolveShowAttribution} from '@thunderid/browser'; +import useThunderID from '../contexts/ThunderID/useThunderID'; + +/** + * Resolves whether the "Powered by ThunderID" attribution badge should render for the + * calling component, honoring (in precedence order) the component's own `showAttribution` + * prop, then the root `ThunderIDProvider`'s `showAttribution` config option, defaulting to + * shown when neither is set. + * + * Must be used within a `ThunderIDProvider`. + * + * @param componentShowAttribution - The calling component's own `showAttribution` prop, if any. + */ +const useAttribution = (componentShowAttribution?: boolean): boolean => { + const {showAttribution: rootShowAttribution} = useThunderID(); + + return resolveShowAttribution(rootShowAttribution, componentShowAttribution); +}; + +export default useAttribution; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 796daaa..05530c8 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -65,6 +65,8 @@ export * from './hooks/useBrowserUrl'; export {default as useTranslation} from './hooks/useTranslation'; export * from './hooks/useTranslation'; +export {default as useAttribution} from './hooks/useAttribution'; + export {normalizeFlowResponse, extractErrorMessage} from './utils/flowTransformer'; export {default as useForm} from './hooks/useForm'; @@ -225,6 +227,9 @@ export * from './components/primitives/Divider/Divider'; export {default as Logo} from './components/primitives/Logo/Logo'; export * from './components/primitives/Logo/Logo'; +export {default as Attribution} from './components/primitives/Attribution/Attribution'; +export * from './components/primitives/Attribution/Attribution'; + export {default as Spinner} from './components/primitives/Spinner/Spinner'; export * from './components/primitives/Spinner/Spinner';