Skip to content

Commit

Permalink
feat(elements): Add SafeIdentifier & Salutation [SDK-1501] (#3004)
Browse files Browse the repository at this point in the history
* feat(elements): Add SafeIdentifier & Salutation [SDK-1501]

* chore(elements): Add requested changes

* Update verification code copy

Co-authored-by: Bryce Kalow <[email protected]>

---------

Co-authored-by: Bryce Kalow <[email protected]>
  • Loading branch information
tmilewski and brkalow authored Mar 15, 2024
1 parent 7dcb5e2 commit 45aeae4
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .changeset/kind-hornets-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -153,6 +163,10 @@ export default function SignInPage() {
<GlobalError className='block text-red-400 font-mono' />

<Verification name='password'>
<P className='text-sm'>
Welcome back <Salutation />!
</P>

<CustomField
label='Password'
name='password'
Expand All @@ -162,6 +176,10 @@ export default function SignInPage() {
</Verification>

<Verification name='email_code'>
<P className='text-sm'>
Welcome back! We&apos;ve sent a temporary code to <SafeIdentifier />
</P>

<CustomField
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
Expand All @@ -174,6 +192,10 @@ export default function SignInPage() {
</Verification>

<Verification name='phone_code'>
<P className='text-sm'>
Welcome back! We&apos;ve sent a temporary code to <SafeIdentifier />
</P>

<CustomField
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
Expand All @@ -188,7 +210,9 @@ export default function SignInPage() {
<Verification name='reset_password_email_code'>
<H3>Verify your email</H3>

<P>Please check your email for a verification code...</P>
<P className='text-sm'>
We've sent a verification code to <SafeIdentifier />.
</P>
</Verification>
</div>

Expand Down
2 changes: 1 addition & 1 deletion packages/elements/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clerk/elements",
"version": "0.1.22",
"version": "0.1.23",
"description": "Clerk Elements",
"keywords": [
"clerk",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
>;
Original file line number Diff line number Diff line change
@@ -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: '[email protected]' })).toBe(
'[email protected]',
);
});

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: '[email protected]' })).toBe('John');
});
});
26 changes: 26 additions & 0 deletions packages/elements/src/internals/machines/utils/formatters.ts
Original file line number Diff line number Diff line change
@@ -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 || '';
}
14 changes: 14 additions & 0 deletions packages/elements/src/react/sign-in/identifiers.tsx
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 2 additions & 0 deletions packages/elements/src/react/sign-in/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down

0 comments on commit 45aeae4

Please sign in to comment.