diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx
index 7365c1a76..a90f10d08 100644
--- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx
@@ -124,23 +124,20 @@ export default function PageClient () {
- {
- (
-
-
-
-
- {/* a transparent cover that prevents the card from being clicked, even when pointer-events is overridden */}
-
-
-
-
-
+
+
+
+
+ {/* a transparent cover that prevents the card from being clicked, even when pointer-events is overridden */}
+
+
+
- )}
+
+
);
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx
index 38e41d315..f97c1803e 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx
@@ -1,13 +1,15 @@
"use client";
import { SettingCard, SettingSwitch } from "@/components/settings";
+import { AdminOAuthProviderConfig, AuthPage, OAuthProviderConfig } from "@stackframe/stack";
import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth";
-import { ActionDialog, Typography } from "@stackframe/stack-ui";
+import { ActionDialog, Badge, BrandIcons, BrowserFrame, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, SimpleTooltip, Typography } from "@stackframe/stack-ui";
+import { AsteriskSquare, CirclePlus, Key, Link2, MoreHorizontal } from "lucide-react";
import { useState } from "react";
import { CardSubtitle } from "../../../../../../../../../packages/stack-ui/dist/components/ui/card";
import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
-import { ProviderSettingSwitch } from "./providers";
+import { ProviderSettingDialog, ProviderSettingSwitch, TurnOffProviderDialog } from "./providers";
function ConfirmSignUpEnabledDialog(props: {
open?: boolean,
@@ -73,57 +75,33 @@ function ConfirmSignUpDisabledDialog(props: {
);
}
-export default function PageClient() {
+function DisabledProvidersDialog({ open, onOpenChange }: { open?: boolean, onOpenChange?: (open: boolean) => void }) {
const stackAdminApp = useAdminApp();
const project = stackAdminApp.useProject();
const oauthProviders = project.config.oauthProviders;
- const [confirmSignUpEnabled, setConfirmSignUpEnabled] = useState(false);
- const [confirmSignUpDisabled, setConfirmSignUpDisabled] = useState(false);
+ const [providerSearch, setProviderSearch] = useState("");
+ const filteredProviders = allProviders
+ .filter((id) => id.toLowerCase().includes(providerSearch.toLowerCase()))
+ .map((id) => [id, oauthProviders.find((provider) => provider.id === id)] as const)
+ .filter(([, provider]) => {
+ return !provider?.enabled;
+ });
- return (
-
-
-
- Email-based
-
- {
- await project.update({
- config: {
- credentialEnabled: checked,
- },
- });
- }}
- />
- {
- await project.update({
- config: {
- magicLinkEnabled: checked,
- },
- });
- }}
- />
- {
- await project.update({
- config: {
- passkeyEnabled: checked,
- },
- });
- }}
- />
-
- SSO (OAuth)
-
- {allProviders.map((id) => {
- const provider = oauthProviders.find((provider) => provider.id === id);
+ return
+ setProviderSearch(e.target.value)}
+ />
+
+ {filteredProviders
+ .map(([id, provider]) => {
return
;
})}
-
+
+ { filteredProviders.length === 0 &&
No providers found. }
+
+
+ ;
+}
+
+function OAuthActionCell({ config }: { config: AdminOAuthProviderConfig }) {
+ const stackAdminApp = useAdminApp();
+ const project = stackAdminApp.useProject();
+ const oauthProviders = project.config.oauthProviders;
+ const [turnOffProviderDialogOpen, setTurnOffProviderDialogOpen] = useState(false);
+ const [providerSettingDialogOpen, setProviderSettingDialogOpen] = useState(false);
+
+
+ const updateProvider = async (provider: AdminOAuthProviderConfig & OAuthProviderConfig) => {
+ const alreadyExist = oauthProviders.some((p) => p.id === config.id);
+ const newOAuthProviders = oauthProviders.map((p) => p.id === config.id ? provider : p);
+ if (!alreadyExist) {
+ newOAuthProviders.push(provider);
+ }
+ await project.update({
+ config: { oauthProviders: newOAuthProviders },
+ });
+ };
+
+ return (
+
+ setTurnOffProviderDialogOpen(false)}
+ providerId={config.id}
+ onConfirm={async () => {
+ await updateProvider({
+ ...config,
+ id: config.id,
+ enabled: false
+ });
+ }}
+ />
+ setProviderSettingDialogOpen(false)}
+ updateProvider={updateProvider}
+ />
+
+
+
+ Open menu
+
+
+
+
+ { setProviderSettingDialogOpen(true); }}>
+ Configure
+
+ { setTurnOffProviderDialogOpen(true); }}
+ >
+ Disable Provider
+
+
+
+ );
+}
+
+const SHARED_TOOLTIP = "Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page.\n\nYou should replace these before you go into production.";
+
+export default function PageClient() {
+ const stackAdminApp = useAdminApp();
+ const project = stackAdminApp.useProject();
+ const oauthProviders = project.config.oauthProviders;
+ const [confirmSignUpEnabled, setConfirmSignUpEnabled] = useState(false);
+ const [confirmSignUpDisabled, setConfirmSignUpDisabled] = useState(false);
+ const [disabledProvidersDialogOpen, setDisabledProvidersDialogOpen] = useState(false);
+
+ const enabledProviders = allProviders
+ .map((id) => [id, oauthProviders.find((provider) => provider.id === id)] as const)
+ .filter(([, provider]) => provider?.enabled);
+
+ return (
+
+
+
+
+
+ Email/password authentication
+
+ }
+ checked={project.config.credentialEnabled}
+ onCheckedChange={async (checked) => {
+ await project.update({
+ config: {
+ credentialEnabled: checked,
+ },
+ });
+ }}
+ />
+
+
+ Magic link (Email OTP)
+
+ }
+ checked={project.config.magicLinkEnabled}
+ onCheckedChange={async (checked) => {
+ await project.update({
+ config: {
+ magicLinkEnabled: checked,
+ },
+ });
+ }}
+ />
+
+
+ Passkey
+
+ }
+ checked={project.config.passkeyEnabled}
+ onCheckedChange={async (checked) => {
+ await project.update({
+ config: {
+ passkeyEnabled: checked,
+ },
+ });
+ }}
+ />
+
+ SSO Providers
+
+
+ { enabledProviders.map(([, provider]) => provider)
+ .filter((provider): provider is AdminOAuthProviderConfig => !!provider).map(provider => {
+ return
+
+
+
+
+
{BrandIcons.toTitle(provider.id)}
+ {provider.type === 'shared' &&
+ Shared keys
+ }
+
+
+
+
;
+ }) }
+
+ {
+ setDisabledProvidersDialogOpen(true);
+ }}
+ variant="secondary"
+ >
+
+ Add SSO providers
+
+ {
+ setDisabledProvidersDialogOpen(x);
+ }}
+ />
+
+
+
+
+
+
+
+ {/* a transparent cover that prevents the card from being clicked, even when pointer-events is overridden */}
+
+
provider)
+ .filter((provider): provider is AdminOAuthProviderConfig => !!provider),
+ },
+ }}
+ />
+
+
+
+
+
+
+
void,
- onConfirm: () => void,
+ onConfirm: () => Promise,
providerId: string,
}) {
return (
@@ -172,7 +171,7 @@ export function TurnOffProviderDialog(props: {
okButton={{
label: `Disable ${toTitle(props.providerId)}`,
onClick: async () => {
- props.onConfirm();
+ await props.onConfirm();
},
}}
cancelButton
@@ -202,35 +201,33 @@ export function ProviderSettingSwitch(props: Props) {
return (
<>
-
- {toTitle(props.id)}
- {isShared && enabled &&
-
- Shared keys
-
- }
-
+ {
+ if (enabled) {
+ setTurnOffProviderDialogOpen(true);
+ } else {
+ setProviderSettingDialogOpen(true);
}
- checked={enabled}
- onCheckedChange={async (checked) => {
- if (!checked) {
- setTurnOffProviderDialogOpen(true);
- return;
- } else {
- setProviderSettingDialogOpen(true);
- }
- }}
- actions={ setProviderSettingDialogOpen(true)} />}
- onlyShowActionsWhenChecked
- />
+ }}
+ >
+
+ {toTitle(props.id)}
+ {isShared && enabled &&
+
+ Shared keys
+
+ }
+
setTurnOffProviderDialogOpen(false)}
providerId={props.id}
- onConfirm={() => runAsynchronously(updateProvider(false))}
+ onConfirm={async () => {
+ await updateProvider(false);
+ }}
/>
setProviderSettingDialogOpen(false)} />
diff --git a/apps/dashboard/src/components/data-table/user-table.tsx b/apps/dashboard/src/components/data-table/user-table.tsx
index 58b0fbd59..6be2bccb9 100644
--- a/apps/dashboard/src/components/data-table/user-table.tsx
+++ b/apps/dashboard/src/components/data-table/user-table.tsx
@@ -173,7 +173,7 @@ export const getCommonUserColumns = () => [
},
] satisfies ColumnDef[];
-const columns: ColumnDef[] = [
+const columns: ColumnDef[] = [
...getCommonUserColumns(),
{
accessorKey: "authTypes",
diff --git a/apps/dashboard/src/components/settings.tsx b/apps/dashboard/src/components/settings.tsx
index 55c88942a..85235bf8d 100644
--- a/apps/dashboard/src/components/settings.tsx
+++ b/apps/dashboard/src/components/settings.tsx
@@ -14,9 +14,10 @@ export function SettingCard(props: {
actions?: React.ReactNode,
children?: React.ReactNode,
accordion?: string,
+ className?: string,
}) {
return (
-
+
{(props.title || props.description) && (
{props.title && {props.title} }
diff --git a/packages/stack-ui/src/components/brand-icons.tsx b/packages/stack-ui/src/components/brand-icons.tsx
new file mode 100644
index 000000000..46a1ba686
--- /dev/null
+++ b/packages/stack-ui/src/components/brand-icons.tsx
@@ -0,0 +1,217 @@
+import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
+
+export function Google({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function Facebook({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+ );
+}
+
+export function GitHub({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+ );
+}
+
+export function Microsoft({ iconSize } : { iconSize: number} ) {
+ return (
+
+ {"MS-SymbolLockup"}
+
+
+
+
+
+ );
+}
+
+export function Spotify({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+ );
+}
+
+export function Discord({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+ );
+}
+
+export function Gitlab({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function Bitbucket({ iconSize }: { iconSize: number }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function LinkedIn({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function Apple({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+
+ );
+}
+
+export function X({ iconSize } : { iconSize: number} ) {
+ return (
+
+
+
+ );
+}
+
+export function Mapping({
+ provider,
+ iconSize,
+}: {
+ provider: string,
+ iconSize: number,
+}) {
+ switch (provider) {
+ case "google": {
+ return ;
+ }
+ case "github": {
+ return ;
+ }
+ case "facebook": {
+ return ;
+ }
+ case "microsoft": {
+ return ;
+ }
+ case "spotify": {
+ return ;
+ }
+ case "discord": {
+ return ;
+ }
+ case "gitlab": {
+ return ;
+ }
+ case "bitbucket": {
+ return ;
+ }
+ case "linkedin": {
+ return ;
+ }
+ case "apple": {
+ return ;
+ }
+ case "x": {
+ return ;
+ }
+ default: {
+ throw new StackAssertionError(`Icon not found for provider: ${provider}`);;
+ }
+ }
+}
+
+export function toTitle(id: string) {
+ return {
+ github: "GitHub",
+ google: "Google",
+ facebook: "Facebook",
+ microsoft: "Microsoft",
+ spotify: "Spotify",
+ discord: "Discord",
+ gitlab: "GitLab",
+ apple: "Apple",
+ bitbucket: "Bitbucket",
+ linkedin: "LinkedIn",
+ x: "X",
+ }[id] || throwErr(`Unknown provider: ${id}`);
+}
+
+export const BRAND_COLORS: Record = {
+ github: '#24292e',
+ google: '#ffffff',
+ facebook: '#0866FF',
+ microsoft: '#2F2F2F',
+ spotify: '#1DD65F',
+ discord: '#5661F5',
+ linkedin: '#0A66C2',
+ x: '#000000',
+};
diff --git a/packages/stack-ui/src/index.ts b/packages/stack-ui/src/index.ts
index 107b22c41..356f20588 100644
--- a/packages/stack-ui/src/index.ts
+++ b/packages/stack-ui/src/index.ts
@@ -1,5 +1,6 @@
export * from "./components/action-dialog";
+export * as BrandIcons from "./components/brand-icons";
export * from "./components/browser-frame";
export * from "./components/copy-button";
export * from "./components/copy-field";
diff --git a/packages/template/src/components/oauth-button.tsx b/packages/template/src/components/oauth-button.tsx
index c416d9100..204d26e01 100644
--- a/packages/template/src/components/oauth-button.tsx
+++ b/packages/template/src/components/oauth-button.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Button } from '@stackframe/stack-ui';
+import { BrandIcons, Button } from '@stackframe/stack-ui';
import Color from 'color';
import { useId } from 'react';
import { useStackApp } from '..';
@@ -8,146 +8,6 @@ import { useTranslation } from '../lib/translations';
const iconSize = 22;
-function GoogleIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
-
-
-
-
- );
-}
-
-function FacebookIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
- );
-}
-
-function GitHubIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
- );
-}
-
-function MicrosoftIcon({ iconSize } : { iconSize: number} ) {
- return (
-
- {"MS-SymbolLockup"}
-
-
-
-
-
- );
-}
-
-function SpotifyIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
- );
-}
-function DiscordIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
- );
-}
-function GitlabIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function BitbucketIcon({ iconSize }: { iconSize: number }) {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function LinkedInIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
-
-
-
-
- );
-}
-
-function AppleIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
-
- );
-}
-
-function XIcon({ iconSize } : { iconSize: number} ) {
- return (
-
-
-
- );
-}
-
const changeColor = (c: Color, value: number) => {
if (c.isLight()) {
value = -value;
@@ -180,7 +40,7 @@ export function OAuthButton({
textColor: '#000',
name: 'Google',
border: '1px solid #ddd',
- icon: ,
+ icon: ,
};
break;
}
@@ -190,7 +50,7 @@ export function OAuthButton({
textColor: '#fff',
border: '1px solid #333',
name: 'GitHub',
- icon: ,
+ icon: ,
};
break;
}
@@ -199,7 +59,7 @@ export function OAuthButton({
backgroundColor: '#1877F2',
textColor: '#fff',
name: 'Facebook',
- icon: ,
+ icon: ,
};
break;
}
@@ -208,7 +68,7 @@ export function OAuthButton({
backgroundColor: '#2f2f2f',
textColor: '#fff',
name: 'Microsoft',
- icon: ,
+ icon: ,
};
break;
}
@@ -217,7 +77,7 @@ export function OAuthButton({
backgroundColor: '#1DB954',
textColor: '#fff',
name: 'Spotify',
- icon: ,
+ icon: ,
};
break;
}
@@ -226,7 +86,7 @@ export function OAuthButton({
backgroundColor: '#5865F2',
textColor: '#fff',
name: 'Discord',
- icon: ,
+ icon: ,
};
break;
}
@@ -236,7 +96,7 @@ export function OAuthButton({
textColor: "#fff",
border: "1px solid #333",
name: "Gitlab",
- icon: ,
+ icon: ,
};
break;
}
@@ -246,7 +106,7 @@ export function OAuthButton({
textColor: "#fff",
border: "1px solid #333",
name: "Apple",
- icon: ,
+ icon: ,
};
break;
}
@@ -256,7 +116,7 @@ export function OAuthButton({
textColor: "#000",
border: "1px solid #ddd",
name: "Bitbucket",
- icon: ,
+ icon: ,
};
break;
}
@@ -265,7 +125,7 @@ export function OAuthButton({
backgroundColor: "#0073b1",
textColor: "#fff",
name: "LinkedIn",
- icon: ,
+ icon: ,
};
break;
}
@@ -274,7 +134,7 @@ export function OAuthButton({
backgroundColor: "#000",
textColor: "#fff",
name: "X",
- icon: ,
+ icon: ,
};
break;
}