diff --git a/.changeset/hungry-pumas-protect.md b/.changeset/hungry-pumas-protect.md new file mode 100644 index 00000000000..b2fbac09189 --- /dev/null +++ b/.changeset/hungry-pumas-protect.md @@ -0,0 +1,5 @@ +--- +"@clerk/astro": patch +--- + +Add unstyled authentication button components for Astro and React integration diff --git a/packages/astro/package.json b/packages/astro/package.json index d823b33004d..4f2197f6e71 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -97,6 +97,8 @@ "./components/react/*": "./components/react/*", "./components/interactive": "./components/interactive/index.ts", "./components/interactive/*": "./components/interactive/*", + "./components/unstyled": "./components/unstyled/index.ts", + "./components/unstyled/*": "./components/unstyled/*", "./components/*": "./components/*", "./package.json": "./package.json" }, diff --git a/packages/astro/src/astro-components/unstyled/SignInButton.astro b/packages/astro/src/astro-components/unstyled/SignInButton.astro new file mode 100644 index 00000000000..df78768bede --- /dev/null +++ b/packages/astro/src/astro-components/unstyled/SignInButton.astro @@ -0,0 +1,48 @@ +--- +import type { HTMLTag, Polymorphic } from 'astro/types' +import type { SignInProps } from "@clerk/types"; +type Props = Polymorphic + +import { customAlphabet, urlAlphabet } from "nanoid"; + +const safeId = customAlphabet(urlAlphabet, 10)(); + +const { + as: Tag = 'button', + forceRedirectUrl, + fallbackRedirectUrl, + signUpFallbackRedirectUrl, + signUpForceRedirectUrl, + mode, + ...elementProps +} = Astro.props + +const signInOptions = { + forceRedirectUrl, + fallbackRedirectUrl, + signUpFallbackRedirectUrl, + signUpForceRedirectUrl, +}; +--- + + + Sign in + + + diff --git a/packages/astro/src/astro-components/unstyled/SignOutButton.astro b/packages/astro/src/astro-components/unstyled/SignOutButton.astro new file mode 100644 index 00000000000..13242cb0ee2 --- /dev/null +++ b/packages/astro/src/astro-components/unstyled/SignOutButton.astro @@ -0,0 +1,28 @@ +--- +import type { HTMLTag, Polymorphic } from 'astro/types' +import type { SignOutOptions } from '@clerk/types'; +type Props = Polymorphic<{ as: Tag; } & SignOutOptions> + +import { customAlphabet, urlAlphabet } from "nanoid"; + +const safeId = customAlphabet(urlAlphabet, 10)(); + +const { + as: Tag = 'button', + redirectUrl = '/', + sessionId, + ...elementProps +} = Astro.props +--- + + + Sign out + + + diff --git a/packages/astro/src/astro-components/unstyled/SignUpButton.astro b/packages/astro/src/astro-components/unstyled/SignUpButton.astro new file mode 100644 index 00000000000..a32b074c806 --- /dev/null +++ b/packages/astro/src/astro-components/unstyled/SignUpButton.astro @@ -0,0 +1,50 @@ +--- +import type { HTMLTag, Polymorphic } from 'astro/types' +import type { SignUpProps } from "@clerk/types"; +type Props = Polymorphic + +import { customAlphabet, urlAlphabet } from "nanoid"; + +const safeId = customAlphabet(urlAlphabet, 10)(); + +const { + as: Tag = 'button', + fallbackRedirectUrl, + forceRedirectUrl, + signInFallbackRedirectUrl, + signInForceRedirectUrl, + mode, + unsafeMetadata, + ...elementProps +} = Astro.props + +const signUpOptions = { + fallbackRedirectUrl, + forceRedirectUrl, + signInFallbackRedirectUrl, + signInForceRedirectUrl, + unsafeMetadata, +} +--- + + + Sign up + + + diff --git a/packages/astro/src/astro-components/unstyled/index.ts b/packages/astro/src/astro-components/unstyled/index.ts new file mode 100644 index 00000000000..d62e09924be --- /dev/null +++ b/packages/astro/src/astro-components/unstyled/index.ts @@ -0,0 +1,8 @@ +// The `ts-ignore` comments here are necessary because we're importing this file inside the `astro:components` +// virtual module's types, which means that `tsc` will try to resolve these imports. Don't mind the editor errors. +// @ts-ignore +export { default as SignInButton } from './SignInButton.astro'; +// @ts-ignore +export { default as SignUpButton } from './SignUpButton.astro'; +// @ts-ignore +export { default as SignOutButton } from './SignOutButton.astro'; diff --git a/packages/astro/src/client/react/SignInButton.tsx b/packages/astro/src/client/react/SignInButton.tsx new file mode 100644 index 00000000000..d519108efc0 --- /dev/null +++ b/packages/astro/src/client/react/SignInButton.tsx @@ -0,0 +1,46 @@ +import type { SignInProps } from '@clerk/types'; +import React from 'react'; + +import type { SignInButtonProps } from './types'; +import { assertSingleChild, normalizeWithDefaultValue, safeExecute, withClerk, type WithClerkProp } from './utils'; + +export type { SignInButtonProps }; + +export const SignInButton = withClerk(({ clerk, children, ...props }: WithClerkProp) => { + const { signUpFallbackRedirectUrl, forceRedirectUrl, fallbackRedirectUrl, signUpForceRedirectUrl, mode, ...rest } = + props; + children = normalizeWithDefaultValue(children, 'Sign in'); + const child = assertSingleChild(children)('SignInButton'); + + const clickHandler = () => { + const opts: SignInProps = { + forceRedirectUrl, + fallbackRedirectUrl, + signUpFallbackRedirectUrl, + signUpForceRedirectUrl, + }; + + if (!clerk) { + return; + } + + if (mode === 'modal') { + return clerk.openSignIn(opts); + } + return clerk.redirectToSignIn({ + ...opts, + signInFallbackRedirectUrl: fallbackRedirectUrl, + signInForceRedirectUrl: forceRedirectUrl, + }); + }; + + const wrappedChildClickHandler: React.MouseEventHandler = async e => { + if (child && typeof child === 'object' && 'props' in child) { + await safeExecute(child.props.onClick)(e); + } + return clickHandler(); + }; + + const childProps = { ...rest, onClick: wrappedChildClickHandler }; + return React.cloneElement(child as React.ReactElement, childProps); +}, 'SignInButton'); diff --git a/packages/astro/src/client/react/SignOutButton.tsx b/packages/astro/src/client/react/SignOutButton.tsx new file mode 100644 index 00000000000..efe3abf4722 --- /dev/null +++ b/packages/astro/src/client/react/SignOutButton.tsx @@ -0,0 +1,28 @@ +import type { SignOutOptions } from '@clerk/types'; +import React from 'react'; + +import type { WithClerkProp } from './utils'; +import { assertSingleChild, normalizeWithDefaultValue, safeExecute, withClerk } from './utils'; + +export type SignOutButtonProps = SignOutOptions & { + children?: React.ReactNode; +}; + +export const SignOutButton = withClerk( + ({ clerk, children, ...props }: React.PropsWithChildren>) => { + const { redirectUrl = '/', sessionId, ...rest } = props; + + children = normalizeWithDefaultValue(children, 'Sign out'); + const child = assertSingleChild(children)('SignOutButton'); + + const clickHandler = () => clerk?.signOut({ redirectUrl, sessionId }); + const wrappedChildClickHandler: React.MouseEventHandler = async e => { + await safeExecute((child as any).props.onClick)(e); + return clickHandler(); + }; + + const childProps = { ...rest, onClick: wrappedChildClickHandler }; + return React.cloneElement(child as React.ReactElement, childProps); + }, + 'SignOutButton', +); diff --git a/packages/astro/src/client/react/SignUpButton.tsx b/packages/astro/src/client/react/SignUpButton.tsx new file mode 100644 index 00000000000..b37651e5609 --- /dev/null +++ b/packages/astro/src/client/react/SignUpButton.tsx @@ -0,0 +1,56 @@ +import type { SignUpProps } from '@clerk/types'; +import React from 'react'; + +import type { SignUpButtonProps } from './types'; +import { assertSingleChild, normalizeWithDefaultValue, safeExecute, withClerk, type WithClerkProp } from './utils'; + +export type { SignUpButtonProps }; + +export const SignUpButton = withClerk(({ clerk, children, ...props }: WithClerkProp) => { + const { + fallbackRedirectUrl, + forceRedirectUrl, + signInFallbackRedirectUrl, + signInForceRedirectUrl, + mode, + unsafeMetadata, + ...rest + } = props; + + children = normalizeWithDefaultValue(children, 'Sign up'); + const child = assertSingleChild(children)('SignUpButton'); + + const clickHandler = () => { + const opts: SignUpProps = { + fallbackRedirectUrl, + forceRedirectUrl, + signInFallbackRedirectUrl, + signInForceRedirectUrl, + unsafeMetadata, + }; + + if (!clerk) { + return; + } + + if (mode === 'modal') { + return clerk.openSignUp(opts); + } + + return clerk.redirectToSignUp({ + ...opts, + signUpFallbackRedirectUrl: fallbackRedirectUrl, + signUpForceRedirectUrl: forceRedirectUrl, + }); + }; + + const wrappedChildClickHandler: React.MouseEventHandler = async e => { + if (child && typeof child === 'object' && 'props' in child) { + await safeExecute(child.props.onClick)(e); + } + return clickHandler(); + }; + + const childProps = { ...rest, onClick: wrappedChildClickHandler }; + return React.cloneElement(child as React.ReactElement, childProps); +}, 'SignUpButton'); diff --git a/packages/astro/src/client/react/index.ts b/packages/astro/src/client/react/index.ts index fca69ad5a18..9b8cbf229f4 100644 --- a/packages/astro/src/client/react/index.ts +++ b/packages/astro/src/client/react/index.ts @@ -1,3 +1,10 @@ +import { SignInButton, type SignInButtonProps } from './SignInButton'; +import { SignOutButton, type SignOutButtonProps } from './SignOutButton'; +import { SignUpButton, type SignUpButtonProps } from './SignUpButton'; + export * from './uiComponents'; export * from './controlComponents'; export * from './hooks'; +export { SignInButton, SignOutButton, SignUpButton }; + +export type { SignInButtonProps, SignOutButtonProps, SignUpButtonProps }; diff --git a/packages/astro/src/client/react/types.ts b/packages/astro/src/client/react/types.ts new file mode 100644 index 00000000000..3028beaaf5d --- /dev/null +++ b/packages/astro/src/client/react/types.ts @@ -0,0 +1,23 @@ +import type { SignInProps, SignUpProps } from '@clerk/types'; + +// TODO-SHARED: Duplicate from @clerk/clerk-react +type ButtonProps = { + mode?: 'redirect' | 'modal'; + children?: React.ReactNode; +}; + +// TODO-SHARED: Duplicate from @clerk/clerk-react +export type SignInButtonProps = ButtonProps & + Pick< + SignInProps, + 'fallbackRedirectUrl' | 'forceRedirectUrl' | 'signUpForceRedirectUrl' | 'signUpFallbackRedirectUrl' + >; + +// TODO-SHARED: Duplicate from @clerk/clerk-react +export type SignUpButtonProps = { + unsafeMetadata?: SignUpUnsafeMetadata; +} & ButtonProps & + Pick< + SignUpProps, + 'fallbackRedirectUrl' | 'forceRedirectUrl' | 'signInForceRedirectUrl' | 'signInFallbackRedirectUrl' + >; diff --git a/packages/astro/src/client/react/utils.tsx b/packages/astro/src/client/react/utils.tsx index 4842c113c30..21b9f33ebb4 100644 --- a/packages/astro/src/client/react/utils.tsx +++ b/packages/astro/src/client/react/utils.tsx @@ -37,3 +37,34 @@ export const withClerk =

( export type WithClerkProp = T & { clerk: LoadedClerk | undefined | null; }; + +// TODO-SHARED: Duplicate from @clerk/clerk-react +export const assertSingleChild = + (children: React.ReactNode) => + (name: 'SignInButton' | 'SignUpButton' | 'SignOutButton' | 'SignInWithMetamaskButton') => { + try { + return React.Children.only(children); + } catch (e) { + return `You've passed multiple children components to <${name}/>. You can only pass a single child component or text.`; + } + }; + +// TODO-SHARED: Duplicate from @clerk/clerk-react +export const normalizeWithDefaultValue = (children: React.ReactNode | undefined, defaultText: string) => { + if (!children) { + children = defaultText; + } + if (typeof children === 'string') { + children = ; + } + return children; +}; + +// TODO-SHARED: Duplicate from @clerk/clerk-react +export const safeExecute = + (cb: unknown) => + (...args: any) => { + if (cb && typeof cb === 'function') { + return cb(...args); + } + };