Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
65ab498
feat(astro): Introduce Astro SDK
panteliselef Jul 1, 2024
52bb715
chore(astro): Cleanup
panteliselef Jul 1, 2024
f6ae5ae
chore(astro): Drop deprecations
panteliselef Jul 1, 2024
0421dd0
chore(astro): Make hotloading the default
panteliselef Jul 1, 2024
3f6b158
chore(astro): add react unstyled components
wobsoriano Jul 2, 2024
2e0aa4f
chore(astro): add astro unstyled components
wobsoriano Jul 2, 2024
82226fd
chore(astro): fix typescript error in signout click handler
wobsoriano Jul 2, 2024
243f023
chore(astro): update polymorphic element name for clarity
wobsoriano Jul 2, 2024
a596fd2
chore(astro): spread element props in dynamic tag
wobsoriano Jul 4, 2024
f247208
chore(astro): make sure options dont merge with element props
wobsoriano Jul 4, 2024
9809f1d
chore(astro): add missing redirectUrl prop type
wobsoriano Jul 4, 2024
2a79d56
chore(astro): add default slot content
wobsoriano Jul 4, 2024
5b5272d
chore(astro): add changeset
wobsoriano Jul 5, 2024
62b269a
chore(astro): add comments regarding duplicate functions
wobsoriano Jul 8, 2024
d405525
chore(astro): add comments regarding duplicate functions
wobsoriano Jul 8, 2024
45ae288
chore(astro): use data attribute in referencing unstyled components
wobsoriano Jul 8, 2024
fb964c8
Merge branch 'main' into rob/eco-2-export-unstyled-components
wobsoriano Jul 8, 2024
0754f04
chore(astro): Set default Prop value for Polymorphic types
wobsoriano Jul 9, 2024
209e67f
fix(astro): Add build step as dependency for e2e tests (#3677)
panteliselef Jul 8, 2024
f4a1500
fix(elements): Correct TS types for verification machine (#3671)
LekoArts Jul 9, 2024
6c609c7
chore(astro): Optimize stream modification (#3673)
wobsoriano Jul 9, 2024
380c329
chore(astro): Add session id prop to sign out button
wobsoriano Jul 9, 2024
ec8a1d4
Merge branch 'main' into rob/eco-2-export-unstyled-components
wobsoriano Jul 9, 2024
61db931
chore(astro): Remove wrapped unstyled react components
wobsoriano Jul 9, 2024
7e92620
chore(astro): Fix children prop type
wobsoriano Jul 9, 2024
0d9ccf9
Merge branch 'main' into rob/eco-2-export-unstyled-components
wobsoriano Jul 9, 2024
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/hungry-pumas-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/astro": patch
---

Add unstyled authentication button components for Astro and React integration
2 changes: 2 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
48 changes: 48 additions & 0 deletions packages/astro/src/astro-components/unstyled/SignInButton.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types'
import type { SignInProps } from "@clerk/types";
type Props<Tag extends HTMLTag = HTMLTag> = Polymorphic<SignInProps & { as: Tag; mode?: 'redirect' | 'modal' }>

import { customAlphabet, urlAlphabet } from "nanoid";

const safeId = customAlphabet(urlAlphabet, 10)();
Comment on lines +6 to +8

@wobsoriano wobsoriano Jul 4, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Starting to notice we're repeating this piece of logic in all our Astro components. I'll make a ticket that will address this


const {
as: Tag = 'button',
forceRedirectUrl,
fallbackRedirectUrl,
signUpFallbackRedirectUrl,
signUpForceRedirectUrl,
mode,
...elementProps
} = Astro.props

const signInOptions = {
forceRedirectUrl,
fallbackRedirectUrl,
signUpFallbackRedirectUrl,
signUpForceRedirectUrl,
};
---

<Tag {...elementProps} data-clerk-unstyled-id={safeId}>
<slot>Sign in</slot>
</Tag >

<script is:inline define:vars={{ signInOptions, mode, safeId }}>
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);

btn.addEventListener("click", () => {
const clerk = window.Clerk

if (mode === 'modal') {
return clerk.openSignIn(signInOptions);
}

return clerk.redirectToSignIn({
...signInOptions,
signInFallbackRedirectUrl: signInOptions.fallbackRedirectUrl,
signInForceRedirectUrl: signInOptions.forceRedirectUrl,
});
});
</script>
28 changes: 28 additions & 0 deletions packages/astro/src/astro-components/unstyled/SignOutButton.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types'
import type { SignOutOptions } from '@clerk/types';
type Props<Tag extends HTMLTag = HTMLTag> = Polymorphic<{ as: Tag; } & SignOutOptions>

import { customAlphabet, urlAlphabet } from "nanoid";

const safeId = customAlphabet(urlAlphabet, 10)();

const {
as: Tag = 'button',
redirectUrl = '/',
sessionId,
...elementProps
} = Astro.props
---

<Tag {...elementProps} data-clerk-unstyled-id={safeId}>
<slot>Sign out</slot>
</Tag >

<script is:inline define:vars={{ redirectUrl, sessionId, safeId }}>
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);

btn.addEventListener("click", () => {
window.Clerk.signOut({ redirectUrl, sessionId })
});
</script>
50 changes: 50 additions & 0 deletions packages/astro/src/astro-components/unstyled/SignUpButton.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types'
import type { SignUpProps } from "@clerk/types";
type Props<Tag extends HTMLTag = HTMLTag> = Polymorphic<SignUpProps & { as: Tag; mode?: 'redirect' | 'modal' }>

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,
}
---

<Tag {...elementProps} data-clerk-unstyled-id={safeId}>
<slot>Sign up</slot>
</Tag >

<script is:inline define:vars={{ signUpOptions, mode, safeId }}>
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);

btn.addEventListener("click", () => {
const clerk = window.Clerk

if (mode === 'modal') {
return clerk.openSignUp(signUpOptions);
}

return clerk.redirectToSignUp({
...signUpOptions,
signUpFallbackRedirectUrl: signUpOptions.fallbackRedirectUrl,
signUpForceRedirectUrl: signUpOptions.forceRedirectUrl,
});
});
</script>
8 changes: 8 additions & 0 deletions packages/astro/src/astro-components/unstyled/index.ts
Original file line number Diff line number Diff line change
@@ -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';
46 changes: 46 additions & 0 deletions packages/astro/src/client/react/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -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<SignInButtonProps>) => {
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<unknown>, childProps);
}, 'SignInButton');
28 changes: 28 additions & 0 deletions packages/astro/src/client/react/SignOutButton.tsx
Original file line number Diff line number Diff line change
@@ -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<WithClerkProp<SignOutButtonProps>>) => {
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<unknown>, childProps);
},
'SignOutButton',
);
56 changes: 56 additions & 0 deletions packages/astro/src/client/react/SignUpButton.tsx
Original file line number Diff line number Diff line change
@@ -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<SignUpButtonProps>) => {
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<unknown>, childProps);
}, 'SignUpButton');
7 changes: 7 additions & 0 deletions packages/astro/src/client/react/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
23 changes: 23 additions & 0 deletions packages/astro/src/client/react/types.ts
Original file line number Diff line number Diff line change
@@ -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'
>;
31 changes: 31 additions & 0 deletions packages/astro/src/client/react/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,34 @@ export const withClerk = <P extends { clerk: LoadedClerk | undefined | null }>(
export type WithClerkProp<T = unknown> = 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 = <button type='button'>{children}</button>;
}
return children;
};

// TODO-SHARED: Duplicate from @clerk/clerk-react
export const safeExecute =
(cb: unknown) =>
(...args: any) => {
if (cb && typeof cb === 'function') {
return cb(...args);
}
};