Skip to content

Commit

Permalink
feat(elements): Remove hardcoded /sign-in path (#2725)
Browse files Browse the repository at this point in the history
* feat(shared): Add joinURL helper

* chore(elements): Example fixes

* feat(elements): Do not hardcode /sign-in

Co-authored-by: Bryce Kalow <br.kalow@gmail.com>

* chore(elements): Chores

* chore(repo): Add empty changeset

* chore(elements): Bump package version

* chore(elements): Bump package version

* chore(repo): Add changeset

* chore(repo): Delete changeset+

* Update thirty-kings-trade.md

* chore(elements): Use SocialProviderIcon from correct imports

* chore(elements): Allow passing required prop to CustomField

* fix(elements): Use OTPInput when name="code"

* chore(elements): Add some JSDoc comments

---------

Co-authored-by: Bryce Kalow <br.kalow@gmail.com>
  • Loading branch information
LekoArts and BRKalow committed Feb 6, 2024
1 parent 5d6937c commit be99136
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/thirty-kings-trade.md
@@ -0,0 +1,5 @@
---
'@clerk/shared': patch
---

Add `joinURL` helper to `@clerk/shared/url`
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -7,7 +7,7 @@ import { AnimatePresence, motion } from 'framer-motion';

export default function Page() {
return (
<SignIn>
<SignIn path='/otp-playground'>
<Start>
<div className='h-dvh flex items-center justify-center bg-neutral-800'>
<Field name='code'>
Expand Down
13 changes: 13 additions & 0 deletions packages/elements/examples/nextjs/app/page.tsx
Expand Up @@ -58,6 +58,19 @@ export default function Home() {
<p className='m-0 max-w-[30ch] text-sm opacity-50'>Sign up using Elements</p>
</Link>

<Link
href='/otp-playground'
className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
>
<h2 className='mb-3 text-2xl font-semibold'>
OTP{' '}
<span className='inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none'>
-&gt;
</span>
</h2>
<p className='m-0 max-w-[30ch] text-sm opacity-50'>OTP Playground</p>
</Link>

<a
href='https://clerk.com/docs/custom-flows/overview#sign-in-flow'
className='group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30'
Expand Down
@@ -1,12 +1,19 @@
'use client';

import { GlobalError, Submit } from '@clerk/elements/common';
import { Continue, Factor, SignIn, SocialProvider, Start, Verification } from '@clerk/elements/sign-in';
import {
Continue,
Factor,
SignIn,
SocialProvider,
SocialProviderIcon,
Start,
Verification,
} from '@clerk/elements/sign-in';

import { H1, H2, H3, HR as Hr, P } from '@/components/design';
import { CustomField, CustomSubmit } from '@/components/form';
import { SignInDebug } from '@/components/sign-in-debug';
import { SocialProviderIcon } from '@/components/social-providers';

export default function SignInPage() {
return (
Expand Down
@@ -1,12 +1,19 @@
'use client';

import { GlobalError } from '@clerk/elements/common';
import { Continue, SignUp, SocialProvider, Start, Verification, Verify } from '@clerk/elements/sign-up';
import {
Continue,
SignUp,
SocialProvider,
SocialProviderIcon,
Start,
Verification,
Verify,
} from '@clerk/elements/sign-up';

import { H1, HR as Hr } from '@/components/design';
import { CustomField, CustomSubmit } from '@/components/form';
import { SignUpDebug } from '@/components/sign-up-debug';
import { SocialProviderIcon } from '@/components/social-providers';

export default function SignUpPage() {
return (
Expand Down
62 changes: 32 additions & 30 deletions packages/elements/examples/nextjs/components/form.tsx
Expand Up @@ -32,38 +32,40 @@ function OTPInputSegment({ value, status }: any) {
);
}

export const CustomField = forwardRef<typeof Input, { name: string; label: string }>(function CustomField(
{ name, label },
forwardedRef,
) {
const inputProps =
name === 'code'
? {
render: OTPInputSegment,
}
: {
className: 'bg-tertiary rounded-sm px-2 py-1 border border-foreground data-[invalid]:border-red-500',
ref: forwardedRef,
};
export const CustomField = forwardRef<typeof Input, { name: string; label: string; required?: boolean }>(
function CustomField({ name, label, required = false }, forwardedRef) {
const inputProps =
name === 'code'
? {
render: OTPInputSegment,
className: 'flex gap-3',
required,
}
: {
className: 'bg-tertiary rounded-sm px-2 py-1 border border-foreground data-[invalid]:border-red-500',
ref: forwardedRef,
required,
};

return (
<ElementsField
name={name}
className='flex flex-col gap-4'
>
<div className='flex gap-4 justify-between items-center'>
<Label>{label}</Label>
<Input
name={name}
{...inputProps}
/>
</div>
return (
<ElementsField
name={name}
className='flex flex-col gap-4'
>
<div className='flex gap-4 justify-between items-center'>
<Label>{label}</Label>
<Input
name={name}
{...inputProps}
/>
</div>

<FieldError className='block text-red-400 font-mono' />
<FieldState>{({ state }) => <pre className='opacity-60 text-xs'>Field state: {state}</pre>}</FieldState>
</ElementsField>
);
});
<FieldError className='block text-red-400 font-mono' />
<FieldState>{({ state }) => <pre className='opacity-60 text-xs'>Field state: {state}</pre>}</FieldState>
</ElementsField>
);
},
);

export const CustomSubmit = forwardRef<HTMLButtonElement, React.ComponentPropsWithoutRef<'button'>>(
function CustomButton(props, forwardedRef) {
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/package.json
@@ -1,6 +1,6 @@
{
"name": "@clerk/elements",
"version": "0.1.1",
"version": "0.1.2",
"description": "Clerk Elements",
"keywords": [
"clerk",
Expand Down
69 changes: 30 additions & 39 deletions packages/elements/src/internals/machines/sign-in/sign-in.machine.ts
@@ -1,5 +1,6 @@
import type { ClerkAPIResponseError } from '@clerk/shared/error';
import { isClerkAPIResponseError } from '@clerk/shared/error';
import { joinURL } from '@clerk/shared/url';
import type {
LoadedClerk,
OAuthStrategy,
Expand Down Expand Up @@ -41,12 +42,14 @@ export interface SignInMachineContext extends MachineContext {
resource: SignInResource | null;
router: ClerkRouter;
thirdPartyProviders: EnabledThirdPartyProviders;
signUpPath: string;
}

export interface SignInMachineInput {
clerk: LoadedClerk;
form: ActorRefFrom<typeof FormMachine>;
router: ClerkRouter;
signUpPath: string;
}

export type SignInMachineTags = 'state:start' | 'state:first-factor' | 'state:second-factor' | 'external';
Expand Down Expand Up @@ -115,7 +118,11 @@ export const SignInMachine = setup({
console.error(event.error);
},
navigateTo({ context }, { path }: { path: string }) {
context.router.replace(path);
const resolvedPath = joinURL(context.router.basePath, path);
context.router.replace(resolvedPath);
},
navigateToSignUp({ context }) {
context.router.push(context.signUpPath);
},
raiseFailure: raise(({ event }) => {
assertActorEventError(event);
Expand Down Expand Up @@ -143,8 +150,7 @@ export const SignInMachine = setup({
isCurrentFactorPassword: ({ context }) => context.currentFactor?.strategy === 'password',
isCurrentFactorTOTP: ({ context }) => context.currentFactor?.strategy === 'totp',
isCurrentPath: ({ context }, params: { path: string }) => {
const path = params?.path;
return path ? context.router.pathname() === path : false;
return context.router.match(params?.path);
},
isLoggedIn: ({ context }) => Boolean(context.clerk.user),
isSignInComplete: ({ context }) => context?.resource?.status === 'complete',
Expand Down Expand Up @@ -179,6 +185,7 @@ export const SignInMachine = setup({
resource: null,
router: input.router,
thirdPartyProviders: getEnabledThirdPartyProviders(input.clerk.__unstable__environment),
signUpPath: input.signUpPath,
}),
initial: 'Init',
on: {
Expand All @@ -198,22 +205,22 @@ export const SignInMachine = setup({
},
{
description: 'If the SignIn resource is empty, invoke the sign-in start flow',
guard: or([not('hasSignInResource'), { type: 'isCurrentPath', params: { path: '/sign-in' } }]),
guard: or([not('hasSignInResource'), { type: 'isCurrentPath', params: { path: '/' } }]),
target: 'Start',
},
{
description: 'Go to FirstFactor flow state',
guard: and(['needsFirstFactor', { type: 'isCurrentPath', params: { path: '/sign-in/continue' } }]),
guard: and(['needsFirstFactor', { type: 'isCurrentPath', params: { path: '/continue' } }]),
target: 'FirstFactor',
},
{
description: 'Go to SecondFactor flow state',
guard: and(['needsSecondFactor', { type: 'isCurrentPath', params: { path: '/sign-in/continue' } }]),
guard: and(['needsSecondFactor', { type: 'isCurrentPath', params: { path: '/continue' } }]),
target: 'SecondFactor',
},
{
description: 'Go to SSO Callback state',
guard: { type: 'isCurrentPath', params: { path: '/sign-in/sso-callback' } },
guard: { type: 'isCurrentPath', params: { path: '/sso-callback' } },
target: 'SSOCallback',
},
{
Expand Down Expand Up @@ -241,12 +248,20 @@ export const SignInMachine = setup({
Start: {
id: 'Start',
tags: 'state:start',
description: 'The intial state of the sign-in flow.',
description: 'The initial state of the sign-in flow.',
initial: 'AwaitingInput',
on: {
'AUTHENTICATE.OAUTH': '#SignIn.AuthenticatingWithRedirect',
'AUTHENTICATE.SAML': '#SignIn.AuthenticatingWithRedirect',
},
entry: [
{
type: 'navigateTo',
params: {
path: '/',
},
},
],
onDone: [
{
guard: 'isSignInComplete',
Expand Down Expand Up @@ -297,7 +312,7 @@ export const SignInMachine = setup({
FirstFactor: {
tags: 'state:first-factor',
initial: 'DeterminingState',
entry: 'assignStartingFirstFactor',
entry: [{ type: 'navigateTo', params: { path: '/continue' } }, 'assignStartingFirstFactor'],
onDone: [
{
guard: 'isSignInComplete',
Expand Down Expand Up @@ -385,7 +400,7 @@ export const SignInMachine = setup({
SecondFactor: {
tags: 'state:second-factor',
initial: 'DeterminingState',
entry: 'assignStartingSecondFactor',
entry: [{ type: 'navigateTo', params: { path: '/continue' } }, 'assignStartingSecondFactor'],
onDone: [
{
guard: 'isSignInComplete',
Expand Down Expand Up @@ -507,48 +522,24 @@ export const SignInMachine = setup({
'CLERKJS.NAVIGATE.RESET_PASSWORD': '#SignIn.NotImplemented',
'CLERKJS.NAVIGATE.SIGN_IN': {
actions: [
log('Navigating to sign in'),
log('Navigating to sign in root'),
{
type: 'navigateTo',
params: {
path: '/sign-in',
path: '/',
},
},
],
},
'CLERKJS.NAVIGATE.SIGN_UP': {
actions: [
log('Navigating to sign in'),
{
type: 'navigateTo',
params: {
path: '/sign-up',
},
},
],
actions: [log('Navigating to sign up'), 'navigateToSignUp'],
},
'CLERKJS.NAVIGATE.VERIFICATION': {
actions: [
log('Navigating to sign in'),
{
type: 'navigateTo',
params: {
path: '/sign-up',
},
},
],
actions: [log('Navigating to sign in'), 'navigateToSignUp'],
},
'CLERKJS.NAVIGATE.CONTINUE': {
description: 'Redirect to the sign-up flow',
actions: [
log('Navigating to sign up'),
{
type: 'navigateTo',
params: {
path: '/sign-up',
},
},
],
actions: [log('Navigating to sign up'), 'navigateToSignUp'],
},
'CLERKJS.NAVIGATE.*': {
target: '#SignIn.Start',
Expand Down
Expand Up @@ -118,8 +118,6 @@ function fieldsToSignUpParams<T extends SignUpCreateParams | SignUpUpdateParams>
): Pick<T, SignUpAdditionalKeys> {
const params: SignUpUpdateParams = {};

// TODO: Determine what takes priority

fields.forEach(({ value }, key) => {
if (isSignUpParam(key) && value !== undefined) {
params[key] = value as string;
Expand Down
Expand Up @@ -329,7 +329,7 @@ export const SignUpMachine = setup({
Start: {
id: 'Start',
tags: 'state:start',
description: 'The intial state of the sign-in flow.',
description: 'The intial state of the sign-up flow.',
entry: 'assignThirdPartyProviders',
initial: 'AwaitingInput',
on: {
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/common/form/index.tsx
Expand Up @@ -148,7 +148,7 @@ const useInput = ({ name: inputName, value: initialValue, type: inputType, ...pa
const shouldBeHidden = false;
const type = inputType ?? determineInputTypeFromName(name);

const Element = inputType === 'otp' ? OTPInput : RadixControl;
const Element = type === 'otp' ? OTPInput : RadixControl;

let props = {};
if (inputType === 'otp') {
Expand Down
5 changes: 5 additions & 0 deletions packages/elements/src/react/router/router.ts
Expand Up @@ -14,6 +14,10 @@ export type ClerkHostRouter = {
* Internal Clerk router, used by Clerk components to interact with the host's router.
*/
export type ClerkRouter = {
/**
* The basePath the router is currently mounted on.
*/
basePath: string;
/**
* Creates a child router instance scoped to the provided base path.
*/
Expand Down Expand Up @@ -108,5 +112,6 @@ export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/
replace,
pathname: router.pathname,
searchParams: router.searchParams,
basePath: normalizedBasePath,
};
}

0 comments on commit be99136

Please sign in to comment.