@stackframe/stack 2.3.3 → 2.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Stack
2
+
3
+ Stack is an open-source, self-hostable, and highly customizable authentication and user management system.
4
+
5
+ We provide frontend and backend libraries for Next.js, React, and JavaScript. You can set it up in one minute and scale with the project as it grows.
6
+
7
+ ## Features
8
+
9
+ - Composable React components & hooks
10
+ - OAuth (Google, Facebook, GitHub, etc.)
11
+ - Email and password authentication (with email verification and password reset)
12
+ - Easy to set up with proxied providers (no need to sign up and create OAuth endpoints yourself on all the providers)
13
+ - User management & analytics
14
+ - User-associated metadata with client-/server-specific permissions
15
+ - Out-of-the-box Dark/Light mode support
16
+ - suports switching out the underlying UI library, support MUI Joy out of the box
17
+ - **100% open-source!**
18
+
19
+ Currently, only Next.js is supported, but we are working on adding other frameworks.
20
+
21
+ ## Installation
22
+
23
+ To get started with Stack, you need to [create a Next.js project](https://nextjs.org/docs/getting-started/installation) using the App router. Then, you can install Stack by running the following command:
24
+
25
+ ```bash
26
+ npm install @stackframe/stack
27
+ ```
28
+
29
+ For setup, refer to [our documentation](https://docs.stack-auth.com).
@@ -1,3 +1,5 @@
1
+ import { User } from "../lib/stack-app";
1
2
  export default function UserAvatar(props: {
2
3
  size?: number;
3
- }): import("react/jsx-runtime").JSX.Element | null;
4
+ user: User | null;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -1,9 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Avatar, AvatarFallback, AvatarImage, Text, useUser } from "..";
2
+ import { Avatar, AvatarFallback, AvatarImage, Text } from "..";
3
+ const anonUserDataUrl = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='-100 -1060 1160 1160' width='24'%3E%3Crect x='-100' y='-1060' width='100%25' height='100%25' fill='%23d8d8d8' /%3E%3Cpath d='M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z'/%3E%3C/svg%3E";
3
4
  export default function UserAvatar(props) {
4
- const user = useUser();
5
- if (!user) {
6
- return null;
7
- }
8
- return (_jsxs(Avatar, { style: { height: props.size, width: props.size }, children: [_jsx(AvatarImage, { src: user.profileImageUrl || '' }), _jsx(AvatarFallback, { children: _jsx(Text, { style: { fontWeight: 500 }, children: (user.displayName || user.primaryEmail)?.slice(0, 2).toUpperCase() }) })] }));
5
+ const user = props.user;
6
+ return (_jsxs(Avatar, { style: { height: props.size, width: props.size }, children: [_jsx(AvatarImage, { src: user ? user.profileImageUrl || '' : anonUserDataUrl }), _jsx(AvatarFallback, { children: _jsx(Text, { style: { fontWeight: 500 }, children: (user?.displayName || user?.primaryEmail)?.slice(0, 2).toUpperCase() ?? "?" }) })] }));
9
7
  }
@@ -1,4 +1,6 @@
1
- export default function UserButton({ showUserInfo, showColorMode, }: {
1
+ type UserButtonProps = {
2
2
  showUserInfo?: boolean;
3
3
  showColorMode?: boolean;
4
- }): import("react/jsx-runtime").JSX.Element | null;
4
+ };
5
+ export default function UserButton({ showUserInfo, showColorMode, }: UserButtonProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useUser, Text, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuLabel, DropdownMenuSeparator, useStackApp, useDesign } from "..";
4
- import { RxPerson, RxEnter, RxSun, RxShadow } from "react-icons/rx";
3
+ import { Suspense } from "react";
4
+ import { useUser, Text, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuLabel, DropdownMenuSeparator, useStackApp, useDesign, Skeleton } from "..";
5
+ import { RxPerson, RxEnter, RxSun, RxShadow, RxFilePlus } from "react-icons/rx";
5
6
  import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
6
7
  import { SECONDARY_FONT_COLORS } from "../utils/constants";
7
8
  import UserAvatar from "./user-avatar";
@@ -9,9 +10,16 @@ import { useRouter } from "next/navigation";
9
10
  function Item(props) {
10
11
  return (_jsxs(DropdownMenuItem, { onClick: () => runAsynchronously(props.onClick), style: { display: 'flex', gap: '0.5rem', alignItems: 'center' }, children: [props.icon, _jsx(Text, { children: props.text })] }));
11
12
  }
12
- export default function UserButton({ showUserInfo = false, showColorMode = true, }) {
13
- const { colorMode, setColorMode } = useDesign();
13
+ export default function UserButton({ showUserInfo = false, showColorMode = false, }) {
14
+ return (_jsx(Suspense, { fallback: _jsx(Skeleton, { children: _jsx(UserButtonInnerInner, { showUserInfo: showUserInfo, showColorMode: showColorMode, user: null }) }), children: _jsx(UserButtonInner, { showUserInfo: showUserInfo, showColorMode: showColorMode }) }));
15
+ }
16
+ function UserButtonInner(props) {
14
17
  const user = useUser();
18
+ return _jsx(UserButtonInnerInner, { showUserInfo: props.showUserInfo, showColorMode: props.showColorMode, user: user });
19
+ }
20
+ function UserButtonInnerInner(props) {
21
+ const { colorMode, setColorMode } = useDesign();
22
+ const user = props.user;
15
23
  const app = useStackApp();
16
24
  const router = useRouter();
17
25
  const textStyles = {
@@ -19,9 +27,7 @@ export default function UserButton({ showUserInfo = false, showColorMode = true,
19
27
  whiteSpace: 'nowrap',
20
28
  overflow: 'hidden'
21
29
  };
22
- if (!user)
23
- return null;
24
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { children: _jsxs("div", { style: { display: 'flex', gap: '0.5rem', alignItems: 'center' }, children: [_jsx(UserAvatar, {}), showUserInfo && _jsxs("div", { style: { display: 'flex', flexDirection: 'column', justifyContent: 'center' }, children: [_jsx(Text, { style: textStyles, children: user.displayName }), _jsx(Text, { style: { ...textStyles, fontWeight: 400 }, variant: "secondary", size: "sm", children: user.primaryEmail })] })] }) }), _jsxs(DropdownMenuContent, { style: { zIndex: 1500 }, children: [_jsx(DropdownMenuLabel, { children: _jsxs("div", { style: { display: 'flex', gap: '0.5rem', alignItems: 'center' }, children: [_jsx(UserAvatar, {}), _jsxs("div", { children: [_jsx(Text, { children: user.displayName }), _jsx(Text, { variant: "secondary", size: "sm", style: { fontWeight: 400 }, children: user.primaryEmail })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(Item, { text: "Account settings", onClick: () => runAsynchronously(router.push(app.urls.accountSettings)), icon: _jsx(RxPerson, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) }), showColorMode && _jsx(Item, { text: colorMode === 'dark' ? 'Light theme' : 'Dark theme', onClick: () => setColorMode(colorMode === 'dark' ? 'light' : 'dark'), icon: colorMode === 'dark' ?
30
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { children: _jsxs("div", { style: { display: 'flex', gap: '0.5rem', alignItems: 'center' }, children: [_jsx(UserAvatar, { user: user }), user && props.showUserInfo && _jsxs("div", { style: { display: 'flex', flexDirection: 'column', justifyContent: 'center' }, children: [_jsx(Text, { style: textStyles, children: user.displayName }), _jsx(Text, { style: { ...textStyles, fontWeight: 400 }, variant: "secondary", size: "sm", children: user.primaryEmail })] })] }) }), _jsxs(DropdownMenuContent, { style: { zIndex: 1500 }, children: [_jsx(DropdownMenuLabel, { children: _jsxs("div", { style: { display: 'flex', gap: '0.5rem', alignItems: 'center' }, children: [_jsx(UserAvatar, { user: user }), _jsxs("div", { children: [user && _jsx(Text, { children: user.displayName }), user && _jsx(Text, { variant: "secondary", size: "sm", style: { fontWeight: 400 }, children: user.primaryEmail }), !user && _jsx(Text, { variant: "secondary", children: "Not logged in" })] })] }) }), _jsx(DropdownMenuSeparator, {}), user && _jsx(Item, { text: "Account settings", onClick: () => runAsynchronously(router.push(app.urls.accountSettings)), icon: _jsx(RxPerson, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) }), !user && _jsx(Item, { text: "Sign in", onClick: () => runAsynchronously(router.push(app.urls.signIn)), icon: _jsx(RxEnter, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) }), !user && _jsx(Item, { text: "Sign up", onClick: () => runAsynchronously(router.push(app.urls.signUp)), icon: _jsx(RxFilePlus, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) }), props.showColorMode && _jsx(Item, { text: colorMode === 'dark' ? 'Light theme' : 'Dark theme', onClick: () => setColorMode(colorMode === 'dark' ? 'light' : 'dark'), icon: colorMode === 'dark' ?
25
31
  _jsx(RxSun, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) :
26
- _jsx(RxShadow, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) }), _jsx(Item, { text: "Sign out", onClick: () => user.signOut(), icon: _jsx(RxEnter, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) })] })] }));
32
+ _jsx(RxShadow, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) }), user && _jsx(Item, { text: "Sign out", onClick: () => user.signOut(), icon: _jsx(RxEnter, { size: 22, color: SECONDARY_FONT_COLORS[colorMode] }) })] })] }));
27
33
  }
@@ -43,3 +43,6 @@ export declare const Tabs: React.ForwardRefExoticComponent<Omit<import("@radix-u
43
43
  export declare const TabsList: React.ForwardRefExoticComponent<Omit<Omit<import("@radix-ui/react-tabs").TabsListProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
44
44
  export declare const TabsContent: React.ForwardRefExoticComponent<Omit<Omit<import("@radix-ui/react-tabs").TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
45
45
  export declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<Omit<import("@radix-ui/react-tabs").TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
46
+ export declare const Skeleton: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
47
+ deactivated?: boolean | undefined;
48
+ } & React.RefAttributes<HTMLSpanElement>, "ref"> & React.RefAttributes<HTMLSpanElement>>;
@@ -46,3 +46,4 @@ export const Tabs = createDynamicComponent('Tabs');
46
46
  export const TabsList = createDynamicComponent('TabsList');
47
47
  export const TabsContent = createDynamicComponent('TabsContent');
48
48
  export const TabsTrigger = createDynamicComponent('TabsTrigger');
49
+ export const Skeleton = createDynamicComponent('Skeleton');
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ declare const Skeleton: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
3
+ deactivated?: boolean | undefined;
4
+ } & React.RefAttributes<HTMLSpanElement>>;
5
+ export { Skeleton };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import styled, { keyframes } from 'styled-components';
5
+ import { SECONDARY_FONT_COLORS } from "../utils/constants";
6
+ import { useDesign } from "../providers/design-provider";
7
+ const animation = keyframes `
8
+ 0% {
9
+ filter: grayscale(1) contrast(0) brightness(0) invert(1) brightness(0.8);
10
+ }
11
+ 100% {
12
+ filter: grayscale(1) contrast(0) brightness(0) invert(1) brightness(0.7);
13
+ }
14
+ `;
15
+ const Primitive = styled("span") `
16
+ &[data-stack-state="activated"] {
17
+ animation: ${animation} 1s infinite alternate-reverse !important;
18
+ }
19
+ &[data-stack-state="activated"], &[data-stack-state="activated"] * {
20
+ pointer-events: none !important;
21
+ -webkit-user-select: none !important;
22
+ -moz-user-select: none !important;
23
+ user-select: none !important;
24
+ cursor: default !important;
25
+ }
26
+ `;
27
+ const Skeleton = React.forwardRef((props, ref) => {
28
+ const { colorMode } = useDesign();
29
+ return _jsx(Primitive, { "$color": colorMode === 'dark' ? SECONDARY_FONT_COLORS.dark : SECONDARY_FONT_COLORS.light, ref: ref, "data-stack-state": props.deactivated ? "deactivated" : "activated", ...props });
30
+ });
31
+ Skeleton.displayName = "Skeleton";
32
+ export { Skeleton };
@@ -17,7 +17,7 @@ function ProfileSection() {
17
17
  return (_jsxs(SettingSection, { title: 'Profile', desc: 'Your profile information', buttonDisabled: !changed, buttonText: 'Save', onButtonClick: async () => {
18
18
  await user?.update(userInfo);
19
19
  setChanged(false);
20
- }, children: [_jsxs("div", { style: { display: 'flex', gap: '1rem', alignItems: 'center' }, children: [_jsx(UserAvatar, { size: 60 }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Text, { size: 'lg', children: user?.displayName }), _jsx(Text, { variant: 'secondary', size: 'sm', children: user?.primaryEmail })] })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Label, { htmlFor: 'display-name', children: "Display Name" }), _jsx(Input, { id: 'display-name', value: userInfo.displayName, onChange: (e) => {
20
+ }, children: [_jsxs("div", { style: { display: 'flex', gap: '1rem', alignItems: 'center' }, children: [_jsx(UserAvatar, { user: user, size: 60 }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Text, { size: 'lg', children: user?.displayName }), _jsx(Text, { variant: 'secondary', size: 'sm', children: user?.primaryEmail })] })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Label, { htmlFor: 'display-name', children: "Display Name" }), _jsx(Input, { id: 'display-name', value: userInfo.displayName, onChange: (e) => {
21
21
  setUserInfo((i) => ({ ...i, displayName: e.target.value }));
22
22
  setChanged(true);
23
23
  } })] })] }));
@@ -37,8 +37,6 @@ export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, Proje
37
37
  }));
38
38
  export type StackClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & {
39
39
  uniqueIdentifier: string;
40
- currentClientUserJson: UserJson | null;
41
- currentProjectJson: ClientProjectJson;
42
40
  };
43
41
  export declare const stackAppInternalsSymbol: unique symbol;
44
42
  type Auth<T, C> = {
@@ -179,7 +177,8 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
179
177
  verifyEmail(code: string): Promise<KnownErrors["EmailVerificationError"] | undefined>;
180
178
  signInWithMagicLink(code: string): Promise<KnownErrors["MagicLinkError"] | undefined>;
181
179
  [stackAppInternalsSymbol]: {
182
- toClientJson(): Promise<StackClientAppJson<HasTokenStore, ProjectId>>;
180
+ toClientJson(): StackClientAppJson<HasTokenStore, ProjectId>;
181
+ setCurrentUser(userJsonPromise: Promise<UserJson | null>): void;
183
182
  };
184
183
  } & AsyncStoreProperty<"project", ClientProjectJson, false> & {
185
184
  [K in `redirectTo${Capitalize<keyof Omit<HandlerUrls, 'handler' | 'oauthCallback'>>}`]: () => Promise<never>;
@@ -9,10 +9,10 @@ import { AsyncStore } from "@stackframe/stack-shared/dist/utils/stores";
9
9
  import { getProductionModeErrors } from "@stackframe/stack-shared/dist/interface/clientInterface";
10
10
  import { isClient } from "../utils/next";
11
11
  import { callOAuthCallback, signInWithOAuth } from "./auth";
12
- import { RedirectType, redirect, useRouter } from "next/navigation";
12
+ import * as NextNavigation from "next/navigation"; // import the entire module to get around some static compiler warnings emitted by Next.js in some cases
13
13
  import { constructRedirectUrl } from "../utils/url";
14
14
  import { filterUndefined, omit } from "@stackframe/stack-shared/dist/utils/objects";
15
- import { neverResolve, resolved, runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
15
+ import { neverResolve, resolved, runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises";
16
16
  import { AsyncCache } from "@stackframe/stack-shared/dist/utils/caches";
17
17
  import { suspend } from "@stackframe/stack-shared/dist/utils/react";
18
18
  function getUrls(partial) {
@@ -36,10 +36,10 @@ function getUrls(partial) {
36
36
  };
37
37
  }
38
38
  function getDefaultProjectId() {
39
- return process.env.NEXT_PUBLIC_STACK_PROJECT_ID || throwErr("No project ID provided. Please copy your project ID from the Stack dashboard and put it in the NEXT_PUBLIC_STACK_PROJECT_ID environment variable.");
39
+ return process.env.NEXT_PUBLIC_STACK_PROJECT_ID || throwErr("Welcome to Stack! It seems that you haven't provided a project ID. Please create a project on the Stack dashboard at https://app.stack-auth.com and put it in the NEXT_PUBLIC_STACK_PROJECT_ID environment variable.");
40
40
  }
41
41
  function getDefaultPublishableClientKey() {
42
- return process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY || throwErr("No publishable client key provided. Please copy your publishable client key from the Stack dashboard and put it in the NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY environment variable.");
42
+ return process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY || throwErr("Welcome to Stack! It seems that you haven't provided a publishable client key. Please create an API key for your project on the Stack dashboard at https://app.stack-auth.com and copy your publishable client key into the NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY environment variable.");
43
43
  }
44
44
  function getDefaultSecretServerKey() {
45
45
  return process.env.STACK_SECRET_SERVER_KEY || throwErr("No secret server key provided. Please copy your publishable client key from the Stack dashboard and put your it in the STACK_SECRET_SERVER_KEY environment variable.");
@@ -172,7 +172,11 @@ class _StackClientAppImpl {
172
172
  _interface;
173
173
  _tokenStoreOptions;
174
174
  _urlOptions;
175
+ __DEMO_ENABLE_SLIGHT_FETCH_DELAY = false;
175
176
  _currentUserCache = createCacheByTokenStore(async (tokenStore) => {
177
+ if (this.__DEMO_ENABLE_SLIGHT_FETCH_DELAY) {
178
+ await wait(2000);
179
+ }
176
180
  const user = await this._interface.getClientUserByToken(tokenStore);
177
181
  return Result.or(user, null);
178
182
  });
@@ -200,19 +204,6 @@ class _StackClientAppImpl {
200
204
  throw new StackAssertionError("A Stack client app with the same unique identifier already exists");
201
205
  }
202
206
  allClientApps.set(this._uniqueIdentifier, [options.checkString ?? "default check string", this]);
203
- // For some important calls, either use the provided cached values or start fetching them now
204
- if (options.currentClientUserJson !== undefined) {
205
- this._currentUserCache.forceSetCachedValue([getTokenStore(this._tokenStoreOptions)], options.currentClientUserJson);
206
- }
207
- else if (this.hasPersistentTokenStore()) {
208
- runAsynchronously(this.getUser(), { ignoreErrors: true });
209
- }
210
- if (options.currentProjectJson !== undefined) {
211
- this._currentProjectCache.forceSetCachedValue([], options.currentProjectJson);
212
- }
213
- else {
214
- runAsynchronously(this.getProject(), { ignoreErrors: true });
215
- }
216
207
  }
217
208
  hasPersistentTokenStore() {
218
209
  return this._tokenStoreOptions !== null;
@@ -401,7 +392,7 @@ class _StackClientAppImpl {
401
392
  if (userJson === null) {
402
393
  switch (options?.or) {
403
394
  case 'redirect': {
404
- redirect(this.urls.signIn, RedirectType.replace);
395
+ NextNavigation.redirect(this.urls.signIn, NextNavigation.RedirectType.replace);
405
396
  throw new Error("redirect should never return!");
406
397
  }
407
398
  case 'throw': {
@@ -416,7 +407,7 @@ class _StackClientAppImpl {
416
407
  }
417
408
  useUser(options) {
418
409
  this._ensurePersistentTokenStore();
419
- const router = useRouter();
410
+ const router = NextNavigation.useRouter();
420
411
  const tokenStore = getTokenStore(this._tokenStoreOptions);
421
412
  const userJson = useCache(this._currentUserCache, [tokenStore], "useUser()");
422
413
  if (userJson === null) {
@@ -569,7 +560,7 @@ class _StackClientAppImpl {
569
560
  static get [stackAppInternalsSymbol]() {
570
561
  return {
571
562
  fromClientJson: (json) => {
572
- const providedCheckString = JSON.stringify(omit(json, ["currentClientUserJson", "currentProjectJson"]));
563
+ const providedCheckString = JSON.stringify(omit(json, [ /* none currently */]));
573
564
  const existing = allClientApps.get(json.uniqueIdentifier);
574
565
  if (existing) {
575
566
  const [existingCheckString, clientApp] = existing;
@@ -587,15 +578,11 @@ class _StackClientAppImpl {
587
578
  }
588
579
  get [stackAppInternalsSymbol]() {
589
580
  return {
590
- toClientJson: async () => {
581
+ toClientJson: () => {
591
582
  if (!("publishableClientKey" in this._interface.options)) {
592
583
  // TODO find a way to do this
593
584
  throw Error("Cannot serialize to JSON from an application without a publishable client key");
594
585
  }
595
- const [user, project] = await Promise.all([
596
- this.getUser(),
597
- this.getProject(),
598
- ]);
599
586
  return {
600
587
  baseUrl: this._interface.options.baseUrl,
601
588
  projectId: this.projectId,
@@ -603,10 +590,11 @@ class _StackClientAppImpl {
603
590
  tokenStore: this._tokenStoreOptions,
604
591
  urls: this._urlOptions,
605
592
  uniqueIdentifier: this._uniqueIdentifier,
606
- currentClientUserJson: user?.toJson() ?? null,
607
- currentProjectJson: project,
608
593
  };
609
- }
594
+ },
595
+ setCurrentUser: (userJsonPromise) => {
596
+ runAsynchronously(this._currentUserCache.forceSetCachedValueAsync([getTokenStore(this._tokenStoreOptions)], userJsonPromise));
597
+ },
610
598
  };
611
599
  }
612
600
  ;
@@ -40,6 +40,9 @@ export declare const Components: {
40
40
  readonly TabsList: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-tabs").TabsListProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
41
41
  readonly TabsContent: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-tabs").TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
42
42
  readonly TabsTrigger: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-tabs").TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
43
+ readonly Skeleton: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
44
+ deactivated?: boolean | undefined;
45
+ } & React.RefAttributes<HTMLSpanElement>>;
43
46
  };
44
47
  export type ComponentConfig = {
45
48
  components?: Partial<typeof Components>;
@@ -83,6 +86,9 @@ export declare function useComponents(): {
83
86
  readonly TabsList: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-tabs").TabsListProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
84
87
  readonly TabsContent: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-tabs").TabsContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
85
88
  readonly TabsTrigger: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-tabs").TabsTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
89
+ readonly Skeleton: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
90
+ deactivated?: boolean | undefined;
91
+ } & React.RefAttributes<HTMLSpanElement>>;
86
92
  };
87
93
  export declare function StackComponentProvider(props: {
88
94
  children?: React.ReactNode;
@@ -14,6 +14,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
14
14
  import { Avatar, AvatarFallback, AvatarImage } from '../components-core/avatar';
15
15
  import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../components-core/collapsible';
16
16
  import { Tabs, TabsList, TabsContent, TabsTrigger } from '../components-core/tabs';
17
+ import { Skeleton } from '../components-core/skeleton';
17
18
  export const Components = {
18
19
  Input,
19
20
  Button,
@@ -45,6 +46,7 @@ export const Components = {
45
46
  TabsList,
46
47
  TabsContent,
47
48
  TabsTrigger,
49
+ Skeleton,
48
50
  };
49
51
  const ComponentContext = createContext(undefined);
50
52
  export function useComponents() {
@@ -34,6 +34,6 @@ export declare function hasCustomColorMode(config: DesignConfig): config is Desi
34
34
  };
35
35
  export declare function StackDesignProvider(props: {
36
36
  children?: React.ReactNode;
37
- } & DesignConfig): import("react/jsx-runtime").JSX.Element | null;
37
+ } & DesignConfig): import("react/jsx-runtime").JSX.Element;
38
38
  export declare function useDesign(): DesignContextValue;
39
39
  export {};
@@ -37,7 +37,6 @@ const useColorMode = (props) => {
37
37
  }
38
38
  };
39
39
  export function StackDesignProvider(props) {
40
- const [mounted, setMounted] = useState(false);
41
40
  const [colorMode, setColorMode] = useColorMode(props);
42
41
  const [designValue, setDesignValue] = useState({
43
42
  colors: getColors(colorMode, props.colors),
@@ -52,11 +51,6 @@ export function StackDesignProvider(props) {
52
51
  colorMode,
53
52
  }));
54
53
  }, [colorMode]);
55
- useEffect(() => {
56
- setMounted(true);
57
- }, []);
58
- if (!mounted)
59
- return null;
60
54
  return (_jsx(DesignContext.Provider, { value: designValue, children: props.children }));
61
55
  }
62
56
  export function useDesign() {
@@ -1,9 +1,13 @@
1
1
  import { StackClientApp, StackClientAppJson } from "../lib/stack-app";
2
2
  import React from "react";
3
+ import { UserJson } from "@stackframe/stack-shared";
3
4
  export declare const StackContext: React.Context<{
4
5
  app: StackClientApp<true>;
5
6
  } | null>;
6
7
  export declare function StackProviderClient(props: {
7
- appJsonPromise: Promise<StackClientAppJson<true, string>>;
8
+ appJson: StackClientAppJson<true, string>;
8
9
  children?: React.ReactNode;
9
10
  }): import("react/jsx-runtime").JSX.Element;
11
+ export declare function UserSetter(props: {
12
+ userJsonPromise: Promise<UserJson | null>;
13
+ }): null;
@@ -1,14 +1,23 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { use } from "react";
3
+ import { useEffect } from "react";
4
4
  import { StackClientApp, stackAppInternalsSymbol } from "../lib/stack-app";
5
5
  import React from "react";
6
+ import { useStackApp } from "..";
6
7
  export const StackContext = React.createContext(null);
7
8
  export function StackProviderClient(props) {
8
- const appJson = use(props.appJsonPromise);
9
+ const appJson = props.appJson;
9
10
  const app = StackClientApp[stackAppInternalsSymbol].fromClientJson(appJson);
10
11
  if (process.env.NODE_ENV === "development") {
11
12
  globalThis.stackApp = app;
12
13
  }
13
14
  return (_jsx(StackContext.Provider, { value: { app }, children: props.children }));
14
15
  }
16
+ export function UserSetter(props) {
17
+ const app = useStackApp();
18
+ useEffect(() => {
19
+ const promise = (async () => await props.userJsonPromise)(); // there is a Next.js bug where Promises passed by server components return `undefined` as their `then` value, so wrap it in a normal promise
20
+ app[stackAppInternalsSymbol].setCurrentUser(promise);
21
+ }, []);
22
+ return null;
23
+ }
@@ -1,6 +1,11 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Suspense } from 'react';
2
3
  import { stackAppInternalsSymbol } from '../lib/stack-app';
3
- import { StackProviderClient } from './stack-provider-client';
4
+ import { StackProviderClient, UserSetter } from './stack-provider-client';
4
5
  export default function StackProvider({ children, app, }) {
5
- return (_jsx(StackProviderClient, { appJsonPromise: app[stackAppInternalsSymbol].toClientJson(), children: children }));
6
+ return (_jsxs(StackProviderClient, { appJson: app[stackAppInternalsSymbol].toClientJson(), children: [_jsx(Suspense, { fallback: null, children: _jsx(UserFetcher, { app: app }) }), children] }));
7
+ }
8
+ function UserFetcher(props) {
9
+ const userPromise = props.app.getUser().then((user) => user?.toJson() ?? null);
10
+ return _jsx(UserSetter, { userJsonPromise: userPromise });
6
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -12,6 +12,7 @@
12
12
  "default": "./dist/joy.js"
13
13
  }
14
14
  },
15
+ "homepage": "https://stack-auth.com",
15
16
  "files": [
16
17
  "README.md",
17
18
  "dist"
@@ -34,11 +35,11 @@
34
35
  "tailwindcss-scoped-preflight": "^2.1.0",
35
36
  "yup": "^1.4.0",
36
37
  "@stackframe/stack-sc": "1.4.1",
37
- "@stackframe/stack-shared": "2.3.2"
38
+ "@stackframe/stack-shared": "2.3.3"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "@mui/joy": "^5.0.0-beta.30",
41
- "next": "^14.1",
42
+ "next": ">=14.1",
42
43
  "react": "^18.2"
43
44
  },
44
45
  "peerDependenciesMeta": {