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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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

@wobsorianowobsorianoJul 4, 2024

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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);
}
};