diff --git a/.changeset/old-sides-check.md b/.changeset/old-sides-check.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/old-sides-check.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index b7e97d6e999..0e6ac9e13f9 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -36,6 +36,11 @@ export const createLongRunningApps = () => { { id: 'next.appRouter.withNeedsClientTrust', config: next.appRouter, env: envs.withNeedsClientTrust }, { id: 'next.appRouter.withSharedUIVariant', config: next.appRouter, env: envs.withSharedUIVariant }, + /** + * NextJS apps - bundled UI + */ + { id: 'next.appRouterBundledUI.withEmailCodes', config: next.appRouterBundledUI, env: envs.withEmailCodes }, + /** * NextJS apps - cache components */ diff --git a/integration/presets/next.ts b/integration/presets/next.ts index b2073bdb5ca..ae212742525 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -36,6 +36,21 @@ const appRouterAPWithClerkNextV6 = appRouterQuickstartV6 .setName('next-app-router-ap-clerk-next-v6') .addDependency('@clerk/nextjs', '6'); +const appRouterBundledUI = applicationConfig() + .setName('next-app-router-bundled-ui') + .useTemplate(templates['next-app-router-bundled-ui']) + .setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`) + .addScript('setup', constants.E2E_NPM_FORCE ? 'pnpm install --force' : 'pnpm install') + .addScript('dev', 'pnpm dev') + .addScript('build', 'pnpm build') + .addScript('serve', 'pnpm start') + .addDependency('next', constants.E2E_NEXTJS_VERSION) + .addDependency('react', constants.E2E_REACT_VERSION) + .addDependency('react-dom', constants.E2E_REACT_DOM_VERSION) + .addDependency('@clerk/nextjs', constants.E2E_CLERK_JS_VERSION || linkPackage('nextjs')) + .addDependency('@clerk/shared', linkPackage('shared')) + .addDependency('@clerk/ui', constants.E2E_CLERK_UI_VERSION || linkPackage('ui')); + const cacheComponents = applicationConfig() .setName('next-cache-components') .useTemplate(templates['next-cache-components']) @@ -54,5 +69,6 @@ export const next = { appRouterAPWithClerkNextLatest, appRouterAPWithClerkNextV6, appRouterQuickstartV6, + appRouterBundledUI, cacheComponents, } as const; diff --git a/integration/templates/index.ts b/integration/templates/index.ts index 4e439b6d18e..b1ed93218a9 100644 --- a/integration/templates/index.ts +++ b/integration/templates/index.ts @@ -7,6 +7,7 @@ export const templates = { 'next-app-router': resolve(__dirname, './next-app-router'), 'next-cache-components': resolve(__dirname, './next-cache-components'), 'next-app-router-quickstart': resolve(__dirname, './next-app-router-quickstart'), + 'next-app-router-bundled-ui': resolve(__dirname, './next-app-router-bundled-ui'), 'next-app-router-quickstart-v6': resolve(__dirname, './next-app-router-quickstart-v6'), 'react-cra': resolve(__dirname, './react-cra'), 'react-vite': resolve(__dirname, './react-vite'), diff --git a/integration/templates/next-app-router-bundled-ui/.gitignore b/integration/templates/next-app-router-bundled-ui/.gitignore new file mode 100644 index 00000000000..8f322f0d8f4 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/integration/templates/next-app-router-bundled-ui/next.config.js b/integration/templates/next-app-router-bundled-ui/next.config.js new file mode 100644 index 00000000000..658404ac690 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +module.exports = nextConfig; diff --git a/integration/templates/next-app-router-bundled-ui/package.json b/integration/templates/next-app-router-bundled-ui/package.json new file mode 100644 index 00000000000..17a85e0571f --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "next-app-router-bundled-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev", + "lint": "next lint", + "start": "next start" + }, + "dependencies": { + "@types/node": "^20.12.12", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "next": "^15.0.1", + "react": "19.2.4", + "react-dom": "19.2.4", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=20.9.0" + } +} diff --git a/integration/templates/next-app-router-bundled-ui/public/next.svg b/integration/templates/next-app-router-bundled-ui/public/next.svg new file mode 100644 index 00000000000..5174b28c565 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/templates/next-app-router-bundled-ui/public/vercel.svg b/integration/templates/next-app-router-bundled-ui/public/vercel.svg new file mode 100644 index 00000000000..d2f84222734 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration/templates/next-app-router-bundled-ui/src/app/favicon.ico b/integration/templates/next-app-router-bundled-ui/src/app/favicon.ico new file mode 100644 index 00000000000..718d6fea483 Binary files /dev/null and b/integration/templates/next-app-router-bundled-ui/src/app/favicon.ico differ diff --git a/integration/templates/next-app-router-bundled-ui/src/app/globals.css b/integration/templates/next-app-router-bundled-ui/src/app/globals.css new file mode 100644 index 00000000000..760b257c8cc --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/globals.css @@ -0,0 +1,78 @@ +:root { + --max-width: 1100px; + --border-radius: 12px; + --font-mono: + ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', + 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; + + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; + + --primary-glow: conic-gradient( + from 180deg at 50% 50%, + #16abff33 0deg, + #0885ff33 55deg, + #54d6ff33 120deg, + #0071ff33 160deg, + transparent 360deg + ); + --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); + + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + + --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); + --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3)); + + --tile-start-rgb: 2, 13, 46; + --tile-end-rgb: 2, 5, 19; + --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80); + + --callout-rgb: 20, 20, 20; + --callout-border-rgb: 108, 108, 108; + --card-rgb: 100, 100, 100; + --card-border-rgb: 200, 200, 200; + } +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/integration/templates/next-app-router-bundled-ui/src/app/layout.tsx b/integration/templates/next-app-router-bundled-ui/src/app/layout.tsx new file mode 100644 index 00000000000..3cf9ae7a673 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/layout.tsx @@ -0,0 +1,28 @@ +import './globals.css'; +import { Inter } from 'next/font/google'; +import { ClerkProvider } from '@clerk/nextjs'; +import { ui } from '@clerk/ui'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata = { + title: 'Bundled UI Test App', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/integration/templates/next-app-router-bundled-ui/src/app/page.tsx b/integration/templates/next-app-router-bundled-ui/src/app/page.tsx new file mode 100644 index 00000000000..cdf9540f65d --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/page.tsx @@ -0,0 +1,17 @@ +import { Show, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs'; + +export default function Home() { + return ( +
+ +

signed-out-state

+ + +
+ +

signed-in-state

+ +
+
+ ); +} diff --git a/integration/templates/next-app-router-bundled-ui/src/app/sign-in/[[...catchall]]/page.tsx b/integration/templates/next-app-router-bundled-ui/src/app/sign-in/[[...catchall]]/page.tsx new file mode 100644 index 00000000000..d193e28a464 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/sign-in/[[...catchall]]/page.tsx @@ -0,0 +1,14 @@ +import { SignIn } from '@clerk/nextjs'; + +export default function Page() { + return ( +
+ Loading sign in} + /> +
+ ); +} diff --git a/integration/templates/next-app-router-bundled-ui/src/app/sign-up/[[...catchall]]/page.tsx b/integration/templates/next-app-router-bundled-ui/src/app/sign-up/[[...catchall]]/page.tsx new file mode 100644 index 00000000000..b26b0967f31 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/sign-up/[[...catchall]]/page.tsx @@ -0,0 +1,14 @@ +import { SignUp } from '@clerk/nextjs'; + +export default function Page() { + return ( +
+ Loading sign up} + /> +
+ ); +} diff --git a/integration/templates/next-app-router-bundled-ui/src/app/themes/page.tsx b/integration/templates/next-app-router-bundled-ui/src/app/themes/page.tsx new file mode 100644 index 00000000000..a1707768122 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/themes/page.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { SignIn } from '@clerk/nextjs'; +import { dark, neobrutalism, shadesOfPurple, shadcn } from '@clerk/ui/themes'; + +export default function ThemesPage() { + return ( +
+
+

Dark

+ Loading dark theme} + /> +
+
+

Neobrutalism

+ Loading neobrutalism theme} + /> +
+
+

Shades of Purple

+ Loading shadesOfPurple theme} + /> +
+
+

Shadcn

+ Loading shadcn theme} + /> +
+
+ ); +} diff --git a/integration/templates/next-app-router-bundled-ui/src/app/user-button/page.tsx b/integration/templates/next-app-router-bundled-ui/src/app/user-button/page.tsx new file mode 100644 index 00000000000..9d776a7809f --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/app/user-button/page.tsx @@ -0,0 +1,9 @@ +import { UserButton } from '@clerk/nextjs'; + +export default function Page() { + return ( +
+ Loading user button} /> +
+ ); +} diff --git a/integration/templates/next-app-router-bundled-ui/src/middleware.ts b/integration/templates/next-app-router-bundled-ui/src/middleware.ts new file mode 100644 index 00000000000..71c73d054cb --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/src/middleware.ts @@ -0,0 +1,7 @@ +import { clerkMiddleware } from '@clerk/nextjs/server'; + +export default clerkMiddleware(); + +export const config = { + matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], +}; diff --git a/integration/templates/next-app-router-bundled-ui/tsconfig.json b/integration/templates/next-app-router-bundled-ui/tsconfig.json new file mode 100644 index 00000000000..eb0b41d94d5 --- /dev/null +++ b/integration/templates/next-app-router-bundled-ui/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/integration/tests/bundled-ui.test.ts b/integration/tests/bundled-ui.test.ts new file mode 100644 index 00000000000..44739e41b29 --- /dev/null +++ b/integration/tests/bundled-ui.test.ts @@ -0,0 +1,108 @@ +import { expect, test } from '@playwright/test'; + +import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../testUtils'; + +testAgainstRunningApps({ withPattern: ['next.appRouterBundledUI.*'] })( + 'bundled UI smoke tests @bundled-ui', + ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + let fakeUser: FakeUser; + + test.beforeAll(async () => { + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + await u.services.users.createBapiUser(fakeUser); + }); + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('does not fetch ui.browser.js from an external URL', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + const externalUiRequests: string[] = []; + + page.on('request', request => { + const url = request.url(); + if (url.includes('ui.browser.js') && !url.startsWith(app.serverUrl)) { + externalUiRequests.push(url); + } + }); + + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + await u.po.expect.toBeSignedOut(); + + expect(externalUiRequests).toEqual([]); + }); + + test('Clerk client loads and renders sign-in/sign-up buttons on home page', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + await u.po.expect.toBeSignedOut(); + + await expect(u.page.getByRole('button', { name: /Sign in/i })).toBeVisible(); + await expect(u.page.getByRole('button', { name: /Sign up/i })).toBeVisible(); + }); + + test('SignIn component renders on /sign-in page', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + }); + + test('SignUp component renders on /sign-up page', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signUp.goTo(); + await u.po.signUp.waitForMounted(); + }); + + test('can sign in with email and password', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + }); + + test('UserButton renders after sign in', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/user-button'); + await u.po.userButton.waitForMounted(); + await expect(u.page.getByRole('button', { name: /Open user menu/i })).toBeVisible(); + }); + + test('can sign out through user button', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToAppHome(); + await u.po.userButton.waitForMounted(); + await u.po.userButton.toggleTrigger(); + await u.po.userButton.waitForPopover(); + await u.po.userButton.triggerSignOut(); + await u.po.expect.toBeSignedOut(); + }); + + test('themes page renders SignIn components with all themes', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/themes'); + await u.page.waitForClerkJsLoaded(); + + await expect(u.page.getByText('Dark')).toBeVisible(); + await expect(u.page.getByText('Neobrutalism')).toBeVisible(); + await expect(u.page.getByText('Shades of Purple')).toBeVisible(); + await expect(u.page.getByText('Shadcn')).toBeVisible(); + }); + }, +);