diff --git a/.changeset/many-boxes-beg.md b/.changeset/many-boxes-beg.md
new file mode 100644
index 00000000000..53dc02b70e7
--- /dev/null
+++ b/.changeset/many-boxes-beg.md
@@ -0,0 +1,5 @@
+---
+"@clerk/astro": patch
+---
+
+Remove `@nanostores/react` from depedency.
diff --git a/.changeset/ten-planes-run.md b/.changeset/ten-planes-run.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/ten-planes-run.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts
index 5d62e882059..54724dc005d 100644
--- a/integration/presets/envs.ts
+++ b/integration/presets/envs.ts
@@ -44,8 +44,6 @@ const withEmailLinks = environmentConfig()
const withCustomRoles = environmentConfig()
.setId('withCustomRoles')
.setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true)
- // Temporarily use the stage api until the custom roles feature is released to prod
- .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev')
.setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-custom-roles'].sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-custom-roles'].pk)
.setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in')
diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts
index b13eafeefac..e6077faa291 100644
--- a/integration/presets/longRunningApps.ts
+++ b/integration/presets/longRunningApps.ts
@@ -23,7 +23,7 @@ export const createLongRunningApps = () => {
{ id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles },
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
{ id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
- { id: 'astro.node.withEmailCodes', config: astro.node, env: envs.withEmailCodes },
+ { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },
] as const;
const apps = configs.map(longRunningApplication);
diff --git a/integration/templates/astro-node/src/components/page-with-user.tsx b/integration/templates/astro-node/src/components/page-with-user.tsx
new file mode 100644
index 00000000000..547c01ee7b9
--- /dev/null
+++ b/integration/templates/astro-node/src/components/page-with-user.tsx
@@ -0,0 +1,17 @@
+import React, { type PropsWithChildren, useSyncExternalStore } from 'react';
+import { UserProfile } from '@clerk/astro/client/react';
+import { $userStore } from '@clerk/astro/client/stores';
+
+export function PageWithUser({ children }: PropsWithChildren) {
+ const user = useSyncExternalStore($userStore.listen, $userStore.get, $userStore.get);
+
+ return (
+ <>
+
My name is: {user?.firstName}
+
+
+
+ {children}
+ >
+ );
+}
diff --git a/integration/templates/astro-node/src/layouts/Protected.astro b/integration/templates/astro-node/src/layouts/Protected.astro
deleted file mode 100644
index a36699ec99d..00000000000
--- a/integration/templates/astro-node/src/layouts/Protected.astro
+++ /dev/null
@@ -1,5 +0,0 @@
----
-const { userId } = Astro.locals.auth()
----
-
-{userId && }
diff --git a/integration/templates/astro-node/src/layouts/Streaming.astro b/integration/templates/astro-node/src/layouts/Streaming.astro
index 326f15f02a9..e19b870db3c 100644
--- a/integration/templates/astro-node/src/layouts/Streaming.astro
+++ b/integration/templates/astro-node/src/layouts/Streaming.astro
@@ -2,7 +2,7 @@
await new Promise((res) =>
setTimeout(() => {
res(undefined);
- }, 1000),
+ }, 4000),
);
---
diff --git a/integration/templates/astro-node/src/layouts/react/Layout.astro b/integration/templates/astro-node/src/layouts/react/Layout.astro
new file mode 100644
index 00000000000..84f6aedcff8
--- /dev/null
+++ b/integration/templates/astro-node/src/layouts/react/Layout.astro
@@ -0,0 +1,143 @@
+---
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+
+import { SignedIn, SignedOut, UserButton } from "@clerk/astro/client/react";
+import { LanguagePicker } from "../../components/LanguagePicker";
+---
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/index.astro b/integration/templates/astro-node/src/pages/index.astro
index ae6a9ed78b6..b3d2a4e0a19 100644
--- a/integration/templates/astro-node/src/pages/index.astro
+++ b/integration/templates/astro-node/src/pages/index.astro
@@ -3,7 +3,7 @@ import Layout from "../layouts/Layout.astro";
import Card from "../components/Card.astro";
import SignOutButton from "../lib/astro-components/SignOutButton.astro";
-import { SignedIn, SignedOut } from "@clerk/astro/client/react";
+import { SignedIn, SignedOut } from "@clerk/astro/components/control";
import { OrganizationSwitcher } from "@clerk/astro/components/interactive";
---
@@ -24,17 +24,12 @@ import { OrganizationSwitcher } from "@clerk/astro/components/interactive";
-
- To get started, open the directory src/pages in your project.
- Code Challenge: Tweak the "Welcome to Astro" message above.
-
-
+
-
+
+ Welcome to Astro + React
+
+
+
+
+
+
+
+
+ To get started, open the directory src/pages in your project.
+ Code Challenge: Tweak the "Welcome to Astro" message above.
+
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/react/only-admins.astro b/integration/templates/astro-node/src/pages/react/only-admins.astro
new file mode 100644
index 00000000000..fa82e45d5bc
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/react/only-admins.astro
@@ -0,0 +1,19 @@
+---
+import { Protect } from "@clerk/astro/client/react";
+import Layout from "../../layouts/react/Layout.astro";
+---
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/react/only-members.astro b/integration/templates/astro-node/src/pages/react/only-members.astro
new file mode 100644
index 00000000000..76fe4c5e74a
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/react/only-members.astro
@@ -0,0 +1,19 @@
+---
+import { Protect } from "@clerk/astro/client/react";
+import Layout from "../../layouts/react/Layout.astro";
+---
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/react/sign-in.astro b/integration/templates/astro-node/src/pages/react/sign-in.astro
new file mode 100644
index 00000000000..90b2bf3e89b
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/react/sign-in.astro
@@ -0,0 +1,10 @@
+---
+import { SignIn } from "@clerk/astro/client/react";
+import Layout from "../../layouts/react/Layout.astro";
+---
+
+
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/react/user.astro b/integration/templates/astro-node/src/pages/react/user.astro
new file mode 100644
index 00000000000..2de93c316cd
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/react/user.astro
@@ -0,0 +1,13 @@
+---
+import { UserProfile } from "@clerk/astro/components/interactive";
+import Layout from "../../layouts/react/Layout.astro";
+import {PageWithUser} from "../../components/page-with-user";
+import StreamUser from "../../components/StreamUser.astro";
+---
+
+
+
+
+
+
+
diff --git a/integration/templates/astro-node/src/pages/user.astro b/integration/templates/astro-node/src/pages/user.astro
index 1c35ba389a8..dfed7f34ff7 100644
--- a/integration/templates/astro-node/src/pages/user.astro
+++ b/integration/templates/astro-node/src/pages/user.astro
@@ -1,15 +1,12 @@
---
import { UserProfile } from "@clerk/astro/components/interactive";
import Layout from "../layouts/Layout.astro";
-import Protected from "../layouts/Protected.astro";
import StreamUser from "../components/StreamUser.astro";
---
-
-
-
-
-
-
+
+
+
+
diff --git a/integration/tests/astro.test.ts b/integration/tests/astro/components.test.ts
similarity index 75%
rename from integration/tests/astro.test.ts
rename to integration/tests/astro/components.test.ts
index aca306ee720..70e0f883cfb 100644
--- a/integration/tests/astro.test.ts
+++ b/integration/tests/astro/components.test.ts
@@ -1,9 +1,9 @@
import { expect, test } from '@playwright/test';
-import type { FakeUser } from '../testUtils';
-import { createTestUtils, testAgainstRunningApps } from '../testUtils';
+import type { FakeUser } from '../../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../../testUtils';
-testAgainstRunningApps({ withPattern: ['astro.node.withEmailCodes'] })('test astro @astro', ({ app }) => {
+testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic flows for @astro', ({ app }) => {
test.describe.configure({ mode: 'parallel' });
let fakeUser: FakeUser;
@@ -64,7 +64,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withEmailCodes'] })('test ast
await expect(u.page.getByText(/profile details/i)).toBeVisible();
});
- test('render user profile', async ({ page, context }) => {
+ test('render user profile with streamed data', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/sign-in');
await u.po.signIn.waitForMounted();
@@ -84,4 +84,16 @@ testAgainstRunningApps({ withPattern: ['astro.node.withEmailCodes'] })('test ast
await u.page.waitForURL(`${app.serverUrl}/sign-in?redirect_url=${encodeURIComponent(`${app.serverUrl}/user`)}`);
await u.po.signIn.waitForMounted();
});
+
+ test('SignedIn, SignedOut SSR', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToStart();
+ await expect(u.page.getByText('Go to this page to log in')).toBeVisible();
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+ await expect(u.page.getByText('Go to this page to see your profile')).toBeVisible();
+ await expect(u.page.getByText('Sign out!')).toBeVisible();
+ });
});
diff --git a/integration/tests/astro/protect.test.ts b/integration/tests/astro/protect.test.ts
new file mode 100644
index 00000000000..5fb4dc857a6
--- /dev/null
+++ b/integration/tests/astro/protect.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from '@playwright/test';
+
+import type { FakeOrganization, FakeUser } from '../../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../../testUtils';
+
+testAgainstRunningApps({
+ withPattern: ['astro.node.withCustomRoles'],
+})('protect @astro with react', ({ app }) => {
+ test.describe.configure({ mode: 'parallel' });
+
+ let fakeAdmin: FakeUser;
+ let fakeOrganization: FakeOrganization;
+
+ test.beforeAll(async () => {
+ const m = createTestUtils({ app });
+ fakeAdmin = m.services.users.createFakeUser();
+ const admin = await m.services.users.createBapiUser(fakeAdmin);
+ fakeOrganization = await m.services.users.createFakeOrganization(admin.id);
+ });
+
+ test.afterAll(async () => {
+ await fakeOrganization.delete();
+ await fakeAdmin.deleteIfExists();
+ await app.teardown();
+ });
+
+ test('only admin', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+ await u.po.organizationSwitcher.waitForMounted();
+ await u.po.organizationSwitcher.toggleTrigger();
+ await u.page.locator('.cl-organizationSwitcherPreviewButton').click();
+ await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
+ await u.page.goToRelative('/only-admins');
+ await expect(u.page.getByText("I'm an admin")).toBeVisible();
+ });
+
+ test('only member', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/sign-in#/?redirect_url=/only-members');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+ await expect(u.page.getByText('Not a member')).toBeVisible();
+ });
+});
diff --git a/integration/tests/astro/react/components.test.ts b/integration/tests/astro/react/components.test.ts
new file mode 100644
index 00000000000..d32fdd5e4a1
--- /dev/null
+++ b/integration/tests/astro/react/components.test.ts
@@ -0,0 +1,71 @@
+import { expect, test } from '@playwright/test';
+
+import type { FakeUser } from '../../../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../../../testUtils';
+
+testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })(
+ 'basic flows for @astro with react',
+ ({ app }) => {
+ test.describe.configure({ mode: 'serial' });
+ let fakeUser: FakeUser;
+
+ test.beforeAll(async () => {
+ const m = createTestUtils({ app });
+ fakeUser = m.services.users.createFakeUser();
+ await m.services.users.createBapiUser(fakeUser);
+ });
+
+ test.afterAll(async () => {
+ await fakeUser.deleteIfExists();
+ await app.teardown();
+ });
+
+ test('render user button', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/react/sign-in#/?redirect_url=/react');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.page.waitForAppUrl('/react');
+ await u.po.expect.toBeSignedIn();
+
+ await u.po.userButton.waitForMounted();
+ await u.po.userButton.toggleTrigger();
+ await u.po.userButton.waitForPopover();
+
+ await u.po.userButton.toHaveVisibleMenuItems([/Manage account/i, /Sign out$/i]);
+
+ await u.po.userButton.triggerManageAccount();
+ await u.po.userProfile.waitForUserProfileModal();
+
+ await expect(u.page.getByText(/profile details/i)).toBeVisible();
+ });
+
+ test('render user profile with streamed data', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/react/sign-in#/?redirect_url=/react');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+ await u.po.userButton.waitForMounted();
+ await u.page.goToRelative('/react/user');
+ await u.po.userProfile.waitForMounted();
+
+ await expect(u.page.getByText(`My name is: ${fakeUser.firstName}`)).toBeVisible();
+
+ // Streams data from Astro.locals.currentUser()
+ await expect(u.page.getByText(`"firstName":"${fakeUser.firstName}"`)).toBeVisible();
+ });
+
+ test('SignedIn, SignedOut SSR', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/react');
+ await expect(u.page.getByText('Go to this page to log in')).toBeVisible();
+ await u.page.goToRelative('/react/sign-in#/?redirect_url=/react');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+ await expect(u.page.getByText('Go to this page to see your profile')).toBeVisible();
+ await expect(u.page.getByText('Sign out!')).toBeVisible();
+ });
+ },
+);
diff --git a/integration/tests/astro/react/protect.test.ts b/integration/tests/astro/react/protect.test.ts
new file mode 100644
index 00000000000..0b67d98aca4
--- /dev/null
+++ b/integration/tests/astro/react/protect.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from '@playwright/test';
+
+import type { FakeOrganization, FakeUser } from '../../../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../../../testUtils';
+
+testAgainstRunningApps({
+ withPattern: ['astro.node.withCustomRoles'],
+})('protect @astro with react', ({ app }) => {
+ test.describe.configure({ mode: 'parallel' });
+
+ let fakeAdmin: FakeUser;
+ let fakeOrganization: FakeOrganization;
+
+ test.beforeAll(async () => {
+ const m = createTestUtils({ app });
+ fakeAdmin = m.services.users.createFakeUser();
+ const admin = await m.services.users.createBapiUser(fakeAdmin);
+ fakeOrganization = await m.services.users.createFakeOrganization(admin.id);
+ });
+
+ test.afterAll(async () => {
+ await fakeOrganization.delete();
+ await fakeAdmin.deleteIfExists();
+ await app.teardown();
+ });
+
+ test('only admin', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/react/sign-in#/?redirect_url=/react');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+ await u.po.organizationSwitcher.waitForMounted();
+ await u.po.organizationSwitcher.toggleTrigger();
+ await u.page.locator('.cl-organizationSwitcherPreviewButton').click();
+ await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
+ await u.page.goToRelative('/react/only-admins');
+ await expect(u.page.getByText("I'm an admin")).toBeVisible();
+ });
+
+ test('only member', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/react/sign-in#/?redirect_url=/react/only-members');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+ await expect(u.page.getByText('Not a member')).toBeVisible();
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 314e473b81e..f4e5800cc0c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8896,24 +8896,6 @@
"darwin"
]
},
- "node_modules/@nanostores/react": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-0.7.2.tgz",
- "integrity": "sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "peerDependencies": {
- "nanostores": "^0.9.0 || ^0.10.0",
- "react": ">=18.0.0"
- }
- },
"node_modules/@netlify/functions": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.0.tgz",
@@ -48386,7 +48368,6 @@
"@clerk/clerk-js": "5.8.1",
"@clerk/shared": "2.3.2",
"@clerk/types": "4.7.0",
- "@nanostores/react": "0.7.2",
"nanoid": "5.0.7",
"nanostores": "0.10.3",
"path-to-regexp": "6.2.2"
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 951c008306b..f46f5d41afe 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -113,7 +113,6 @@
"@clerk/clerk-js": "5.8.1",
"@clerk/shared": "2.3.2",
"@clerk/types": "4.7.0",
- "@nanostores/react": "0.7.2",
"nanoid": "5.0.7",
"nanostores": "0.10.3",
"path-to-regexp": "6.2.2"
diff --git a/packages/astro/src/client/react/utils.tsx b/packages/astro/src/client/react/utils.tsx
index 21b9f33ebb4..b99ed6276ec 100644
--- a/packages/astro/src/client/react/utils.tsx
+++ b/packages/astro/src/client/react/utils.tsx
@@ -1,10 +1,18 @@
import type { LoadedClerk } from '@clerk/types';
-import { useStore } from '@nanostores/react';
-import { computed } from 'nanostores';
+import { computed, type Store, type StoreValue } from 'nanostores';
import React from 'react';
import { $clerk, $csrState } from '../../stores/internal';
+/**
+ * This implementation of `useStore` is an alternative solution to the hook exported by nanostores
+ * Reference: https://github.com/nanostores/react/blob/main/index.js
+ */
+function useStore>(store: T): SV {
+ const get = store.get.bind(store);
+ return React.useSyncExternalStore(store.listen, get, get);
+}
+
export const withClerk = (
Component: React.ComponentType
,
displayName?: string,