Skip to content

feat(elements,ui): Determine SafeIdentifier based on strategy #3749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-cherries-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/elements": patch
---

Add support for `transform` prop on `SignIn.SafeIdentifier` and determine identifier based on strategy
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export { SignInRouterMachine, SignInRouterMachineId } from './router.machine';
export { SignInStartMachine, SignInStartMachineId } from './start.machine';
export { SignInResetPasswordMachine, SignInResetPasswordMachineId } from './reset-password.machine';

export { SignInSafeIdentifierSelector, SignInSalutationSelector } from './router.selectors';
export { SignInSafeIdentifierSelectorForStrategy, SignInSalutationSelector } from './router.selectors';

export type { TSignInRouterMachine } from './router.machine';
export type { TSignInStartMachine } from './start.machine';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import type { SignInStrategyName } from '~/internals/machines/shared';
import { formatSalutation } from '~/internals/machines/utils/formatters';

import type { SignInRouterSnapshot } from './router.types';

export function SignInSafeIdentifierSelector(s: SignInRouterSnapshot): string {
return s.context.clerk?.client.signIn.identifier || '';
export function SignInSafeIdentifierSelectorForStrategy(
strategy: SignInStrategyName | undefined,
): (s: SignInRouterSnapshot) => string {
return (s: SignInRouterSnapshot) => {
const signIn = s.context.clerk?.client.signIn;
const identifier = signIn.identifier || '';

if (strategy) {
const matchingFactor = [...(signIn.supportedFirstFactors ?? []), ...(signIn.supportedSecondFactors ?? [])].find(
f => f.strategy === strategy,
);
if (matchingFactor && 'safeIdentifier' in matchingFactor) {
return matchingFactor.safeIdentifier;
}
}

return identifier;
};
}

export function SignInSalutationSelector(s: SignInRouterSnapshot): string {
Expand Down
20 changes: 11 additions & 9 deletions packages/elements/src/react/sign-in/choose-strategy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SignInRouterSystemId } from '~/internals/machines/sign-in';
import { useActiveTags } from '../hooks';
import { ActiveTagsMode } from '../hooks/use-active-tags.hook';
import { createContextForDomValidation } from '../utils/create-context-for-dom-validation';
import { SignInRouterCtx } from './context';
import { SignInRouterCtx, SignInStrategyContext } from './context';

// --------------------------------- HELPERS ---------------------------------

Expand Down Expand Up @@ -134,14 +134,16 @@ export const SignInSupportedStrategy = React.forwardRef<SignInSupportedStrategyE
const defaultProps = asChild ? {} : { type: 'button' as const };

return factor ? (
<Comp
{...defaultProps}
{...rest}
ref={forwardedRef}
onClick={sendUpdateStrategyEvent}
>
{children || factor.strategy}
</Comp>
<SignInStrategyContext.Provider value={{ strategy: name }}>
<Comp
{...defaultProps}
{...rest}
ref={forwardedRef}
onClick={sendUpdateStrategyEvent}
>
{children || factor.strategy}
</Comp>
</SignInStrategyContext.Provider>
) : null;
},
);
Expand Down
1 change: 1 addition & 0 deletions packages/elements/src/react/sign-in/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
useSignInSecondFactorStep,
useSignInResetPasswordStep,
} from './router.context';
export { SignInStrategyContext, useSignInStrategy } from './sign-in-strategy.context';
export { StrategiesContext, useStrategy } from './strategies.context';

export type { StrategiesContextValue } from './strategies.context';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createContext, useContext } from 'react';

import { ClerkElementsRuntimeError } from '~/internals/errors';
import type { SignInStrategyName } from '~/internals/machines/shared';

export type SignInStrategyContextValue = {
strategy: SignInStrategyName | undefined;
};

export const SignInStrategyContext = createContext<SignInStrategyContextValue>({
strategy: undefined,
});

export function useSignInStrategy() {
const ctx = useContext(SignInStrategyContext);

if (!ctx) {
throw new ClerkElementsRuntimeError(
'useSignInStrategy must be used within a <SignIn.Strategy> or <SignIn.SupportedStrategy> component.',
);
}

const { strategy } = ctx;

return strategy;
}
18 changes: 14 additions & 4 deletions packages/elements/src/react/sign-in/identifiers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SignInSafeIdentifierSelector, SignInSalutationSelector } from '~/internals/machines/sign-in';
import { useMemo } from 'react';

import { SignInRouterCtx } from './context';
import { SignInSafeIdentifierSelectorForStrategy, SignInSalutationSelector } from '~/internals/machines/sign-in';

import { SignInRouterCtx, useSignInStrategy } from './context';

/**
* Render an identifier that has been provided by the user during a sign-in attempt. Renders a `string` (or empty string if it can't find an identifier).
Expand All @@ -11,8 +13,16 @@ import { SignInRouterCtx } from './context';
* <p>We've sent a code to <SignIn.SafeIdentifier />.</p>
* </SignIn.Strategy>
*/
export function SignInSafeIdentifier(): string {
return SignInRouterCtx.useSelector(SignInSafeIdentifierSelector);
export function SignInSafeIdentifier({ transform }: { transform?: (s: string) => string }): string {
const strategy = useSignInStrategy();
const selector = useMemo(() => SignInSafeIdentifierSelectorForStrategy(strategy), [strategy]);
const safeIdentifier = SignInRouterCtx.useSelector(selector);

if (transform) {
return transform(safeIdentifier);
}

return safeIdentifier;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/elements/src/react/sign-in/verifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Form } from '~/react/common/form';
import { useActiveTags } from '~/react/hooks';
import {
SignInRouterCtx,
SignInStrategyContext,
StrategiesContext,
useSignInFirstFactorStep,
useSignInSecondFactorStep,
Expand Down Expand Up @@ -86,7 +87,9 @@ export function SignInStrategy({ children, name }: SignInStrategyProps) {
};
}, [factorCtx, name]);

return active ? <>{children}</> : null;
return active ? (
<SignInStrategyContext.Provider value={{ strategy: name }}>{children}</SignInStrategyContext.Provider>
) : null;
}

/**
Expand Down
43 changes: 30 additions & 13 deletions packages/ui/src/components/sign-in/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as Icon from '~/primitives/icon';
import { LinkButton } from '~/primitives/link-button';
import { SecondaryButton } from '~/primitives/secondary-button';
import { Seperator } from '~/primitives/seperator';
import { formatSafeIdentifier } from '~/utils/format-safe-identifier';

/**
* Implementation Details:
Expand Down Expand Up @@ -293,7 +294,7 @@ export function SignInComponentLoaded() {
<Card.Description>{t('signIn.password.subtitle')}</Card.Description>
<Card.Description>
<span className='flex items-center justify-center gap-2'>
<SignIn.SafeIdentifier />
<SignIn.SafeIdentifier transform={formatSafeIdentifier} />
<SignIn.Action
navigate='start'
asChild
Expand Down Expand Up @@ -678,9 +679,13 @@ export function SignInComponentLoaded() {
asChild
>
<SecondaryButton icon={<Icon.LinkSm />}>
{t('signIn.alternativeMethods.blockButton__emailLink', {
identifier: SignIn.SafeIdentifier,
})}
<SignIn.SafeIdentifier
transform={(identifier: string) =>
t('signIn.alternativeMethods.blockButton__emailLink', {
identifier,
})
}
/>
</SecondaryButton>
</SignIn.SupportedStrategy>

Expand All @@ -689,9 +694,13 @@ export function SignInComponentLoaded() {
asChild
>
<SecondaryButton icon={<Icon.Envelope />}>
{t('signIn.alternativeMethods.blockButton__emailCode', {
identifier: SignIn.SafeIdentifier,
})}
<SignIn.SafeIdentifier
transform={(identifier: string) =>
t('signIn.alternativeMethods.blockButton__emailCode', {
identifier,
})
}
/>
</SecondaryButton>
</SignIn.SupportedStrategy>
</div>
Expand Down Expand Up @@ -759,9 +768,13 @@ export function SignInComponentLoaded() {
asChild
>
<SecondaryButton icon={<Icon.LinkSm />}>
{t('signIn.alternativeMethods.blockButton__emailLink', {
identifier: SignIn.SafeIdentifier,
})}
<SignIn.SafeIdentifier
transform={(identifier: string) =>
t('signIn.alternativeMethods.blockButton__emailLink', {
identifier,
})
}
/>
</SecondaryButton>
</SignIn.SupportedStrategy>

Expand All @@ -770,9 +783,13 @@ export function SignInComponentLoaded() {
asChild
>
<SecondaryButton icon={<Icon.Envelope />}>
{t('signIn.alternativeMethods.blockButton__emailCode', {
identifier: SignIn.SafeIdentifier,
})}
<SignIn.SafeIdentifier
transform={(identifier: string) =>
t('signIn.alternativeMethods.blockButton__emailCode', {
identifier,
})
}
/>
</SecondaryButton>
</SignIn.SupportedStrategy>
</div>
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/src/utils/format-safe-identifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { stringToFormattedPhoneString } from '~/common/phone-number-field/utils';

export const isMaskedIdentifier = (str: string | undefined | null) => str && str.includes('**');

/**
* Formats a string that can contain an email, a username or a phone number.
* Depending on the scenario, the string might be obfuscated (parts of the identifier replaced with "*")
* Refer to the tests for examples.
*/
export function formatSafeIdentifier(str: null): null;
export function formatSafeIdentifier(str: undefined): undefined;
export function formatSafeIdentifier(str: string): string;
export function formatSafeIdentifier(str: string | undefined | null) {
if (!str || str.includes('@') || isMaskedIdentifier(str) || str.match(/[a-zA-Z]/)) {
return str;
}
return stringToFormattedPhoneString(str);
}
Loading