diff --git a/.changeset/kind-hornets-develop.md b/.changeset/kind-hornets-develop.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/kind-hornets-develop.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx index 5c97c12915e..bde49027cdf 100644 --- a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx +++ b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx @@ -1,7 +1,17 @@ 'use client'; import { Field, FieldError, GlobalError, Input, Label } from '@clerk/elements/common'; -import { Navigate, Provider, ProviderIcon, SignIn, Step, StrategyOption, Verification } from '@clerk/elements/sign-in'; +import { + Navigate, + Provider, + ProviderIcon, + SafeIdentifier, + Salutation, + SignIn, + Step, + StrategyOption, + Verification, +} from '@clerk/elements/sign-in'; import Link from 'next/link'; import { type ComponentProps, useState } from 'react'; @@ -153,6 +163,10 @@ export default function SignInPage() { +

+ Welcome back ! +

+ +

+ Welcome back! We've sent a temporary code to +

+ +

+ Welcome back! We've sent a temporary code to +

+

Verify your email

-

Please check your email for a verification code...

+

+ We've sent a verification code to . +

diff --git a/packages/elements/package.json b/packages/elements/package.json index 9445c801376..c5f51638a90 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -1,6 +1,6 @@ { "name": "@clerk/elements", - "version": "0.1.22", + "version": "0.1.23", "description": "Clerk Elements", "keywords": [ "clerk", diff --git a/packages/elements/src/internals/machines/sign-in/selectors/sign-in.selectors.ts b/packages/elements/src/internals/machines/sign-in/selectors/sign-in.selectors.ts new file mode 100644 index 00000000000..b5e799a9444 --- /dev/null +++ b/packages/elements/src/internals/machines/sign-in/selectors/sign-in.selectors.ts @@ -0,0 +1,16 @@ +import type { SignInRouterSnapshot } from '~/internals/machines/sign-in/types'; +import { formatSalutation } from '~/internals/machines/utils/formatters'; + +export function SignInSafeIdentifierSelector(s: SignInRouterSnapshot): string { + return s.context.clerk?.client.signIn.identifier || ''; +} + +export function SignInSalutationSelector(s: SignInRouterSnapshot): string { + const signIn = s.context.clerk?.client.signIn; + + return formatSalutation({ + firstName: signIn?.userData?.firstName, + identifier: signIn?.identifier, + lastName: signIn?.userData?.lastName, + }); +} diff --git a/packages/elements/src/internals/machines/sign-in/types/router.types.ts b/packages/elements/src/internals/machines/sign-in/types/router.types.ts index e8d0ef3431f..b4efae5b10e 100644 --- a/packages/elements/src/internals/machines/sign-in/types/router.types.ts +++ b/packages/elements/src/internals/machines/sign-in/types/router.types.ts @@ -1,5 +1,5 @@ import type { SignInResource } from '@clerk/types'; -import type { AnyActorLogic } from 'xstate'; +import type { AnyActorLogic, MachineSnapshot } from 'xstate'; import type { SignInVerificationFactorUpdateEvent } from '~/internals/machines/sign-in/types'; import type { @@ -93,3 +93,18 @@ export interface SignInRouterSchema { events: SignInRouterEvents; tags: SignInRouterTags; } + +// ---------------------------------- Schema ---------------------------------- // + +export type SignInChildren = any; // TODO: Update +export type SignInOuptut = any; // TODO: Update +export type SignInStateValue = any; // TODO: Update + +export type SignInRouterSnapshot = MachineSnapshot< + SignInRouterContext, + SignInRouterEvents, + SignInChildren, + SignInStateValue, + SignInRouterTags, + SignInOuptut +>; diff --git a/packages/elements/src/internals/machines/utils/__tests__/formatters.test.ts b/packages/elements/src/internals/machines/utils/__tests__/formatters.test.ts new file mode 100644 index 00000000000..b1ee63bc791 --- /dev/null +++ b/packages/elements/src/internals/machines/utils/__tests__/formatters.test.ts @@ -0,0 +1,47 @@ +import { formatName, formatSalutation } from '../formatters'; + +describe('formatName', () => { + test('returns undefined when no arguments are provided', () => { + expect(formatName()).toBeUndefined(); + }); + + test('returns the titleized version of the single argument', () => { + expect(formatName('john')).toBe('John'); + }); + + test('returns the titleized version of multiple arguments joined by space', () => { + expect(formatName('john', 'doe')).toBe('John Doe'); + }); + + test('ignores undefined arguments and returns the titleized version of the rest', () => { + expect(formatName(undefined, 'john', undefined, 'doe')).toBe('John Doe'); + }); +}); + +describe('formatSalutation', () => { + test('returns the formatted salutation based on firstName', () => { + expect(formatSalutation({ firstName: 'John', lastName: undefined, identifier: undefined })).toBe('John'); + }); + + test('returns the formatted salutation based on lastName', () => { + expect(formatSalutation({ firstName: undefined, lastName: 'Doe', identifier: undefined })).toBe('Doe'); + }); + + test('returns the formatted salutation based on identifier', () => { + expect(formatSalutation({ firstName: undefined, lastName: undefined, identifier: 'test@clerk.dev' })).toBe( + 'test@clerk.dev', + ); + }); + + test('returns an empty string when no arguments are provided', () => { + expect(formatSalutation({ firstName: undefined, lastName: undefined, identifier: undefined })).toBe(''); + }); + + test('returns the formatted salutation based on firstName and lastName', () => { + expect(formatSalutation({ firstName: 'John', lastName: 'Doe', identifier: undefined })).toBe('John'); + }); + + test('returns the formatted salutation based on firstName, lastName, and identifier', () => { + expect(formatSalutation({ firstName: 'John', lastName: 'Doe', identifier: 'test@clerk.dev' })).toBe('John'); + }); +}); diff --git a/packages/elements/src/internals/machines/utils/formatters.ts b/packages/elements/src/internals/machines/utils/formatters.ts new file mode 100644 index 00000000000..13b41c8b957 --- /dev/null +++ b/packages/elements/src/internals/machines/utils/formatters.ts @@ -0,0 +1,26 @@ +import { titleize } from '@clerk/shared'; + +// TODO: ideally the derivation of these values lives in FAPI and comes back directly from the API + +export function formatName(...args: (string | undefined)[]): string | undefined { + switch (args.length) { + case 0: + return undefined; + case 1: + return titleize(args[0]); + default: + return args.filter(Boolean).map(titleize).join(' '); + } +} + +export function formatSalutation({ + firstName, + lastName, + identifier, +}: { + firstName: string | undefined; + lastName: string | undefined; + identifier: string | undefined | null; +}): string { + return (firstName && formatName(firstName)) || (lastName && formatName(lastName)) || identifier || ''; +} diff --git a/packages/elements/src/react/sign-in/identifiers.tsx b/packages/elements/src/react/sign-in/identifiers.tsx new file mode 100644 index 00000000000..64d4509de8f --- /dev/null +++ b/packages/elements/src/react/sign-in/identifiers.tsx @@ -0,0 +1,14 @@ +import { + SignInSafeIdentifierSelector, + SignInSalutationSelector, +} from '~/internals/machines/sign-in/selectors/sign-in.selectors'; + +import { SignInRouterCtx } from './context'; + +export function SignInSafeIdentifier(): string { + return SignInRouterCtx.useSelector(SignInSafeIdentifierSelector); +} + +export function SignInSalutation(): string { + return SignInRouterCtx.useSelector(SignInSalutationSelector); +} diff --git a/packages/elements/src/react/sign-in/index.ts b/packages/elements/src/react/sign-in/index.ts index b5568e92aa5..3b0d62212fe 100644 --- a/packages/elements/src/react/sign-in/index.ts +++ b/packages/elements/src/react/sign-in/index.ts @@ -14,6 +14,8 @@ export { SignInVerification as Verification, } from './verifications'; +export { SignInSafeIdentifier as SafeIdentifier, SignInSalutation as Salutation } from './identifiers'; + export { useIsLoading_unstable } from './hooks/use-loading.hook'; /** @internal Internal use only */