Skip to content
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/small-bees-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/elements": patch
---

Add support for submit and passkey loading scopes
19 changes: 19 additions & 0 deletions packages/elements/src/internals/machines/shared/shared.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function sendToLoading({ context, event }: SendToLoadingProps): void {
// Unrelated to the `context` of each machine, the step passed to the loading event must use BaseRouterLoadingStep
let step: BaseRouterLoadingStep | undefined;
let strategy: SignInStrategy | undefined;
let action: string | undefined;

// By default the loading state is set to `true` when this function is called
// Only if these events are received, the loading state is set to `false`
Expand Down Expand Up @@ -73,32 +74,50 @@ export function sendToLoading({ context, event }: SendToLoadingProps): void {
} else if (context.loadingStep === 'continue') {
step = 'continue';
strategy = undefined;
action = 'action' in event ? event.action : undefined;

return context.parent.send({
type: 'LOADING',
isLoading: true,
step,
strategy,
action,
});
} else if (context.loadingStep === 'reset-password') {
step = 'reset-password';
strategy = undefined;
action = 'action' in event ? event.action : undefined;

return context.parent.send({
type: 'LOADING',
isLoading: true,
step,
strategy,
action,
});
} else if (context.loadingStep === 'start') {
step = 'start';
strategy = undefined;
action = 'action' in event ? event.action : undefined;

return context.parent.send({
type: 'LOADING',
isLoading: true,
step,
strategy,
action,
});
} else {
step = context.loadingStep;
strategy = undefined;
action = 'action' in event ? event.action : undefined;

return context.parent.send({
type: 'LOADING',
isLoading: true,
step,
strategy,
action,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type SignInResetPasswordTags = 'state:pending' | 'state:attempting' | 'st

// ---------------------------------- Events ---------------------------------- //

export type SignInResetPasswordSubmitEvent = { type: 'SUBMIT' };
export type SignInResetPasswordSubmitEvent = { type: 'SUBMIT'; action: 'submit' };

export type SignInResetPasswordEvents = ErrorActorEvent | SignInResetPasswordSubmitEvent;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export const SignInRouterMachine = setup({
isLoading: event.isLoading,
step: event.step,
strategy: event.strategy,
action: event.action,
},
})),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export type SignInStartTags = 'state:pending' | 'state:attempting' | 'state:load

// ---------------------------------- Events ---------------------------------- //

export type SignInStartSubmitEvent = { type: 'SUBMIT' };
export type SignInStartPasskeyEvent = { type: 'AUTHENTICATE.PASSKEY' };
export type SignInStartSubmitEvent = { type: 'SUBMIT'; action: 'submit' };
export type SignInStartPasskeyEvent = { type: 'AUTHENTICATE.PASSKEY'; action: 'passkey' };
export type SignInStartPasskeyAutofillEvent = { type: 'AUTHENTICATE.PASSKEY.AUTOFILL' };
export type SignInStartWeb3Event = { type: 'AUTHENTICATE.WEB3'; strategy: Web3Strategy };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type SignInVerificationTags =

// ---------------------------------- Events ---------------------------------- //

export type SignInVerificationSubmitEvent = { type: 'SUBMIT' };
export type SignInVerificationSubmitEvent = { type: 'SUBMIT'; action: 'submit' };
export type SignInVerificationFactorUpdateEvent = { type: 'STRATEGY.UPDATE'; factor: SignInFactor | undefined };
export type SignInVerificationRetryEvent = { type: 'RETRY' };
export type SignInVerificationStrategyRegisterEvent = { type: 'STRATEGY.REGISTER'; factor: SignInStrategyName };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type SignUpContinueTags = 'state:pending' | 'state:attempting' | 'state:l

// ---------------------------------- Events ---------------------------------- //

export type SignUpContinueSubmitEvent = { type: 'SUBMIT' };
export type SignUpContinueSubmitEvent = { type: 'SUBMIT'; action: 'submit' };

export type SignUpContinueEvents = ErrorActorEvent | SignUpContinueSubmitEvent;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export const SignUpRouterMachine = setup({
isLoading: event.isLoading,
step: event.step,
strategy: event.strategy,
action: event.action,
},
})),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type SignUpStartTags = 'state:pending' | 'state:attempting' | 'state:load

// ---------------------------------- Events ---------------------------------- //

export type SignUpStartSubmitEvent = { type: 'SUBMIT' };
export type SignUpStartSubmitEvent = { type: 'SUBMIT'; action: 'submit' };

// TODO: Consolidate with SignInStartMachine
export type SignUpStartRedirectOauthEvent = { type: 'AUTHENTICATE.OAUTH'; strategy: OAuthStrategy };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type SignUpVerificationFriendlyTags = 'code' | 'email_link' | 'email_code

// ---------------------------------- Events ---------------------------------- //

export type SignUpVerificationSubmitEvent = { type: 'SUBMIT' };
export type SignUpVerificationSubmitEvent = { type: 'SUBMIT'; action: 'submit' };
export type SignUpVerificationNextEvent = { type: 'NEXT'; resource?: SignUpResource };
export type SignUpVerificationRetryEvent = { type: 'RETRY' };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ export type BaseRouterLoadingEvent<TSteps extends BaseRouterLoadingStep> = (
| {
step: TSteps | undefined;
strategy?: never;
action?: string;
}
| {
step?: never;
strategy: SignInStrategy | undefined;
action?: never;
}
) & { type: 'LOADING'; isLoading: boolean };

Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/common/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DISPLAY_NAME = 'ClerkElementsForm';
type FormElement = React.ElementRef<typeof RadixForm>;
export type FormProps = Omit<RadixFormProps, 'children'> & {
children: React.ReactNode;
flowActor?: BaseActorRef<{ type: 'SUBMIT' }>;
flowActor?: BaseActorRef<{ type: 'SUBMIT'; action: 'submit' }>;
};

export const Form = React.forwardRef<FormElement, FormProps>(({ flowActor, onSubmit, ...rest }, forwardedRef) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/elements/src/react/common/form/hooks/use-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useGlobalErrors } from './use-global-errors';
/**
* Provides the form submission handler along with the form's validity via a data attribute
*/
export function useForm({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT' }> }) {
export function useForm({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT'; action: 'submit' }> }) {
const { errors } = useGlobalErrors();

// Register the onSubmit handler for form submission
Expand All @@ -15,7 +15,7 @@ export function useForm({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMI
(event: React.FormEvent<Element>) => {
event.preventDefault();
if (flowActor) {
flowActor.send({ type: 'SUBMIT' });
flowActor.send({ type: 'SUBMIT', action: 'submit' });
}
},
[flowActor],
Expand Down
16 changes: 11 additions & 5 deletions packages/elements/src/react/common/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import { SIGN_UP_STEPS } from '~/react/sign-up/step';
import { isProviderStrategyScope, mapScopeToStrategy } from '~/react/utils/map-scope-to-strategy';

type Strategy = OAuthProvider | SamlStrategy | 'metamask';
type LoadingScope<T extends TSignInStep | TSignUpStep> = 'global' | `step:${T}` | `provider:${Strategy}` | undefined;
type LoadingScope<T extends TSignInStep | TSignUpStep> =
| 'global'
| `step:${T}`
| `provider:${Strategy}`
| 'submit'
| 'passkey'
| undefined;

type LoadingProps = {
scope?: LoadingScope<TSignInStep | TSignUpStep>;
Expand Down Expand Up @@ -134,7 +140,7 @@ type SignInLoadingProps = {
};

function SignInLoading({ children, scope, routerRef }: SignInLoadingProps) {
const [isLoading, { step: loadingStep, strategy }] = useLoading(routerRef);
const [isLoading, { step: loadingStep, strategy, action }] = useLoading(routerRef);
const tags = useSelector(routerRef, s => s.tags);

const isStepLoading = (step: TSignInStep) => isLoading && loadingStep === step;
Expand All @@ -150,7 +156,7 @@ function SignInLoading({ children, scope, routerRef }: SignInLoadingProps) {
loadingResult = isLoading && loadingStep === undefined && strategy === mapScopeToStrategy(scope);
} else if (scope) {
// Specified Loading Scope
loadingResult = isStepLoading(scope.replace('step:', '') as TSignInStep);
loadingResult = isStepLoading(scope.replace('step:', '') as TSignInStep) || scope === action;
} else {
// Inferred Loading Scope
loadingResult =
Expand All @@ -171,7 +177,7 @@ type SignUpLoadingProps = {
};

function SignUpLoading({ children, scope, routerRef }: SignUpLoadingProps) {
const [isLoading, { step: loadingStep, strategy }] = useLoading(routerRef);
const [isLoading, { step: loadingStep, strategy, action }] = useLoading(routerRef);
const tags = useSelector(routerRef, s => s.tags);

const isStepLoading = (step: TSignUpStep) => isLoading && loadingStep === step;
Expand All @@ -186,7 +192,7 @@ function SignUpLoading({ children, scope, routerRef }: SignUpLoadingProps) {
// Provider-Specific Loading Scope
loadingResult = isLoading && loadingStep === undefined && strategy === mapScopeToStrategy(scope);
} else if (scope) {
loadingResult = isStepLoading(scope.replace('step:', '') as TSignUpStep);
loadingResult = isStepLoading(scope.replace('step:', '') as TSignUpStep) || scope === action;
} else {
// Inferred Loading Scope
loadingResult =
Expand Down
6 changes: 3 additions & 3 deletions packages/elements/src/react/hooks/use-loading.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type ActorSignUp = ActorRefFrom<TSignUpRouterMachine>;
type LoadingContext<T> = T extends ActorSignIn ? SignInRouterLoadingContext : SignUpRouterLoadingContext;
type UseLoadingReturn<T> = [
isLoading: boolean,
{ step: LoadingContext<T>['step']; strategy: LoadingContext<T>['strategy'] },
{ step: LoadingContext<T>['step']; strategy: LoadingContext<T>['strategy']; action: LoadingContext<T>['action'] },
];

const selectLoading = <T extends SnapshotFrom<TSignInRouterMachine> | SnapshotFrom<TSignUpRouterMachine>>(
Expand All @@ -33,8 +33,8 @@ export function useLoading<TActor extends ActorSignIn | ActorSignUp>(actor: TAct
const loadingCtx = useSelector(actor, selectLoading, compareLoadingValue) as LoadingContext<TActor>;

if (!loadingCtx) {
return [false, { step: undefined, strategy: undefined }];
return [false, { step: undefined, strategy: undefined, action: undefined }];
}

return [loadingCtx.isLoading, { step: loadingCtx.step, strategy: loadingCtx.strategy }];
return [loadingCtx.isLoading, { step: loadingCtx.step, strategy: loadingCtx.strategy, action: loadingCtx.action }];
}
30 changes: 15 additions & 15 deletions packages/ui/src/components/sign-in/steps/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,32 +66,32 @@ export function SignInResetPassword() {
</div>
</Card.Body>

<Common.Loading>
{isSubmitting => {
return (
<Card.Actions>
<Card.Actions>
<Common.Loading scope='submit'>
{isSubmitting => {
return (
<SignIn.Action
submit
asChild
>
<Button
busy={isSubmitting}
disabled={isGlobalLoading || isSubmitting}
disabled={isGlobalLoading}
>
{t('signIn.resetPassword.formButtonPrimary')}
</Button>
</SignIn.Action>
);
}}
</Common.Loading>

<SignIn.Action
navigate='start'
asChild
>
<LinkButton>{t('backButton')}</LinkButton>
</SignIn.Action>
</Card.Actions>
);
}}
</Common.Loading>
<SignIn.Action
navigate='start'
asChild
>
<LinkButton>{t('backButton')}</LinkButton>
</SignIn.Action>
</Card.Actions>
</Card.Content>
<Card.Footer {...cardFooterProps} />
</Card.Root>
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/components/sign-in/steps/start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function SignInStart() {
) : null}
</Card.Body>
<Card.Actions>
<Common.Loading>
<Common.Loading scope='submit'>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will need this added to each submit action across both sign in and signup. Not just the start step.

{isSubmitting => {
return (
<SignIn.Action
Expand All @@ -162,13 +162,13 @@ export function SignInStart() {
// setState on click, but we'll need to find a way to clean
// up the state based on `isSubmitting`
passkeyEnabled ? (
<Common.Loading>
<Common.Loading scope='passkey'>
{isSubmitting => {
return (
<SignIn.Passkey asChild>
<LinkButton
type='button'
disabled={isGlobalLoading || isSubmitting}
disabled={isGlobalLoading}
>
{t('signIn.start.actionLink__use_passkey')}
</LinkButton>
Expand Down
Loading