Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading
, '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
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/common-teams-do.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/astro': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `clerk({ treatPendingAsSignedOut: false })`
5 changes: 5 additions & 0 deletions .changeset/early-boats-help.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `app.use(clerkPlugin, { treatPendingAsSignedOut: false })`
7 changes: 7 additions & 0 deletions .changeset/modern-paws-mate.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

Update `useAuth` to handle pending sessions as signed-out by default, with opt-out via `useAuth({ treatPendingAsSignedOut: false })` or `<ClerkProvider treatPendingAsSignedOut={false} />`

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll spin up different changeset per package since <ClerkProvider treatPendingAsSignedOut={false} /> is specific to React

1 change: 1 addition & 0 deletions packages/astro/src/env.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ interface InternalEnv {
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
readonly PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT?: string;
}

interface ImportMeta {
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/integration/create-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,15 @@ type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & {

function createIntegration<Params extends HotloadAstroClerkIntegrationParams>() {
return (params?: Params): AstroIntegration => {
const { proxyUrl, isSatellite, domain, signInUrl, signUpUrl, enableEnvSchema = true } = params || {};
const {
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
enableEnvSchema = true,
treatPendingAsSignedOut,
} = params || {};

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
Expand DownExpand Up@@ -57,6 +65,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
/**
* Convert the integration params to environment variable in order for it to be readable from the server
*/
...buildEnvVarFromOption(treatPendingAsSignedOut, 'PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT'),
...buildEnvVarFromOption(signInUrl, 'PUBLIC_CLERK_SIGN_IN_URL'),
...buildEnvVarFromOption(signUpUrl, 'PUBLIC_CLERK_SIGN_UP_URL'),
...buildEnvVarFromOption(isSatellite, 'PUBLIC_CLERK_IS_SATELLITE'),
Expand Down
147 changes: 25 additions & 122 deletions packages/astro/src/react/hooks.ts
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
import { resolveAuthState } from '@clerk/shared/authorization';
import type {
ActClaim,
CheckAuthorizationWithCustomPermissions,
Clerk,
GetToken,
OrganizationCustomRoleKey,
PendingSessionOptions,
SignOut,
UseAuthReturn,
} from '@clerk/types';
import type { Store, StoreValue } from 'nanostores';
import { useCallback, useSyncExternalStore } from 'react';

import { authAsyncStorage } from '#async-local-storage';

import { $authStore } from '../stores/external';
import { $authStore, $clerkStore } from '../stores/external';
import { $clerk, $csrState } from '../stores/internal';

type CheckAuthorizationSignedOut = undefined;
type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => false;

/**
* @internal
*/
Expand DownExpand Up@@ -54,61 +52,7 @@ const createSignOut = () => {
};
};

type UseAuthReturn =
| {
isLoaded: false;
isSignedIn: undefined;
userId: undefined;
sessionId: undefined;
actor: undefined;
orgId: undefined;
orgRole: undefined;
orgSlug: undefined;
has: CheckAuthorizationSignedOut;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: false;
userId: null;
sessionId: null;
actor: null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: null;
orgRole: null;
orgSlug: null;
has: CheckAuthorizationWithoutOrgOrUser;
signOut: SignOut;
getToken: GetToken;
}
| {
isLoaded: true;
isSignedIn: true;
userId: string;
sessionId: string;
actor: ActClaim | null;
orgId: string;
orgRole: OrganizationCustomRoleKey;
orgSlug: string | null;
has: CheckAuthorizationWithCustomPermissions;
signOut: SignOut;
getToken: GetToken;
};

type UseAuth = () => UseAuthReturn;
type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn;

/**
* Returns the current auth state, the user and session ids and the `getToken`
Expand DownExpand Up@@ -138,12 +82,15 @@ type UseAuth = () => UseAuthReturn;
* return <div>...</div>
* }
*/
export const useAuth: UseAuth = () => {
const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useStore($authStore);
export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => {
const authContext = useStore($authStore);
const clerkContext = useStore($clerkStore);

const getToken: GetToken = useCallback(createGetToken(), []);
const signOut: SignOut = useCallback(createSignOut(), []);

const { userId, orgId, orgRole, orgPermissions } = authContext;

const has = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (!params?.permission && !params?.role) {
Expand All@@ -169,71 +116,27 @@ export const useAuth: UseAuth = () => {
[orgId, orgRole, userId, orgPermissions],
);

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
isSignedIn: undefined,
sessionId,
userId,
actor: undefined,
orgId: undefined,
orgRole: undefined,
orgSlug: undefined,
has: undefined,
signOut,
const payload = resolveAuthState({
authObject: {
...authContext,
getToken,
};
}

if (sessionId === null && userId === null) {
return {
isLoaded: true,
isSignedIn: false,
sessionId,
userId,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
}

if (!!sessionId && !!userId && !!orgId && !!orgRole) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
signOut,
getToken,
};
}
},
options: {
treatPendingAsSignedOut:
// Fallback from option provided via SSR / CSR contexts
treatPendingAsSignedOut ??
clerkContext?.__internal_getOption?.('treatPendingAsSignedOut') ??
import.meta.env.PUBLIC_CLERK_TREAT_PENDING_AS_SIGNED_OUT,
},
});

if (!!sessionId && !!userId && !orgId) {
return {
isLoaded: true,
isSignedIn: true,
sessionId,
userId,
actor: actor || null,
orgId: null,
orgRole: null,
orgSlug: null,
has: () => false,
signOut,
getToken,
};
if (!payload) {
throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
}

throw new Error('Invalid state. Feel free to submit a bug or reach out to support');
return payload;
};

/**
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ export function PromisifiedAuthProvider({
* }
* ```
*/
export function usePromisifiedAuth() {
export function usePromisifiedAuth(options: Parameters<typeof useAuth>[0] = {}) {
const isPagesRouter = useRouter();
const valueFromContext = React.useContext(PromisifiedAuthContext);

Expand All@@ -64,16 +64,15 @@ export function usePromisifiedAuth() {
}

// At this point we should have a usable auth object

if (typeof window === 'undefined') {
// Pages router should always use useAuth as it is able to grab initial auth state from context during SSR.
if (isPagesRouter) {
return useAuth();
return useAuth(options);
}

// We don't need to deal with Clerk being loaded here
return useDerivedAuth(resolvedData);
return useDerivedAuth({ ...resolvedData, ...options });
} else {
return useAuth(resolvedData);
return useAuth({ ...resolvedData, ...options });
}
}
88 changes: 88 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,94 @@ describe('useDerivedAuth', () => {
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('returns signed out state when session has pending status by default', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: true` option, returns signed out state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: true }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(false);
expect(current.sessionId).toBeNull();
expect(current.userId).toBeNull();
expect(current.actor).toBeNull();
expect(current.orgId).toBeNull();
expect(current.orgRole).toBeNull();
expect(current.orgSlug).toBeNull();
expect(current.has).toBeInstanceOf(Function);
expect(current.has?.({ permission: 'test' })).toBe(false);
});

it('with `treatPendingAsSignedOut: false` option, returns signed in state when session has pending status', () => {
const authObject = {
sessionId: 'session123',
sessionStatus: 'pending',
userId: 'user123',
actor: 'actor123',
orgId: 'org123',
orgRole: 'admin',
orgSlug: 'my-org',
signOut: vi.fn(),
getToken: vi.fn(),
};

const {
result: { current },
} = renderHook(() => useDerivedAuth(authObject, { treatPendingAsSignedOut: false }));

expect(current.isLoaded).toBe(true);
expect(current.isSignedIn).toBe(true);
expect(current.sessionId).toBe('session123');
expect(current.userId).toBe('user123');
expect(current.actor).toBe('actor123');
expect(current.orgId).toBe('org123');
expect(current.orgRole).toBe('admin');
expect(current.orgSlug).toBe('my-org');
expect(typeof current.has).toBe('function');
expect(current.signOut).toBe(authObject.signOut);
expect(current.getToken).toBe(authObject.getToken);
});

it('returns signed in with org context when sessionId, userId, orgId, and orgRole are present', () => {
const authObject = {
sessionId: 'session123',
Expand Down
Loading