diff --git a/.changeset/wild-pianos-matter.md b/.changeset/wild-pianos-matter.md new file mode 100644 index 00000000000..9630956da7e --- /dev/null +++ b/.changeset/wild-pianos-matter.md @@ -0,0 +1,18 @@ +--- +'@clerk/chrome-extension': minor +--- + +Added new `createClerkClient()` export from @clerk/chrome-extension/client + +```ts +import { createClerkClient } from '@clerk/chrome-extension/client'; + +const publishableKey = process.env.CLERK_PUBLISHABLE_KEY; +// Use createClerkClient in a popup or side panel +const clerk = createClerkClient({ publishableKey }); + +// Use createClerkClient in a background service worker +const clerk = await createClerkClient({ publishableKey: 'pk_...', background: true }); +``` + +`createClerkClient()` from @clerk/chrome-extension/background is deprecated. diff --git a/.gitignore b/.gitignore index 1adb12ec3e6..b7afcc544e2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ out-tsc out **/dist/* **/build/* +!playground/browser-extension-js/build/manifest.json +!playground/browser-extension-js/build/popup.html +!playground/browser-extension-js/build/popup.css packages/*/dist/** **/.pnpm-store/** @@ -62,6 +65,7 @@ lerna-debug.log .dev.vars .env.local playground/*/build +!playground/browser-extension-js/build playground/*/public/build playground/*/.cache playground/custom diff --git a/packages/chrome-extension/README.md b/packages/chrome-extension/README.md index cab49740c87..88ec35c5006 100644 --- a/packages/chrome-extension/README.md +++ b/packages/chrome-extension/README.md @@ -41,41 +41,44 @@ Please see the latest extension [authentication support matrix](https://clerk.co ### Usage -1. **Installation:** `npm install @clerk/chrome-extension` -2. **Set a consistent extension key**: A browser extension can be identified by its unique key, in a similar way to how a website can be identified by its domain. You will need to explicitly configure your extension's key or it will change often. If the key changes, it can cause the extension to fail. See the [Configure a Consistent Key](https://clerk.com/docs/references/chrome-extension/configure-consistent-crx-id?utm_source=github&utm_medium=clerk_chrome_extension) guide for more information. -3. **Update Clerk Settings**: Once you've set up a consistent extension key, you'll need to configure your Clerk settings to allow the extension to communicate with your Clerk API. - You can do this by adding the extension key to the list of allowed origins in your Clerk settings. Setting the `allowed_origins` is **required** for both **Development** and **Production** instances. - - ```bash - curl -X PATCH https://api.clerk.com/v1/instance \ - -H "Content-type: application/json" \ - -H "Authorization: Bearer " \ - -d '{"allowed_origins": ["chrome-extension://"]}' - ``` - -4. **Set Environment Variables:** Retrieve the **Publishable key** from your [Clerk dashboard](https://dashboard.clerk.com/last-active?path=api-keys&utm_source=github&utm_medium=clerk_chrome_extension) and set it as an environment variable. - - ```sh - # Vite - VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxx - ``` - - ```sh - # Plasmo - PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx - ``` - -5. **Update the extension manifest:** You'll need to update your extension manifest permissions to support Clerk. - 1. [**Base configuration**:](/packages/chrome-extension/docs/manifest.md#base-configuration) Use this if you plan to only use Clerk in the context of the extension. - 2. [**Session sync configuration**:](/packages/chrome-extension/docs/manifest.md#sync-host-configuration) Use this if you plan to share authentication with a website in the same browser. -6. **Add Clerk to your app:** Though not required, we generally suggest using Plasmo for browser extension development. This will enforce common standards across your extension as well as allow for easier integration with other browsers in the future. - 1. [**Via `ClerkProvider`:**](/packages/chrome-extension/docs/clerk-provider.md) This is the general approach to all extensions. From here you'll be able to support extension-only authentication as well as sharing authentication with a website in the same browser. - 2. [**Via service workers**:](/packages/chrome-extension/docs/service-workers.md) If you also require the use of background service workers, this will allow you to access the Clerk client from the extension context. +1. **Installation:** `npm install @clerk/chrome-extension` + +2. **Set a consistent extension key**: A browser extension can be identified by its unique key, in a similar way to how a website can be identified by its domain. You will need to explicitly configure your extension's key or it will change often. If the key changes, it can cause the extension to fail. See the [Configure a Consistent Key](https://clerk.com/docs/references/chrome-extension/configure-consistent-crx-id?utm_source=github&utm_medium=clerk_chrome_extension) guide for more information. + +3. **Update Clerk Settings**: Once you've set up a consistent extension key, you'll need to configure your Clerk settings to allow the extension to communicate with your Clerk API. + You can do this by adding the extension key to the list of allowed origins in your Clerk settings. Setting the `allowed_origins` is **required** for both **Development** and **Production** instances. + + ```bash + curl -X PATCH https://api.clerk.com/v1/instance \ + -H "Content-type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"allowed_origins": ["chrome-extension://"]}' + ``` + +4. **Set Environment Variables:** Retrieve the **Publishable key** from your [Clerk dashboard](https://dashboard.clerk.com/last-active?path=api-keys&utm_source=github&utm_medium=clerk_chrome_extension) and set it as an environment variable. + + ```sh + # Vite + VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxx + ``` + + ```sh + # Plasmo + PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx + ``` + +5. **Update the extension manifest:** You'll need to update your extension manifest permissions to support Clerk. + 1. [**Base configuration**:](/packages/chrome-extension/docs/manifest.md#base-configuration) Use this if you plan to only use Clerk in the context of the extension. + 2. [**Session sync configuration**:](/packages/chrome-extension/docs/manifest.md#sync-host-configuration) Use this if you plan to share authentication with a website in the same browser. + +6. **Add Clerk to your app:** Though not required, we generally suggest using Plasmo for browser extension development. This will enforce common standards across your extension as well as allow for easier integration with other browsers in the future. + 1. [**Via `ClerkProvider`:**](/packages/chrome-extension/docs/clerk-provider.md) This is the general approach to all extensions. From here you'll be able to support extension-only authentication as well as sharing authentication with a website in the same browser. + 2. [**Via service workers**:](/packages/chrome-extension/docs/service-workers.md) If you also require the use of background service workers, this will allow you to access the Clerk client from the extension context. ## Example repositories -- [Standalone](https://github.com/clerk/clerk-chrome-extension-starter/tree/main): The extension is using its own authentication -- [WebSSO](https://github.com/clerk/clerk-chrome-extension-starter/tree/webapp_sso): The extensions shares authentication with a website in the same browser +- [Quickstart](https://github.com/clerk/clerk-chrome-extension-quickstart): The extension is using its own authentication +- [SyncHost, Service Workers and `react-router`](https://github.com/clerk/clerk-chrome-extension-demo): The extension shares auth with a website in the same browser, needs to access user information from Clerk in a service worker or needs to use `react-router` ## Support diff --git a/packages/chrome-extension/client/package.json b/packages/chrome-extension/client/package.json new file mode 100644 index 00000000000..24c8bcbb0e7 --- /dev/null +++ b/packages/chrome-extension/client/package.json @@ -0,0 +1,5 @@ +{ + "main": "../dist/cjs/client/index.js", + "module": "../dist/esm/client/index.js", + "types": "../dist/types/client/index.d.ts" +} diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index caa5af2de82..db51fbd488b 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -22,11 +22,48 @@ }, "license": "MIT", "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./client": { + "import": { + "types": "./dist/types/client/index.d.ts", + "default": "./dist/esm/client/index.js" + }, + "require": { + "types": "./dist/types/client/index.d.ts", + "default": "./dist/cjs/client/index.js" + } + }, + "./background": { + "import": { + "types": "./dist/types/background/index.d.ts", + "default": "./dist/esm/background/index.js" + }, + "require": { + "types": "./dist/types/background/index.d.ts", + "default": "./dist/cjs/background/index.js" + } + }, + "./types": { + "types": "./dist/types/types/index.d.ts" + }, + "./package.json": "./package.json" + }, "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/types/index.d.ts", "files": [ "background", + "client", "dist", "internal", "react", @@ -42,7 +79,7 @@ "format": "node ../../scripts/format-package.mjs", "format:check": "node ../../scripts/format-package.mjs --check", "lint": "eslint src", - "lint:attw": "attw --pack . --profile node16", + "lint:attw": "attw --pack . --profile node16 --ignore-rules unexpected-module-syntax", "lint:publint": "publint", "test": "vitest run", "test:ci": "vitest run --maxWorkers=70%", diff --git a/packages/chrome-extension/src/__tests__/__snapshots__/client-exports.test.ts.snap b/packages/chrome-extension/src/__tests__/__snapshots__/client-exports.test.ts.snap new file mode 100644 index 00000000000..7c65e5da3dc --- /dev/null +++ b/packages/chrome-extension/src/__tests__/__snapshots__/client-exports.test.ts.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`client public exports > should not include a breaking change 1`] = ` +[ + "createClerkClient", +] +`; diff --git a/packages/chrome-extension/src/__tests__/client-exports.test.ts b/packages/chrome-extension/src/__tests__/client-exports.test.ts new file mode 100644 index 00000000000..6dfd1c68a65 --- /dev/null +++ b/packages/chrome-extension/src/__tests__/client-exports.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; + +import * as publicExports from '../client'; + +describe('client public exports', () => { + it('should not include a breaking change', () => { + expect(Object.keys(publicExports).sort()).toMatchSnapshot(); + }); +}); diff --git a/packages/chrome-extension/src/background/clerk.ts b/packages/chrome-extension/src/background/clerk.ts index 57c68d5097d..4da5a01902f 100644 --- a/packages/chrome-extension/src/background/clerk.ts +++ b/packages/chrome-extension/src/background/clerk.ts @@ -3,13 +3,34 @@ import type { Clerk } from '@clerk/clerk-js/no-rhc'; import { createClerkClient as _createClerkClient, type CreateClerkClientOptions as _CreateClerkClientOptions, -} from '../internal'; -import { SCOPE } from '../types'; +} from '../utils/clerk-client'; -export type CreateClerkClientOptions = Omit<_CreateClerkClientOptions, 'scope'>; +/** + * @deprecated Use `createClerkClient` from `@clerk/chrome-extension/client` with `{ background: true }` instead. + * + * @example + * // Before (deprecated): + * import { createClerkClient } from '@clerk/chrome-extension/background'; + * const clerk = await createClerkClient({ publishableKey: 'pk_...' }); + * + * // After: + * import { createClerkClient } from '@clerk/chrome-extension/client'; + * const clerk = await createClerkClient({ publishableKey: 'pk_...', background: true }); + */ +export type CreateClerkClientOptions = Omit<_CreateClerkClientOptions, 'background'>; -export async function createClerkClient(opts: CreateClerkClientOptions): Promise { - const clerk = await _createClerkClient({ ...opts, scope: SCOPE.BACKGROUND }); - await clerk.load({ standardBrowser: false }); - return clerk; +/** + * @deprecated Use `createClerkClient` from `@clerk/chrome-extension/client` with `{ background: true }` instead. + * + * @example + * // Before (deprecated): + * import { createClerkClient } from '@clerk/chrome-extension/background'; + * const clerk = await createClerkClient({ publishableKey: 'pk_...' }); + * + * // After: + * import { createClerkClient } from '@clerk/chrome-extension/client'; + * const clerk = await createClerkClient({ publishableKey: 'pk_...', background: true }); + */ +export function createClerkClient(opts: CreateClerkClientOptions): Promise { + return _createClerkClient({ ...opts, background: true }) as Promise; } diff --git a/packages/chrome-extension/src/client/index.ts b/packages/chrome-extension/src/client/index.ts new file mode 100644 index 00000000000..5be037a2fef --- /dev/null +++ b/packages/chrome-extension/src/client/index.ts @@ -0,0 +1,2 @@ +export { createClerkClient } from '../utils/clerk-client'; +export type { CreateClerkClientOptions } from '../utils/clerk-client'; diff --git a/packages/chrome-extension/src/utils/__tests__/clerk-client.test.ts b/packages/chrome-extension/src/utils/__tests__/clerk-client.test.ts new file mode 100644 index 00000000000..01083b6b52b --- /dev/null +++ b/packages/chrome-extension/src/utils/__tests__/clerk-client.test.ts @@ -0,0 +1,54 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mockLoad = vi.fn().mockResolvedValue(undefined); +const mockUi = { __brand: 'clerk-ui', ClerkUI: vi.fn() }; + +vi.mock('@clerk/clerk-js/no-rhc', () => { + const Clerk = vi.fn(() => ({ + load: mockLoad, + })) as ReturnType & { sdkMetadata: Record }; + Clerk.sdkMetadata = {}; + return { Clerk }; +}); + +vi.mock('@clerk/ui', () => ({ + ui: mockUi, +})); + +import { createClerkClient } from '../clerk-client'; + +describe('createClerkClient', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('non-background (popup)', () => { + it('returns a Clerk instance synchronously', () => { + const clerk = createClerkClient({ publishableKey: 'pk_test_123' }); + expect(clerk).toBeDefined(); + expect(clerk).not.toBeInstanceOf(Promise); + }); + + it('wraps load() to inject @clerk/ui', async () => { + const clerk = createClerkClient({ publishableKey: 'pk_test_123' }); + const loadOpts = { afterSignOutUrl: '/signed-out' }; + + await clerk.load(loadOpts); + + expect(mockLoad).toHaveBeenCalledOnce(); + expect(mockLoad).toHaveBeenCalledWith({ + ...loadOpts, + ui: mockUi, + }); + }); + + it('calls load() with ui even when no options are passed', async () => { + const clerk = createClerkClient({ publishableKey: 'pk_test_123' }); + + await clerk.load(); + + expect(mockLoad).toHaveBeenCalledOnce(); + expect(mockLoad).toHaveBeenCalledWith({ ui: mockUi }); + }); + }); +}); diff --git a/packages/chrome-extension/src/utils/clerk-client.ts b/packages/chrome-extension/src/utils/clerk-client.ts new file mode 100644 index 00000000000..7baa9c5ee56 --- /dev/null +++ b/packages/chrome-extension/src/utils/clerk-client.ts @@ -0,0 +1,36 @@ +import { Clerk } from '@clerk/clerk-js/no-rhc'; + +import { + createClerkClient as _createClerkClient, + type CreateClerkClientOptions as _CreateClerkClientOptions, +} from '../internal'; +import { SCOPE } from '../types'; + +export type CreateClerkClientOptions = Omit<_CreateClerkClientOptions, 'scope'> & { + background?: boolean; +}; + +export function createClerkClient(opts: CreateClerkClientOptions & { background: true }): Promise; +export function createClerkClient(opts: Omit): Clerk; +export function createClerkClient(opts: CreateClerkClientOptions): Clerk | Promise { + if (opts.background) { + const { background: _, ...rest } = opts; + const clerk = _createClerkClient({ ...rest, scope: SCOPE.BACKGROUND }); + return clerk.load({ standardBrowser: false }).then(() => clerk); + } + + Clerk.sdkMetadata = { + name: PACKAGE_NAME, + version: PACKAGE_VERSION, + }; + + const clerk = new Clerk(opts.publishableKey, {}); + + const originalLoad = clerk.load.bind(clerk); + clerk.load = async (loadOpts?: Parameters[0]) => { + const { ui } = await import('@clerk/ui'); + return originalLoad({ ...loadOpts, ui }); + }; + + return clerk; +} diff --git a/packages/chrome-extension/tsup.config.ts b/packages/chrome-extension/tsup.config.ts index e680b4916a3..86e574b9d0f 100644 --- a/packages/chrome-extension/tsup.config.ts +++ b/packages/chrome-extension/tsup.config.ts @@ -9,7 +9,13 @@ export default defineConfig(overrideOptions => { const shouldPublish = !!overrideOptions.env?.publish; const common: Options = { - entry: ['./src/index.ts', './src/background/index.ts', './src/react/index.ts', './src/types/index.ts'], + entry: [ + './src/index.ts', + './src/background/index.ts', + './src/client/index.ts', + './src/react/index.ts', + './src/types/index.ts', + ], bundle: true, clean: true, minify: false, diff --git a/playground/browser-extension-js/.env.example b/playground/browser-extension-js/.env.example new file mode 100644 index 00000000000..044f7822c33 --- /dev/null +++ b/playground/browser-extension-js/.env.example @@ -0,0 +1 @@ +CLERK_PUBLISHABLE_KEY= diff --git a/playground/browser-extension-js/.gitignore b/playground/browser-extension-js/.gitignore new file mode 100644 index 00000000000..d83a4589141 --- /dev/null +++ b/playground/browser-extension-js/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +pnpm-lock.yaml +build/*.js +.env +.yalc +yalc.lock +.npmrc diff --git a/playground/browser-extension-js/README.md b/playground/browser-extension-js/README.md new file mode 100644 index 00000000000..a97991314f7 --- /dev/null +++ b/playground/browser-extension-js/README.md @@ -0,0 +1,126 @@ +# Browser Extension JS Playground + +A Chrome extension demo using `@clerk/chrome-extension` with plain TypeScript. No Chrome extension frameworks (no WXT, Plasmo, CRXJS, etc.). Uses `bun build` to bundle the TypeScript source. + +## Project structure + +``` +src/ + popup.ts # TypeScript source (bundled to build/popup.js) +build/ + manifest.json # Chrome extension manifest (Manifest V3) + popup.html # Popup page + popup.css # Popup styles + popup.js # Bundled output (gitignored) +.env.example # Environment variable template +.env # Your publishable key (gitignored) +``` + +Static extension files (`manifest.json`, `popup.html`, `popup.css`) live directly in `build/` and are checked into git. `bun build` automatically loads `.env`, replaces `process.env.CLERK_PUBLISHABLE_KEY` at build time, and bundles `src/popup.ts` into `build/popup.js`. + +## Getting started + +### 1. Build `@clerk/chrome-extension` and its dependencies + +From the repository root: + +```bash +pnpm turbo build --filter=@clerk/chrome-extension... +``` + +### 2. Start pkglab and publish packages + +From the repository root, start the local registry and publish `@clerk/chrome-extension` (along with its workspace dependencies): + +```bash +pnpm pkglab up +pnpm pkglab pub @clerk/chrome-extension +``` + +### 3. Add packages from pkglab + +```bash +cd playground/browser-extension-js +``` + +To see which packages have been published: + +```bash +pnpm pkglab:ls +``` + +From this playground directory (`playground/browser-extension-js`): + +```bash +pnpm pkglab:add +``` + +### 4. Install dependencies + +This playground is not a pnpm workspace member, so use `pnpm install` to install dependencies: + +```bash +pnpm install --ignore-workspace +``` + +The first time you install packages you will will may to approve builds. Use: + +```bash +pnpm approve-builds --ignore-workspace +``` + +### 5. Set up environment + +Copy `.env.example` to `.env` and add your Clerk publishable key: + +```bash +cp .env.example .env +``` + +Then edit `.env`: + +``` +CLERK_PUBLISHABLE_KEY=pk_test_... +``` + +### 6. Build the extension + +```bash +pnpm build +``` + +### 7. Load the extension in Chrome + +1. Open `chrome://extensions/` +2. Enable **Developer mode** (top right) +3. Click **Load unpacked** +4. Select the `build/` directory inside this playground + +## Development + +To rebuild on file changes: + +```bash +pnpm dev +``` + +## Rebuilding after package changes + +When you make changes to `@clerk/chrome-extension` (or its dependencies), rebuild and republish to pkglab: + +```bash +# From repo root +pnpm turbo build --filter=@clerk/chrome-extension... && pnpm pkglab pub @clerk/chrome-extension + + +# From this playground + +# If you haven't approved build yet +pnpm install --ignore-workspace +pnpm approve-builds +pnpm dev + +# If you have approved builds already +pnpm install +pnpm dev +``` diff --git a/playground/browser-extension-js/build/manifest.json b/playground/browser-extension-js/build/manifest.json new file mode 100644 index 00000000000..dcab155d51b --- /dev/null +++ b/playground/browser-extension-js/build/manifest.json @@ -0,0 +1,17 @@ +{ + "manifest_version": 3, + "name": "Clerk Extension JS Demo", + "description": "A Chrome extension demo using @clerk/chrome-extension with plain TypeScript.", + "version": "0.0.1", + "permissions": [ + "cookies", + "storage" + ], + "host_permissions": [ + "http://localhost/*" + ], + "action": { + "default_title": "Clerk Extension Demo", + "default_popup": "popup.html" + } +} diff --git a/playground/browser-extension-js/build/popup.css b/playground/browser-extension-js/build/popup.css new file mode 100644 index 00000000000..223269c6f61 --- /dev/null +++ b/playground/browser-extension-js/build/popup.css @@ -0,0 +1,134 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 600px; + max-height: 600px; + height: 600px; + background: #111; + color: #fff; + font-family: system-ui, sans-serif; +} + +#app { + display: flex; + flex-direction: column; + min-height: 600px; +} + +#content { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + padding: 1rem; +} + +h1 { + font-size: 1.1rem; + font-weight: 600; + padding: 1rem 2rem; + text-align: center; +} + +#nav { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0.75rem 1rem; + background: #222; + border-top: 1px solid #333; +} + +#nav:has(#sign-out-btn) { + justify-content: space-between; +} + +#sign-in-btn { + background: #2563eb; + color: #fff; + border: none; + border-radius: 6px; + padding: 0.5rem 1.25rem; + font-size: 0.875rem; + cursor: pointer; + transition: background 0.15s; +} + +#sign-in-btn:hover { + background: #1d4ed8; +} + +#sign-in-btn:active { + background: #1e40af; +} + +#sign-out-btn { + background: #6C47FF; + color: #fff; + border: none; + border-radius: 6px; + padding: 0.5rem 1.25rem; + font-size: 0.875rem; + cursor: pointer; + transition: background 0.15s; +} + +#sign-out-btn:hover { + background: #5639CC; +} + +#sign-out-btn:active { + background: #4A30B3; +} + +.user-card { + width: 100%; + margin-top: 0.75rem; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 10px; + padding: 0.75rem; + text-align: left; +} + +.user-card-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.user-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; +} + +.user-name { + font-size: 1rem; + font-weight: 600; +} + +.user-info-row { + display: flex; + justify-content: space-between; + padding: 0.3rem 0; + font-size: 0.8rem; + border-top: 1px solid #2a2a2a; +} + +.info-label { + color: #888; +} + +.info-value { + color: #fff; + text-align: right; + word-break: break-all; +} diff --git a/playground/browser-extension-js/build/popup.html b/playground/browser-extension-js/build/popup.html new file mode 100644 index 00000000000..3cc1c64380d --- /dev/null +++ b/playground/browser-extension-js/build/popup.html @@ -0,0 +1,22 @@ + + + + + + + Clerk Extension Demo + + + + +
+
+

Clerk JS Chrome Extension Quickstart

+
+
+ +
+ + + + diff --git a/playground/browser-extension-js/esbuild.config.mjs b/playground/browser-extension-js/esbuild.config.mjs new file mode 100644 index 00000000000..9e54d8bda5b --- /dev/null +++ b/playground/browser-extension-js/esbuild.config.mjs @@ -0,0 +1,31 @@ +import esbuild from 'esbuild'; + +process.loadEnvFile(); + +if (!process.env.CLERK_PUBLISHABLE_KEY) { + throw new Error('Missing CLERK_PUBLISHABLE_KEY in .env'); +} + +const watch = process.argv.includes('--watch'); + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints: ['./src/popup.ts'], + outfile: './build/popup.js', + bundle: true, + format: 'iife', + platform: 'browser', + target: 'es2022', + sourcemap: true, + define: { + 'process.env.CLERK_PUBLISHABLE_KEY': JSON.stringify(process.env.CLERK_PUBLISHABLE_KEY ?? ''), + }, +}; + +if (watch) { + const ctx = await esbuild.context(options); + await ctx.watch(); + console.log('Watching for changes...'); +} else { + await esbuild.build(options); +} diff --git a/playground/browser-extension-js/package.json b/playground/browser-extension-js/package.json new file mode 100644 index 00000000000..45e31f3bb37 --- /dev/null +++ b/playground/browser-extension-js/package.json @@ -0,0 +1,22 @@ +{ + "name": "browser-extension-js", + "private": true, + "version": "0.0.1", + "description": "A Chrome extension demo using @clerk/chrome-extension with plain TypeScript.", + "author": "Clerk", + "type": "module", + "scripts": { + "build": "node esbuild.config.mjs", + "dev": "node esbuild.config.mjs --watch", + "pkglab:add": "pnpm pkglab add @clerk/chrome-extension", + "pkglab:ls": "pnpm pkglab pkg ls" + }, + "dependencies": { + "@clerk/chrome-extension": "0.0.0-pkglab.1772136211844" + }, + "devDependencies": { + "@types/chrome": "^0.1.12", + "esbuild": "^0.25.0", + "typescript": "5.8.3" + } +} diff --git a/playground/browser-extension-js/pnpm-workspace.yaml b/playground/browser-extension-js/pnpm-workspace.yaml new file mode 100644 index 00000000000..b5ac8b5e39d --- /dev/null +++ b/playground/browser-extension-js/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +onlyBuiltDependencies: + - '@clerk/shared' + - browser-tabs-lock + - bufferutil + - core-js + - utf-8-validate diff --git a/playground/browser-extension-js/src/env.d.ts b/playground/browser-extension-js/src/env.d.ts new file mode 100644 index 00000000000..d41502e388e --- /dev/null +++ b/playground/browser-extension-js/src/env.d.ts @@ -0,0 +1 @@ +declare const process: { env: { CLERK_PUBLISHABLE_KEY: string } }; diff --git a/playground/browser-extension-js/src/popup.ts b/playground/browser-extension-js/src/popup.ts new file mode 100644 index 00000000000..b4bced92bbf --- /dev/null +++ b/playground/browser-extension-js/src/popup.ts @@ -0,0 +1,117 @@ +import { createClerkClient } from '@clerk/chrome-extension/client'; + +const publishableKey = process.env.CLERK_PUBLISHABLE_KEY; + +const EXTENSION_URL = chrome.runtime.getURL('.'); +const POPUP_URL = `${EXTENSION_URL}popup.html`; + +const clerk = createClerkClient({ publishableKey }); + +const contentEl = document.getElementById('content') as HTMLDivElement; +const navEl = document.getElementById('nav') as HTMLDivElement; + +function render() { + contentEl.innerHTML = ''; + navEl.innerHTML = ''; + + if (clerk.user) { + const user = clerk.user; + + const card = document.createElement('div'); + card.className = 'user-card'; + + const header = document.createElement('div'); + header.className = 'user-card-header'; + + const avatar = document.createElement('img'); + avatar.className = 'user-avatar'; + avatar.src = user.imageUrl; + avatar.alt = 'Profile'; + + const name = document.createElement('span'); + name.className = 'user-name'; + name.textContent = user.fullName ?? 'Anonymous'; + + header.appendChild(avatar); + header.appendChild(name); + card.appendChild(header); + + const email = user.primaryEmailAddress?.emailAddress; + if (email) { + card.appendChild(createInfoRow('Email', email)); + } + + if (user.username) { + card.appendChild(createInfoRow('Username', user.username)); + } + + if (clerk.session) { + card.appendChild(createInfoRow('Session ID', truncate(clerk.session.id))); + } + + if (user.lastSignInAt) { + card.appendChild(createInfoRow('Last sign-in', user.lastSignInAt.toLocaleDateString())); + } + + if (user.createdAt) { + card.appendChild(createInfoRow('Account created', user.createdAt.toLocaleDateString())); + } + + contentEl.appendChild(card); + + const userBtnEl = document.createElement('div'); + navEl.appendChild(userBtnEl); + clerk.mountUserButton(userBtnEl); + + const signOutBtn = document.createElement('button'); + signOutBtn.textContent = 'Sign Out'; + signOutBtn.id = 'sign-out-btn'; + signOutBtn.addEventListener('click', () => { + clerk.signOut({ redirectUrl: POPUP_URL }); + }); + navEl.appendChild(signOutBtn); + } else { + const signInBtn = document.createElement('button'); + signInBtn.textContent = 'Sign In'; + signInBtn.id = 'sign-in-btn'; + signInBtn.addEventListener('click', () => { + clerk.openSignIn({}); + }); + navEl.appendChild(signInBtn); + } +} + +function truncate(str: string): string { + if (str.length <= 15) return str; + const charsToKeep = str.length - 15; + const front = Math.ceil(charsToKeep / 2); + const back = Math.floor(charsToKeep / 2); + return `${str.slice(0, front)}...${str.slice(str.length - back)}`; +} + +function createInfoRow(label: string, value: string): HTMLDivElement { + const row = document.createElement('div'); + row.className = 'user-info-row'; + const labelSpan = document.createElement('span'); + labelSpan.className = 'info-label'; + labelSpan.textContent = label; + const valueSpan = document.createElement('span'); + valueSpan.className = 'info-value'; + valueSpan.textContent = value; + row.appendChild(labelSpan); + row.appendChild(valueSpan); + return row; +} + +clerk + .load({ + afterSignOutUrl: POPUP_URL, + signInForceRedirectUrl: POPUP_URL, + signUpForceRedirectUrl: POPUP_URL, + allowedRedirectProtocols: ['chrome-extension:'], + }) + .then(() => { + console.log('working') + clerk.addListener(render); + render(); + }); diff --git a/playground/browser-extension-js/tsconfig.json b/playground/browser-extension-js/tsconfig.json new file mode 100644 index 00000000000..b773145b856 --- /dev/null +++ b/playground/browser-extension-js/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "outDir": "build", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} + diff --git a/playground/browser-extension/README.md b/playground/browser-extension/README.md index b546e548fb9..7886935d2a3 100644 --- a/playground/browser-extension/README.md +++ b/playground/browser-extension/README.md @@ -1,6 +1,7 @@ -## Running the Playground +## Running the Playground Standard: + ```bash pnpm dev ``` @@ -17,31 +18,14 @@ Clerk is a developer-first authentication and user management solution. It provi After following the quickstart you'll have learned how to: -- Scaffold a new application using the Plasmo framework -- Install `@clerk/chrome-extension` -- Set your environment variables -- Add `` to your application -- Create a header with Clerk components for users to sign in and out -- Configure a consistent CRX key -- Load your Chrome Extension into your Chromium-based browser -- Test your Chrome Extension - - -## Connect with development package - -Run the development server: -```bash -> cd playground/browser-extension -> n 18.17 -> npm i -> npm run dev -``` - -In a separate terminal, build and publish the development package to your local registry: -```bash -> cd packages/chrome-extension -> pnpm build && pnpm publish:local -``` +* Scaffold a new application using the Plasmo framework +* Install `@clerk/chrome-extension` +* Set your environment variables +* Add `` to your application +* Create a header with Clerk components for users to sign in and out +* Configure a consistent CRX key +* Load your Chrome Extension into your Chromium-based browser +* Test your Chrome Extension ## Running the template @@ -51,9 +35,9 @@ git clone https://github.com/clerkinc/clerk-chrome-extension-quickstart To run the example locally, you need to: -1. Sign up for a Clerk account at [https://clerk.com](https://dashboard.clerk.com/sign-up?utm_source=readme&utm_medium=owned&utm_campaign=chrome-extension&utm_content=10-24-2023&utm_term=clerk-chrome-extension-quickstart). +1. Sign up for a Clerk account at [https://clerk.com](https://dashboard.clerk.com/sign-up?utm_source=readme\&utm_medium=owned\&utm_campaign=chrome-extension\&utm_content=10-24-2023\&utm_term=clerk-chrome-extension-quickstart). -2. Go to the [Clerk dashboard](https://dashboard.clerk.com?utm_source=readme&utm_medium=owned&utm_campaign=chrome-extension&utm_content=10-24-2023&utm_term=clerk-chrome-extension-quickstart) and create an application. +2. Go to the [Clerk dashboard](https://dashboard.clerk.com?utm_source=readme\&utm_medium=owned\&utm_campaign=chrome-extension\&utm_content=10-24-2023\&utm_term=clerk-chrome-extension-quickstart) and create an application. 3. Set the required Clerk environment variables as shown in [the example `.env.development` file](./.env.development.example). @@ -61,18 +45,19 @@ To run the example locally, you need to: 5. Set the public key are shown in [the example `.env.chrome` file](./.env.chrome.example). -5. `pnpm install` the required dependencies. +6. `pnpm install` the required dependencies. -6. `pnpm dev` to launch the development server. +7. `pnpm dev` to launch the development server. ## Learn more To learn more about Clerk and Chrome Extensions, check out the following resources: -- [Quickstart: Get started with Chrome Extensions and Clerk](https://clerk.com/docs/quickstarts/chrome-extension?utm_source=readme&utm_medium=owned&utm_campaign=chrome-extension&utm_content=10-24-2023&utm_term=clerk-chrome-extension-quickstart) +* [Quickstart: Get started with Chrome Extensions and Clerk](https://clerk.com/docs/quickstarts/chrome-extension?utm_source=readme\&utm_medium=owned\&utm_campaign=chrome-extension\&utm_content=10-24-2023\&utm_term=clerk-chrome-extension-quickstart) + +* [Clerk Documentation](https://clerk.com/docs?utm_source=readme\&utm_medium=owned\&utm_campaign=chrome-extension\&utm_content=10-24-2023\&utm_term=clerk-chrome-extension-quickstart) -- [Clerk Documentation](https://clerk.com/docs?utm_source=readme&utm_medium=owned&utm_campaign=chrome-extension&utm_content=10-24-2023&utm_term=clerk-chrome-extension-quickstart) -- [Chrome Extensions](https://developer.chrome.com/docs/extensions) +* [Chrome Extensions](https://developer.chrome.com/docs/extensions) ## Found an issue or want to leave feedback diff --git a/playground/browser-extension/package.json b/playground/browser-extension/package.json index dc21f0c86ad..9400e034dd7 100644 --- a/playground/browser-extension/package.json +++ b/playground/browser-extension/package.json @@ -19,9 +19,9 @@ "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "plasmo": "0.89.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "plasmo": "0.90.5", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.27.0", "tailwind-merge": "^2.5.4", "tailwindcss": "3.4.14", @@ -31,8 +31,8 @@ "@ianvs/prettier-plugin-sort-imports": "4.1.1", "@types/chrome": "0.0.280", "@types/node": "20.16.14", - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", "@types/webextension-polyfill": "^0.12.1", "postcss": "8.4.49", "prettier": "3.3.3",