diff --git a/docs/data/toolpad/core/all-components/all-components.md b/docs/data/toolpad/core/all-components/all-components.md index 7d4652725bb..c32844330c6 100644 --- a/docs/data/toolpad/core/all-components/all-components.md +++ b/docs/data/toolpad/core/all-components/all-components.md @@ -8,3 +8,4 @@ - [Dashboard Layout](/toolpad/core/react-dashboard-layout/) - [Page Container](/toolpad/core/react-page-container/) - [Sign-in Page](/toolpad/core/react-sign-in-page/) +- [Sign-up Page](/toolpad/core/react-sign-up-page/) diff --git a/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.js index 536b3b76da4..4651293269b 100644 --- a/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Credentials' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.tsx index f3d758e7263..028ec4fadee 100644 --- a/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/BrandingSignInPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Credentials' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.js index 01d5cfe3684..b5c072efcaf 100644 --- a/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; // preview-start diff --git a/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.tsx index 07e0fb1b9c4..ba36183446f 100644 --- a/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/CredentialsSignInPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; // preview-start diff --git a/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.js index 7e5269208e4..2f44fd91e64 100644 --- a/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { hiIN } from '@toolpad/core/locales'; const providers = [ diff --git a/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.tsx index 28cb13b4ef8..8450ec95d5d 100644 --- a/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/LocaleSignInPage.tsx @@ -4,7 +4,7 @@ import { SignInPage, type AuthProvider, type AuthResponse, -} from '@toolpad/core/SignInPage'; +} from '@toolpad/core/AuthPage'; import { hiIN } from '@toolpad/core/locales'; const providers = [ diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js index 73800c4d07b..28866594029 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { AppProvider } from '@toolpad/core/AppProvider'; import { useTheme } from '@mui/material/styles'; diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx index f7e674890a2..8f11ee5a94a 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx @@ -4,7 +4,7 @@ import { SignInPage, SupportedAuthProvider, AuthResponse, -} from '@toolpad/core/SignInPage'; +} from '@toolpad/core/AuthPage'; import { AppProvider } from '@toolpad/core/AppProvider'; import { useTheme } from '@mui/material/styles'; diff --git a/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.js b/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.js index 575370713a5..de983fc559b 100644 --- a/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.js +++ b/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Email and password' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.tsx b/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.tsx index 373d8f60c03..0fe83f99d43 100644 --- a/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.tsx @@ -4,7 +4,7 @@ import { SignInPage, type AuthProvider, type AuthResponse, -} from '@toolpad/core/SignInPage'; +} from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Email and password' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js index 508b1b47cdf..93208f4c351 100644 --- a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; // preview-start diff --git a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx index 5b7bee73947..ec65e9ab28d 100644 --- a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx @@ -1,10 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { - AuthResponse, - SignInPage, - type AuthProvider, -} from '@toolpad/core/SignInPage'; +import { AuthResponse, SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; // preview-start diff --git a/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.js b/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.js index ec39c35ddda..5b0aa402e88 100644 --- a/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; // preview-start const providers = [{ id: 'passkey', name: 'Passkey' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.tsx index 352bdc93677..88624c5563b 100644 --- a/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/PasskeySignInPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, AuthProvider } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; // preview-start const providers = [{ id: 'passkey', name: 'Passkey' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.js b/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.js index 7272ae9cf59..322991d687a 100644 --- a/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.js +++ b/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.js @@ -2,7 +2,7 @@ import * as React from 'react'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [ diff --git a/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.tsx b/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.tsx index 7272ae9cf59..322991d687a 100644 --- a/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/SlotPropsSignIn.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [ diff --git a/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.js b/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.js index e9b32bcb3c5..cd841e45633 100644 --- a/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.js +++ b/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.js @@ -16,7 +16,7 @@ import AccountCircle from '@mui/icons-material/AccountCircle'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Email and Password' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.tsx b/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.tsx index b2f8f3e7e50..925e6134455 100644 --- a/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/SlotsSignIn.tsx @@ -16,7 +16,7 @@ import AccountCircle from '@mui/icons-material/AccountCircle'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Email and Password' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.js index ad29307d2bd..1bb7c2ab001 100644 --- a/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { createTheme } from '@mui/material/styles'; import { useColorSchemeShim } from 'docs/src/modules/components/ThemeContext'; import { getDesignTokens, inputsCustomizations } from './customTheme'; diff --git a/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.tsx index 19bff6a4b4f..73fd301eaff 100644 --- a/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.tsx @@ -4,7 +4,7 @@ import { SignInPage, type AuthProvider, type AuthResponse, -} from '@toolpad/core/SignInPage'; +} from '@toolpad/core/AuthPage'; import { createTheme } from '@mui/material/styles'; import { useColorSchemeShim } from 'docs/src/modules/components/ThemeContext'; import { getDesignTokens, inputsCustomizations } from './customTheme'; diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index b315ac2ad87..1365d81dd78 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -18,7 +18,7 @@ The `SignInPage` component is a quick way to generate a ready-to-use authenticat ```tsx import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; export default function App() { return ( @@ -36,6 +36,12 @@ export default function App() { ## OAuth +:::warning + +This function works similarly to the sign-up flowγ€€to authenticate users, but is focused on signing in existing accounts rather than creating new ones. For details on the sign-up flow, see the [Sign-up Page documentation](/toolpad/core/components/sign-up-page/). + +::: + The `SignInPage` component can be set up with an OAuth provider by passing in a list of providers in the `providers` prop, along with a `signIn` function that accepts the `provider` as a parameter. {{"demo": "OAuthSignInPage.js", "iframe": true, "height": 600}} @@ -177,7 +183,7 @@ To have a fully built "Sign in with GitHub" page appear at the `/auth/signin` ro ```tsx title="./app/auth/signin/page.tsx" // ... import * as React from 'react'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { AuthError } from 'next-auth'; import { providerMap, signIn } from '../../../auth'; @@ -294,6 +300,12 @@ Beyond the [global localization options](/toolpad/core/introduction/base-concept The `SignInPage` component has versions with different layouts for authentication - one column, two column and others such. The APIs of these components are identical. This is in progress. +### 🚧 SignUp + +A dedicated `SignUpPage` component is planned to provide a seamless registration experience for new users. This will support multiple authentication providers and allow for custom fields, validation, and branding, similar to the `SignInPage`. Stay tuned for updates and check the [Sign-up Page documentation](/toolpad/core/components/sign-up-page/) for the latest information. + ## 🚧 Other authentication flows -Besides the `SignInPage` , the team is planning work on several other components that enable new workflows such as [sign up](https://github.com/mui/toolpad/issues/4068) and [password reset](https://github.com/mui/toolpad/issues/4265). +Additional authentication flows such as password reset, multi-factor authentication (MFA), and account recovery are on the roadmap. These features aim to enhance security and user experience. Follow the [Toolpad GitHub issues](https://github.com/mui/toolpad/issues) for progress and upcoming releases. + +Besides the `SignInPage` , the team is planning work on several other components that enable new workflows such as [password reset](https://github.com/mui/toolpad/issues/4265). diff --git a/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.js b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.js new file mode 100644 index 00000000000..ac2e8763dda --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.js @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/AuthPage'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [{ id: 'credentials', name: 'Email and Password' }]; +// preview-end + +const signIn = async (provider, formData) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + alert( + `Signing in with "${provider.name}" and credentials: ${formData.get('email')}, ${formData.get('password')}`, + ); + resolve(); + }, 300); + }); + return promise; +}; + +export default function CredentialsSignUpPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx new file mode 100644 index 00000000000..4d90fb071a1 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage, type AuthProvider } from '@toolpad/core/AuthPage'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [{ id: 'credentials', name: 'Email and Password' }]; +// preview-end + +const signIn: (provider: AuthProvider, formData: FormData) => void = async ( + provider, + formData, +) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + alert( + `Signing in with "${provider.name}" and credentials: ${formData.get('email')}, ${formData.get('password')}`, + ); + resolve(); + }, 300); + }); + return promise; +}; + +export default function CredentialsSignUpPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx.preview b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx.preview new file mode 100644 index 00000000000..2e78c08f2d0 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx.preview @@ -0,0 +1,11 @@ +const providers = [{ id: 'credentials', name: 'Email and Password' }]; + +// ... + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.js b/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.js new file mode 100644 index 00000000000..34f8d536801 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.js @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/AuthPage'; +import { useTheme } from '@mui/material/styles'; + +const providers = [{ id: 'credentials', name: 'Email and password' }]; + +const signUp = async (provider, formData) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + const email = formData?.get('email'); + const password = formData?.get('password'); + alert( + `Signing up with "${provider.name}" and credentials: ${email}, ${password}`, + ); + // preview-start + resolve({ + error: 'Invalid credentials.', + }); + // preview-end + }, 300); + }); + return promise; +}; + +export default function NotificationsSignUpPageError() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.tsx b/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.tsx new file mode 100644 index 00000000000..1b355513223 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { + SignUpPage, + type AuthProvider, + type SignUpActionResponse, +} from '@toolpad/core/AuthPage'; +import { useTheme } from '@mui/material/styles'; + +const providers = [{ id: 'credentials', name: 'Email and password' }]; + +const signUp: ( + provider: AuthProvider, + formData?: FormData, +) => Promise | void = async (provider, formData) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + const email = formData?.get('email'); + const password = formData?.get('password'); + alert( + `Signing up with "${provider.name}" and credentials: ${email}, ${password}`, + ); + // preview-start + resolve({ + error: 'Invalid credentials.', + }); + // preview-end + }, 300); + }); + return promise; +}; + +export default function NotificationsSignUpPageError() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.tsx.preview b/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.tsx.preview new file mode 100644 index 00000000000..3f42987e696 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/NotificationsSignUpPageError.tsx.preview @@ -0,0 +1,13 @@ +resolve({ + error: 'Invalid credentials.', +}); + +// ... + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js new file mode 100644 index 00000000000..eccecc97253 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/AuthPage'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [ + { id: 'github', name: 'GitHub' }, + { id: 'google', name: 'Google' }, + { id: 'facebook', name: 'Facebook' }, + { id: 'twitter', name: 'Twitter' }, + { id: 'linkedin', name: 'LinkedIn' }, +]; + +// preview-end + +const signUp = async (provider) => { + // preview-start + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve({ error: 'This is a fake error' }); + }, 500); + }); + // preview-end + return promise; +}; + +export default function OAuthSignUpPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx new file mode 100644 index 00000000000..939d2be356f --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { + AuthProvider, + SignUpActionResponse, + SignUpPage, +} from '@toolpad/core/AuthPage'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [ + { id: 'github', name: 'GitHub' }, + { id: 'google', name: 'Google' }, + { id: 'facebook', name: 'Facebook' }, + { id: 'twitter', name: 'Twitter' }, + { id: 'linkedin', name: 'LinkedIn' }, +]; + +// preview-end + +const signUp = async (provider: AuthProvider): Promise => { + // preview-start + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve({ error: 'This is a fake error' } as SignUpActionResponse); + }, 500); + }) as Promise; + // preview-end + return promise; +}; + +export default function OAuthSignUpPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx.preview b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx.preview new file mode 100644 index 00000000000..50c1ebf86b0 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx.preview @@ -0,0 +1,22 @@ +const providers = [ + { id: 'github', name: 'GitHub' }, + { id: 'google', name: 'Google' }, + { id: 'facebook', name: 'Facebook' }, + { id: 'twitter', name: 'Twitter' }, + { id: 'linkedin', name: 'LinkedIn' }, +]; + +// ... + +const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve({ error: 'This is a fake error' } as SignUpActionResponse); + }, 500); +}) as Promise; + +// ... + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-up-page/sign-up-page.md b/docs/data/toolpad/core/components/sign-up-page/sign-up-page.md index ae978f3779a..207d36c3794 100644 --- a/docs/data/toolpad/core/components/sign-up-page/sign-up-page.md +++ b/docs/data/toolpad/core/components/sign-up-page/sign-up-page.md @@ -1,15 +1,104 @@ --- productId: toolpad-core title: Sign-up Page +components: SignUpPage --- # Sign-up Page 🚧 -

A customizable sign-up component that abstracts away the pain needed to wire together a secure sign-up/register page for your application..

+

A customizable sign-up component that abstracts away the pain needed to wire together a secure sign-up/register page for your application.

+ +:::info +If this is your first time using Toolpad Core, it's recommended to read about the [basic concepts](/toolpad/core/introduction/base-concepts/) first. +::: :::warning -The Sign-up component isn't available yet, but you can upvote [**this GitHub issue**](https://github.com/mui/toolpad/issues/4068) to see it arrive sooner. +This feature is currently experimental and may change in future releases. +::: + +The `SignUpPage` component is a quick way to generate a ready-to-use registration page with multiple OAuth providers, or a credentials from. + +## Basic Usage + +```tsx +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/AuthPage'; + +export default function App() { + return ( + + { + // Your sign in logic + }} + /> + + ); +} +``` + +## OAuth + +:::warning + +This function uses the same logic as the sign-in flow, but instead of authenticating an existing user, it creates a new user account with the selected provider. Make sure your `signUp` function handles user creation and any required validation or error handling for a smooth registration experience. -Don't hesitate to leave a comment there to influence what gets built. -Especially if you already have a use case for this component, or if you're facing a pain point with your current solution. ::: + +The `SignUpPage` component can be set up with an OAuth provider by passing in a list of providers in the `providers` prop, along with a `signUp` function that accepts the `provider` as a parameter. + +{{"demo": "OAuthSignUpPage.js", "iframe": true, "height": 600}} + +:::info + +The following OAuth providers are supported and maintained by default: + +- Google +- GitHub +- Facebook +- Microsoft (Entra ID) +- Apple +- Auth0 +- AWS Cognito +- GitLab +- Instagram +- LINE +- Okta +- FusionAuth +- Twitter +- TikTok +- LinkedIn +- Slack +- Spotify +- Twitch +- Discord +- Keycloak + +Find details on how to set up each provider in the [Auth.js documentation](https://authjs.dev/getting-started/authentication/oauth). +::: + +## Credentials + +:::warning +The Credentials provider is not the most secure way to authenticate users. It's recommended to use any of the other providers for a more robust solution. +::: + +To use the Credentials provider, add it to the `providers` array and implement your own sign-up logic. + +{{"demo": "CredentialsSignUpPage.js", "iframe": true, "height": 600}} + +### Alerts + +The `signUp` prop takes a function which can either return `void` or a `Promise` which resolves with an `SignUpActionResponse` object of the form: + +```ts +interface SignUpActionResponse { + error?: string; + success?: string; +} +``` + +{{"demo": "NotificationsSignUpPageError.js", "iframe": true, "height": 600}} + +This renders an alert with the `error` string as the message. diff --git a/docs/data/toolpad/core/integrations/nextjs-approuter.md b/docs/data/toolpad/core/integrations/nextjs-approuter.md index 993f6964071..063aa582ded 100644 --- a/docs/data/toolpad/core/integrations/nextjs-approuter.md +++ b/docs/data/toolpad/core/integrations/nextjs-approuter.md @@ -181,7 +181,7 @@ Use the `SignInPage` component to add a sign-in page to your app. For example, ` ```tsx title="app/auth/signin/page.tsx" import * as React from 'react'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { AuthError } from 'next-auth'; import { providerMap, signIn } from '../../../auth'; diff --git a/docs/data/toolpad/core/integrations/nextjs-pagesrouter.md b/docs/data/toolpad/core/integrations/nextjs-pagesrouter.md index 6eac0cbe05d..ff72ddab7a8 100644 --- a/docs/data/toolpad/core/integrations/nextjs-pagesrouter.md +++ b/docs/data/toolpad/core/integrations/nextjs-pagesrouter.md @@ -337,7 +337,7 @@ Use the `SignInPage` component to add a sign-in page to your app. For example, ` import * as React from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import Link from '@mui/material/Link'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { signIn } from 'next-auth/react'; import { useRouter } from 'next/router'; import { auth, providerMap } from '../../auth'; diff --git a/docs/data/toolpad/core/integrations/react-router.md b/docs/data/toolpad/core/integrations/react-router.md index fa7218fdb51..ece06062c55 100644 --- a/docs/data/toolpad/core/integrations/react-router.md +++ b/docs/data/toolpad/core/integrations/react-router.md @@ -461,7 +461,7 @@ You can protect any page or groups of pages through this mechanism. ```tsx title="src/pages/signIn.tsx" 'use client'; import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import LinearProgress from '@mui/material/LinearProgress'; import { Navigate, useNavigate } from 'react-router'; import { useSession, type Session } from '../SessionContext'; diff --git a/docs/data/toolpad/core/introduction/base-concepts.md b/docs/data/toolpad/core/introduction/base-concepts.md index 1330fb1fd23..3bdc1103757 100644 --- a/docs/data/toolpad/core/introduction/base-concepts.md +++ b/docs/data/toolpad/core/introduction/base-concepts.md @@ -81,7 +81,7 @@ Toolpad Core uses slots for component customization. Slots allow you to override Here's an example using the `SignInPage` component: ```tsx -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; function MyComponent() { return (
  • Sign-in Page
  • ", "cssComponent": false diff --git a/docs/pages/toolpad/core/api/sign-up-page.js b/docs/pages/toolpad/core/api/sign-up-page.js new file mode 100644 index 00000000000..13d07e2b975 --- /dev/null +++ b/docs/pages/toolpad/core/api/sign-up-page.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './sign-up-page.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs-toolpad/translations/api-docs/sign-up-page', + false, + /\.\/sign-up-page.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/toolpad/core/api/sign-up-page.json b/docs/pages/toolpad/core/api/sign-up-page.json new file mode 100644 index 00000000000..e6c624b349e --- /dev/null +++ b/docs/pages/toolpad/core/api/sign-up-page.json @@ -0,0 +1,100 @@ +{ + "props": { + "localeText": { "type": { "name": "object" } }, + "providers": { + "type": { "name": "arrayOf", "description": "Array<{ id: string, name: string }>" }, + "default": "[]" + }, + "signUp": { + "type": { "name": "func" }, + "default": "undefined", + "signature": { + "type": "function(provider: AuthProvider, formData: FormData, callbackUrl: string) => void | Promise", + "describedArgs": ["provider", "formData", "callbackUrl"] + } + }, + "slotProps": { + "type": { + "name": "shape", + "description": "{ confirmPasswordField?: object, emailField?: object, form?: object, oAuthButton?: object, passwordField?: object, privacyLink?: object, submitButton?: object, termsLink?: object }" + }, + "default": "{}" + }, + "slots": { + "type": { + "name": "shape", + "description": "{ confirmPasswordField?: elementType, emailField?: elementType, passwordField?: elementType, privacyLink?: string, submitButton?: elementType, subtitle?: elementType, termsLink?: string, title?: elementType }" + }, + "default": "{}", + "additionalInfo": { "slotsApi": true } + }, + "sx": { + "type": { + "name": "union", + "description": "Array<func
    | object
    | bool>
    | func
    | object" + }, + "additionalInfo": { "sx": true } + } + }, + "name": "SignUpPage", + "imports": [ + "import { SignUpPage } from '@toolpad/core/SignUpPage';", + "import { SignUpPage } from '@toolpad/core';" + ], + "slots": [ + { + "name": "emailField", + "description": "The custom email field component used in the credentials form.", + "default": "TextField", + "class": null + }, + { + "name": "passwordField", + "description": "The custom password field component used in the credentials form.", + "default": "TextField", + "class": null + }, + { + "name": "confirmPasswordField", + "description": "The custom confirm password field component used in the credentials form.", + "default": "TextField", + "class": null + }, + { + "name": "submitButton", + "description": "The custom submit button component used in the credentials form.", + "default": "Button", + "class": null + }, + { + "name": "termsLink", + "description": "The custom terms and conditions link component.", + "default": "Link", + "class": null + }, + { + "name": "privacyLink", + "description": "The custom privacy policy link component.", + "default": "Link", + "class": null + }, + { + "name": "title", + "description": "A component to override the default title section", + "default": "Typography", + "class": null + }, + { + "name": "subtitle", + "description": "A component to override the default subtitle section", + "default": "Typography", + "class": null + } + ], + "classes": [], + "muiName": "SignUpPage", + "filename": "/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/src/components/landing/ToolpadAuthDemo.tsx b/docs/src/components/landing/ToolpadAuthDemo.tsx index 69d4c9e78a6..0dec3e764bd 100644 --- a/docs/src/components/landing/ToolpadAuthDemo.tsx +++ b/docs/src/components/landing/ToolpadAuthDemo.tsx @@ -3,7 +3,7 @@ import Paper from '@mui/material/Paper'; import { HighlightedCode } from '@mui/docs/HighlightedCode'; import DemoSandbox from 'docs/src/modules/components/DemoSandbox'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import Frame from '../../modules/components/Frame'; const NOOP = () => {}; diff --git a/docs/translations/api-docs/sign-up-page/sign-up-page.json b/docs/translations/api-docs/sign-up-page/sign-up-page.json new file mode 100644 index 00000000000..909bdf315a6 --- /dev/null +++ b/docs/translations/api-docs/sign-up-page/sign-up-page.json @@ -0,0 +1,31 @@ +{ + "componentDescription": "", + "propDescriptions": { + "localeText": { "description": "The labels for the account component." }, + "providers": { "description": "The list of authentication providers to display." }, + "signUp": { + "description": "Callback fired when a user signs up.", + "typeDescriptions": { + "provider": "The authentication provider.", + "formData": "The form data if the provider id is 'credentials'.\\", + "callbackUrl": "The URL to redirect to after signing up." + } + }, + "slotProps": { "description": "The props used for each slot inside." }, + "slots": { "description": "The components used for each slot inside." }, + "sx": { + "description": "The prop used to customize the styles on the SignUpPage container" + } + }, + "classDescriptions": {}, + "slotDescriptions": { + "confirmPasswordField": "The custom confirm password field component used in the credentials form.", + "emailField": "The custom email field component used in the credentials form.", + "passwordField": "The custom password field component used in the credentials form.", + "privacyLink": "The custom privacy policy link component.", + "submitButton": "The custom submit button component used in the credentials form.", + "subtitle": "A component to override the default subtitle section", + "termsLink": "The custom terms and conditions link component.", + "title": "A component to override the default title section" + } +} diff --git a/examples/core/auth-nextjs-email/src/app/auth/signin/page.tsx b/examples/core/auth-nextjs-email/src/app/auth/signin/page.tsx index e1838f9807e..3af539ace20 100644 --- a/examples/core/auth-nextjs-email/src/app/auth/signin/page.tsx +++ b/examples/core/auth-nextjs-email/src/app/auth/signin/page.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { providerMap } from '../../../auth'; import signIn from './actions'; diff --git a/examples/core/auth-nextjs-pages-nextauth-4/src/pages/auth/signin.tsx b/examples/core/auth-nextjs-pages-nextauth-4/src/pages/auth/signin.tsx index be1d6b32d5a..23bdeca3165 100644 --- a/examples/core/auth-nextjs-pages-nextauth-4/src/pages/auth/signin.tsx +++ b/examples/core/auth-nextjs-pages-nextauth-4/src/pages/auth/signin.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import Link from '@mui/material/Link'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { getProviders, signIn } from 'next-auth/react'; import { getServerSession } from 'next-auth/next'; import { useRouter } from 'next/router'; diff --git a/examples/core/auth-nextjs-pages/src/pages/auth/signin.tsx b/examples/core/auth-nextjs-pages/src/pages/auth/signin.tsx index b31e5a63ec2..49eb5e93eac 100644 --- a/examples/core/auth-nextjs-pages/src/pages/auth/signin.tsx +++ b/examples/core/auth-nextjs-pages/src/pages/auth/signin.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { signIn } from 'next-auth/react'; import { useRouter } from 'next/router'; import { auth, providerMap } from '../../auth'; diff --git a/examples/core/auth-nextjs-passkey/src/app/auth/signin/page.tsx b/examples/core/auth-nextjs-passkey/src/app/auth/signin/page.tsx index 6ae609504a5..f8f4880bef2 100644 --- a/examples/core/auth-nextjs-passkey/src/app/auth/signin/page.tsx +++ b/examples/core/auth-nextjs-passkey/src/app/auth/signin/page.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import type { AuthProvider } from '@toolpad/core'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { signIn as webauthnSignIn } from 'next-auth/webauthn'; import { providerMap } from '../../../auth'; import serverSignIn from './actions'; diff --git a/examples/core/auth-nextjs-themed/app/auth/signin/page.tsx b/examples/core/auth-nextjs-themed/app/auth/signin/page.tsx index 64936cdf273..1b089c37d09 100644 --- a/examples/core/auth-nextjs-themed/app/auth/signin/page.tsx +++ b/examples/core/auth-nextjs-themed/app/auth/signin/page.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Link from '@mui/material/Link'; import Alert from '@mui/material/Alert'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { providerMap } from '../../../auth'; import signIn from './actions'; diff --git a/examples/core/auth-nextjs/src/app/auth/signin/page.tsx b/examples/core/auth-nextjs/src/app/auth/signin/page.tsx index ae6c70bf28e..9cf6355f2cb 100644 --- a/examples/core/auth-nextjs/src/app/auth/signin/page.tsx +++ b/examples/core/auth-nextjs/src/app/auth/signin/page.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage, type AuthProvider } from '@toolpad/core/AuthPage'; import { AuthError } from 'next-auth'; import { providerMap, signIn } from '../../../auth'; diff --git a/examples/core/auth-vite/src/pages/signIn.tsx b/examples/core/auth-vite/src/pages/signIn.tsx index 17008f42546..2932313b84e 100644 --- a/examples/core/auth-vite/src/pages/signIn.tsx +++ b/examples/core/auth-vite/src/pages/signIn.tsx @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import type { Session } from '@toolpad/core/AppProvider'; import { useNavigate } from 'react-router'; import { useSession } from '../SessionContext'; diff --git a/examples/core/firebase-vite/src/pages/signin.tsx b/examples/core/firebase-vite/src/pages/signin.tsx index 32e8bdc8e34..8a231d85a19 100644 --- a/examples/core/firebase-vite/src/pages/signin.tsx +++ b/examples/core/firebase-vite/src/pages/signin.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Alert from '@mui/material/Alert'; import LinearProgress from '@mui/material/LinearProgress'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { Navigate, useNavigate } from 'react-router'; import { useSession, type Session } from '../SessionContext'; import { signInWithGoogle, signInWithGithub, signInWithCredentials } from '../firebase/auth'; diff --git a/packages/create-toolpad-app/src/index.ts b/packages/create-toolpad-app/src/index.ts index e77a9b9e412..346f36f10c7 100644 --- a/packages/create-toolpad-app/src/index.ts +++ b/packages/create-toolpad-app/src/index.ts @@ -6,7 +6,7 @@ import { input, confirm, select, checkbox } from '@inquirer/prompts'; import chalk from 'chalk'; import { satisfies } from 'semver'; import invariant from 'invariant'; -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/AuthPage'; import { bashResolvePath } from '@toolpad/utils/cli'; import { downloadAndExtractExample } from './examples'; import type { SupportedFramework, SupportedRouter, GenerateProjectOptions } from './types'; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts index 79f71489c87..62f7f0c6832 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts @@ -1,4 +1,4 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/AuthPage'; import { kebabToConstant, kebabToPascal } from '@toolpad/utils/strings'; import { requiresIssuer, requiresTenantId } from './utils'; import { Template } from '../../../types'; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts index d30264a6fd2..cd73306a2ba 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts @@ -5,7 +5,7 @@ const signInPage: Template = (options) => { return `${hasPasskeyProvider ? "'use client';" : ''} import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; ${hasPasskeyProvider ? "import { signIn as webauthnSignIn } from 'next-auth/webauthn';" : ''} ${hasPasskeyProvider && hasNodemailerProvider ? `import { getProviders } from "next-auth/react";` : `import { providerMap } from '../../../auth';`} ${hasPasskeyProvider ? `import type { AuthProvider } from '@toolpad/core';` : ''} diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts index 8fe3c4aefdd..249f1224706 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts @@ -5,7 +5,7 @@ const signIn: Template = (options) => { return `import * as React from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; ${hasPasskeyProvider ? "import { signIn as webauthnSignIn } from 'next-auth/webauthn';" : ''} import { signIn } from 'next-auth/react'; import { useRouter } from 'next/router'; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts index 719277b1d92..d6a71993d6a 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts @@ -1,4 +1,4 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/AuthPage'; export function requiresIssuer(provider: SupportedAuthProvider) { return ( diff --git a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts index bbb0d580cf3..9d5d8138260 100644 --- a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts +++ b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts @@ -16,7 +16,7 @@ const signinTemplate: Template = (options) => { import * as React from 'react'; import Alert from '@mui/material/Alert'; import LinearProgress from '@mui/material/LinearProgress'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { Navigate, useNavigate } from 'react-router'; import { useSession, type Session } from '../SessionContext'; import { ${[ diff --git a/packages/create-toolpad-app/src/types.ts b/packages/create-toolpad-app/src/types.ts index e4ed06d01af..01f4015c39a 100644 --- a/packages/create-toolpad-app/src/types.ts +++ b/packages/create-toolpad-app/src/types.ts @@ -1,4 +1,4 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/AuthPage'; import { PackageJson } from './packageType'; export type SupportedRouter = 'nextjs-app' | 'nextjs-pages'; diff --git a/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx b/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx index e0b921a5fd9..0172bf779be 100644 --- a/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx +++ b/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx @@ -19,6 +19,16 @@ export interface LocaleText { providerSignInTitle: (provider: string) => string; signInRememberMe: string; + // SignUpPage + signUpTitle: string | ((brandingTitle?: string) => string); + signUpSubtitle: string; + providerSignUpTitle: (provider: string) => string; + passwordsDoNotMatch: string; + confirmPassword: string; + terms: string; + privacy: string; + agree: string; + // Common authentication labels email: string; passkey: string; @@ -37,6 +47,7 @@ export interface LocaleText { alert: string; confirm: string; loading: string; + and: string; // CRUD createNewButtonLabel: string; diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx b/packages/toolpad-core/src/AuthPage/SignInPage/SignInPage.test.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/SignInPage.test.tsx rename to packages/toolpad-core/src/AuthPage/SignInPage/SignInPage.test.tsx diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/AuthPage/SignInPage/SignInPage.tsx similarity index 94% rename from packages/toolpad-core/src/SignInPage/SignInPage.tsx rename to packages/toolpad-core/src/AuthPage/SignInPage/SignInPage.tsx index 46c890adcae..bae22df4582 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/AuthPage/SignInPage/SignInPage.tsx @@ -17,26 +17,27 @@ import FingerprintIcon from '@mui/icons-material/Fingerprint'; import AppleIcon from '@mui/icons-material/Apple'; import { alpha, useTheme, SxProps, type Theme } from '@mui/material/styles'; import { LinkProps } from '@mui/material/Link'; -import GoogleIcon from './icons/Google'; -import FacebookIcon from './icons/Facebook'; -import TwitterIcon from './icons/Twitter'; -import InstagramIcon from './icons/Instagram'; -import TikTokIcon from './icons/TikTok'; -import LinkedInIcon from './icons/LinkedIn'; -import SlackIcon from './icons/Slack'; -import SpotifyIcon from './icons/Spotify'; -import TwitchIcon from './icons/Twitch'; -import DiscordIcon from './icons/Discord'; -import LineIcon from './icons/Line'; -import Auth0Icon from './icons/Auth0'; -import MicrosoftEntraIdIcon from './icons/MicrosoftEntra'; -import CognitoIcon from './icons/Cognito'; -import GitLabIcon from './icons/GitLab'; -import KeycloakIcon from './icons/Keycloak'; -import OktaIcon from './icons/Okta'; -import FusionAuthIcon from './icons/FusionAuth'; -import { BrandingContext, RouterContext } from '../shared/context'; -import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; +import GoogleIcon from '../../shared/icons/Google'; +import FacebookIcon from '../../shared/icons/Facebook'; +import TwitterIcon from '../../shared/icons/Twitter'; +import InstagramIcon from '../../shared/icons/Instagram'; +import TikTokIcon from '../../shared/icons/TikTok'; +import LinkedInIcon from '../../shared/icons/LinkedIn'; +import SlackIcon from '../../shared/icons/Slack'; +import SpotifyIcon from '../../shared/icons/Spotify'; +import TwitchIcon from '../../shared/icons/Twitch'; +import DiscordIcon from '../../shared/icons/Discord'; +import LineIcon from '../../shared/icons/Line'; +import Auth0Icon from '../../shared/icons/Auth0'; +import MicrosoftEntraIdIcon from '../../shared/icons/MicrosoftEntra'; +import CognitoIcon from '../../shared/icons/Cognito'; +import GitLabIcon from '../../shared/icons/GitLab'; +import KeycloakIcon from '../../shared/icons/Keycloak'; +import OktaIcon from '../../shared/icons/Okta'; +import FusionAuthIcon from '../../shared/icons/FusionAuth'; +import { BrandingContext, RouterContext } from '../../shared/context'; +import { useLocaleText, type LocaleText } from '../../AppProvider/LocalizationProvider'; +import { SupportedAuthProvider, AuthProvider } from '../authTypes'; const mergeSlotSx = (defaultSx: SxProps, slotProps?: { sx?: SxProps }) => { if (Array.isArray(slotProps?.sx)) { @@ -81,35 +82,6 @@ const getCommonTextFieldProps = (theme: Theme, baseProps: TextFieldProps = {}): }, }); -type SupportedOAuthProvider = - | 'github' - | 'google' - | 'facebook' - | 'gitlab' - | 'twitter' - | 'apple' - | 'instagram' - | 'tiktok' - | 'linkedin' - | 'slack' - | 'spotify' - | 'twitch' - | 'discord' - | 'line' - | 'auth0' - | 'cognito' - | 'keycloak' - | 'okta' - | 'fusionauth' - | 'microsoft-entra-id'; - -export type SupportedAuthProvider = - | SupportedOAuthProvider - | 'credentials' - | 'passkey' - | 'nodemailer' - | string; - const IconProviderMap = new Map([ ['github', ], ['credentials', ], @@ -148,23 +120,6 @@ interface SignInPageLocaleText { to: string; } -export interface AuthProvider { - /** - * The unique identifier of the authentication provider. - * @default undefined - * @example 'google' - * @example 'github' - */ - id: SupportedAuthProvider; - /** - * The name of the authentication provider. - * @default '' - * @example 'Google' - * @example 'GitHub' - */ - name: string; -} - export interface AuthResponse { /** * The error message if the sign-in failed. diff --git a/packages/toolpad-core/src/SignInPage/index.ts b/packages/toolpad-core/src/AuthPage/SignInPage/index.ts similarity index 100% rename from packages/toolpad-core/src/SignInPage/index.ts rename to packages/toolpad-core/src/AuthPage/SignInPage/index.ts diff --git a/packages/toolpad-core/src/AuthPage/SignUpPage/SignUpPage.tsx b/packages/toolpad-core/src/AuthPage/SignUpPage/SignUpPage.tsx new file mode 100644 index 00000000000..0e65ed2c828 --- /dev/null +++ b/packages/toolpad-core/src/AuthPage/SignUpPage/SignUpPage.tsx @@ -0,0 +1,735 @@ +'use client'; + +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Alert from '@mui/material/Alert'; +import Button, { ButtonProps } from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Container from '@mui/material/Container'; +import Divider from '@mui/material/Divider'; +import Link, { LinkProps } from '@mui/material/Link'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import PasswordIcon from '@mui/icons-material/Password'; +import FingerprintIcon from '@mui/icons-material/Fingerprint'; +import AppleIcon from '@mui/icons-material/Apple'; +import { alpha, useTheme, SxProps, type Theme } from '@mui/material/styles'; +import GoogleIcon from '../../shared/icons/Google'; +import FacebookIcon from '../../shared/icons/Facebook'; +import TwitterIcon from '../../shared/icons/Twitter'; +import InstagramIcon from '../../shared/icons/Instagram'; +import TikTokIcon from '../../shared/icons/TikTok'; +import LinkedInIcon from '../../shared/icons/LinkedIn'; +import SlackIcon from '../../shared/icons/Slack'; +import SpotifyIcon from '../../shared/icons/Spotify'; +import TwitchIcon from '../../shared/icons/Twitch'; +import DiscordIcon from '../../shared/icons/Discord'; +import LineIcon from '../../shared/icons/Line'; +import Auth0Icon from '../../shared/icons/Auth0'; +import MicrosoftEntraIdIcon from '../../shared/icons/MicrosoftEntra'; +import CognitoIcon from '../../shared/icons/Cognito'; +import GitLabIcon from '../../shared/icons/GitLab'; +import KeycloakIcon from '../../shared/icons/Keycloak'; +import OktaIcon from '../../shared/icons/Okta'; +import FusionAuthIcon from '../../shared/icons/FusionAuth'; +import { BrandingContext, RouterContext } from '../../shared/context'; +import { LocaleText, useLocaleText } from '../../AppProvider/LocalizationProvider'; +import { SupportedAuthProvider, AuthProvider } from '../authTypes'; + +const mergeSlotSx = (defaultSx: SxProps, slotProps?: { sx?: SxProps }) => { + if (Array.isArray(slotProps?.sx)) { + return [defaultSx, ...slotProps.sx]; + } + + if (slotProps?.sx) { + return [defaultSx, slotProps?.sx]; + } + + return [defaultSx]; +}; + +const getCommonTextFieldProps = (theme: Theme, baseProps: TextFieldProps = {}): TextFieldProps => ({ + required: true, + fullWidth: true, + ...baseProps, + slotProps: { + ...baseProps.slotProps, + htmlInput: { + ...baseProps.slotProps?.htmlInput, + sx: mergeSlotSx( + { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, + typeof baseProps.slotProps?.htmlInput === 'function' ? {} : baseProps.slotProps?.htmlInput, + ), + }, + inputLabel: { + ...baseProps.slotProps?.inputLabel, + sx: mergeSlotSx( + { + lineHeight: theme.typography.pxToRem(12), + fontSize: theme.typography.pxToRem(14), + }, + typeof baseProps.slotProps?.inputLabel === 'function' + ? {} + : baseProps.slotProps?.inputLabel, + ), + }, + }, +}); + +const IconProviderMap = new Map([ + ['github', ], + ['credentials', ], + ['google', ], + ['facebook', ], + ['passkey', ], + ['twitter', ], + ['apple', ], + ['instagram', ], + ['tiktok', ], + ['linkedin', ], + ['slack', ], + ['spotify', ], + ['twitch', ], + ['discord', ], + ['line', ], + ['auth0', ], + ['microsoft-entra-id', ], + ['cognito', ], + ['gitlab', ], + ['keycloak', ], + ['okta', ], + ['fusionauth', ], +]); + +interface SignUpPageLocaleText { + signUpTitle: string | ((brandingTitle?: string) => string); + signUpSubtitle: string; + providerSignUpTitle: (provider: string) => string; + email: string; + password: string; + confirmPassword: string; + or: string; + with: string; + passkey: string; + to: string; + terms: string; + privacy: string; + agree: string; + and: string; + passwordsDoNotMatch: string; +} + +export interface SignUpActionResponse { + /** + * The error message if the sign-up failed. + * @default '' + */ + error?: string; + /** + * The success notification if the sign-up was successful. + * @default '' + * Only used for magic link sign-up. + * @example 'Check your email for a magic link.' + */ + success?: string; +} + +export interface SignUpPageSlots { + /** + * The custom email field component used in the credentials form. + * @default TextField + */ + emailField?: React.JSXElementConstructor; + /** + * The custom password field component used in the credentials form. + * @default TextField + */ + passwordField?: React.JSXElementConstructor; + /** + * The custom confirm password field component used in the credentials form. + * @default TextField + */ + confirmPasswordField?: React.JSXElementConstructor; + /** + * The custom submit button component used in the credentials form. + * @default Button + */ + submitButton?: React.JSXElementConstructor; + /** + * The custom terms and conditions link component. + * @default Link + */ + termsLink?: string; + /** + * The custom privacy policy link component. + * @default Link + */ + privacyLink?: string; + /** + * A component to override the default title section + * @default Typography + */ + title?: React.ElementType; + /** + * A component to override the default subtitle section + * @default Typography + */ + subtitle?: React.ElementType; +} + +export interface SignUpPageProps { + /** + * The list of authentication providers to display. + * @default [] + */ + providers?: AuthProvider[]; + /** + * Callback fired when a user signs up. + * @param {AuthProvider} provider The authentication provider. + * @param {FormData} formData The form data if the provider id is 'credentials'.\ + * @param {string} callbackUrl The URL to redirect to after signing up. + * @returns {void|Promise} + * @default undefined + */ + signUp?: ( + provider: AuthProvider, + formData?: any, + callbackUrl?: string, + ) => void | Promise | undefined; + /** + * The components used for each slot inside. + * @default {} + */ + slots?: SignUpPageSlots; + /** + * The props used for each slot inside. + * @default {} + */ + slotProps?: { + emailField?: TextFieldProps; + passwordField?: TextFieldProps; + confirmPasswordField?: TextFieldProps; + submitButton?: ButtonProps; + termsLink?: LinkProps; + privacyLink?: LinkProps; + form?: Partial>; + oAuthButton?: ButtonProps; + }; + /** + * The prop used to customize the styles on the `SignUpPage` container + */ + sx?: SxProps; + /** + * The labels for the account component. + */ + localeText?: Partial; +} + +const defaultLocaleText: Pick = { + signUpTitle: (brandingTitle?: string) => + brandingTitle ? `Sign up to ${brandingTitle}` : 'Sign up', + signUpSubtitle: 'Create your account', + providerSignUpTitle: (provider: string) => `Sign up with ${provider}`, + email: 'Email', + password: 'Password', + confirmPassword: 'Confirm Password', + or: 'or', + with: 'with', + passkey: 'Passkey', + to: 'to', + terms: 'Terms and Conditions', + privacy: 'Privacy Policy', + agree: 'I agree to the', + and: 'and', + passwordsDoNotMatch: 'Passwords do not match', +}; + +/** + * + * Demos: + * + * - [Sign-up Page 🚧](https://mui.com/toolpad/core/react-sign-up-page/) + * + * API: + * + * - [SignUpPage API](https://mui.com/toolpad/core/api/sign-up-page) + */ +function SignUpPage(props: SignUpPageProps) { + const { providers, signUp, slots, slotProps, sx, localeText: propsLocaleText } = props; + const theme = useTheme(); + const branding = React.useContext(BrandingContext); + const router = React.useContext(RouterContext); + const globalLocaleText = useLocaleText(); + const localeText = { ...defaultLocaleText, ...globalLocaleText, ...propsLocaleText }; + + const [{ loading, selectedProviderId, error, success }, setFormStatus] = React.useState<{ + loading: boolean; + selectedProviderId?: SupportedAuthProvider; + error?: string; + success?: string; + }>({ + selectedProviderId: undefined, + loading: false, + error: '', + success: '', + }); + const callbackUrl = router?.searchParams.get('callbackUrl') ?? '/'; + const singleProvider = React.useMemo(() => providers?.length === 1, [providers]); + + const isOauthProvider = React.useCallback( + (provider?: SupportedAuthProvider) => + provider && provider !== 'credentials' && provider !== 'nodemailer' && provider !== 'passkey', + [], + ); + const hasOauthProvider = React.useMemo( + () => providers?.some((provider) => isOauthProvider(provider.id)), + [isOauthProvider, providers], + ); + + const isEmailProvider = React.useCallback( + (provider?: SupportedAuthProvider) => provider && provider === 'nodemailer', + [], + ); + + const isCredentialsProvider = React.useCallback( + (provider?: SupportedAuthProvider) => provider && provider === 'credentials', + [], + ); + + const [currentInputedPassword, setPassword] = React.useState(); + const [currentInputedConfirmedPassword, setConfirmPassword] = React.useState(); + + return ( + + + + {branding?.logo} + + {slots?.title ? ( + + ) : ( + + {typeof localeText.signUpTitle === 'string' + ? localeText.signUpTitle + : localeText.signUpTitle(branding?.title)} + + )} + {slots?.subtitle ? ( + + ) : ( + + {localeText?.signUpSubtitle} + + )} + + + {error && isOauthProvider(selectedProviderId) ? ( + {error} + ) : null} + {Object.values(providers ?? {}) + .filter((provider) => isOauthProvider(provider.id)) + .map((provider: AuthProvider) => { + return ( +
    { + event.preventDefault(); + setFormStatus({ + error: '', + selectedProviderId: provider.id, + loading: true, + }); + const oauthResponse = signUp + ? await signUp(provider, undefined, callbackUrl) + : { error: 'No signUp function provided' }; + setFormStatus((prev) => ({ + ...prev, + loading: oauthResponse?.error ? false : prev.loading, + error: oauthResponse?.error, + })); + }} + {...slotProps?.form} + > + +
    + ); + })} +
    + + {Object.values(providers ?? {}) + .filter((provider) => !isOauthProvider(provider.id)) + .map((provider: AuthProvider, index: number) => { + return ( + + {isEmailProvider(provider.id) ? ( + + {hasOauthProvider || index > 0 ? ( + {localeText.or} + ) : null} + {error && selectedProviderId === 'nodemailer' ? ( + + {error} + + ) : null} + {success && selectedProviderId === 'nodemailer' ? ( + + {success} + + ) : null} + { + event.preventDefault(); + setFormStatus({ + error: '', + selectedProviderId: provider.id, + loading: true, + }); + const formData = new FormData(event.currentTarget); + const emailResponse = await signUp?.(provider, formData, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: emailResponse?.error, + success: emailResponse?.success, + })); + }} + {...slotProps?.form} + > + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.submitButton ? ( + + ) : ( + + )} + + + ) : null} + + {isCredentialsProvider(provider.id) ? ( + + {hasOauthProvider || index > 0 ? ( + {localeText.or} + ) : null} + {error && selectedProviderId === 'credentials' ? ( + + {error} + + ) : null} + { + setFormStatus({ + error: '', + selectedProviderId: provider.id, + loading: true, + }); + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const credentialsResponse = await signUp?.( + provider, + formData, + callbackUrl, + ); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: credentialsResponse?.error, + })); + }} + {...slotProps?.form} + > + + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.passwordField ? ( + + ) : ( + ) => { + setPassword(event.target.value); + }} + {...getCommonTextFieldProps(theme, { + name: 'password', + type: 'password', + label: localeText.password, + id: 'password', + placeholder: '*****', + autoComplete: 'new-password', + ...slotProps?.passwordField, + })} + /> + )} + {slots?.confirmPasswordField ? ( + + ) : ( + ) => { + setConfirmPassword(event.target.value); + }} + error={currentInputedConfirmedPassword !== currentInputedPassword} + helperText={ + currentInputedConfirmedPassword !== currentInputedPassword + ? localeText.passwordsDoNotMatch + : undefined + } + {...getCommonTextFieldProps(theme, { + name: 'confirmPassword', + type: 'password', + label: localeText.confirmPassword, + id: 'confirm-password', + placeholder: '*****', + autoComplete: 'new-password', + ...slotProps?.confirmPasswordField, + })} + {...getCommonTextFieldProps(theme, { + name: 'confirmPassword', + type: 'password', + label: localeText.confirmPassword, + id: 'confirm-password', + placeholder: '*****', + autoComplete: 'new-password', + ...slotProps?.confirmPasswordField, + })} + /> + )} + + {slots?.termsLink || slots?.privacyLink ? ( + + + + {localeText.agree} + + {slots?.termsLink ? ( + + {localeText.terms} + + ) : null} + {slots?.termsLink && slots?.privacyLink ? ( + + {localeText.and} + + ) : null} + {slots?.privacyLink ? ( + + {localeText.privacy} + + ) : null} + + + ) : null} + + {slots?.submitButton ? ( + + ) : ( + + )} + + + ) : null} + + ); + })} +
    +
    +
    +
    + ); +} + +SignUpPage.propTypes /* remove-proptypes */ = { + // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Warning ──────────────────────────────┐ + // β”‚ These PropTypes are generated from the TypeScript type definitions. β”‚ + // β”‚ To update them, edit the TypeScript types and run `pnpm proptypes`. β”‚ + // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + /** + * The labels for the account component. + */ + localeText: PropTypes.object, + /** + * The list of authentication providers to display. + * @default [] + */ + providers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ), + /** + * Callback fired when a user signs up. + * @param {AuthProvider} provider The authentication provider. + * @param {FormData} formData The form data if the provider id is 'credentials'.\ + * @param {string} callbackUrl The URL to redirect to after signing up. + * @returns {void|Promise} + * @default undefined + */ + signUp: PropTypes.func, + /** + * The props used for each slot inside. + * @default {} + */ + slotProps: PropTypes.shape({ + confirmPasswordField: PropTypes.object, + emailField: PropTypes.object, + form: PropTypes.object, + oAuthButton: PropTypes.object, + passwordField: PropTypes.object, + privacyLink: PropTypes.object, + submitButton: PropTypes.object, + termsLink: PropTypes.object, + }), + /** + * The components used for each slot inside. + * @default {} + */ + slots: PropTypes.shape({ + confirmPasswordField: PropTypes.elementType, + emailField: PropTypes.elementType, + passwordField: PropTypes.elementType, + privacyLink: PropTypes.string, + submitButton: PropTypes.elementType, + subtitle: PropTypes.elementType, + termsLink: PropTypes.string, + title: PropTypes.elementType, + }), + /** + * The prop used to customize the styles on the `SignUpPage` container + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), +} as any; + +export { SignUpPage }; diff --git a/packages/toolpad-core/src/AuthPage/SignUpPage/index.ts b/packages/toolpad-core/src/AuthPage/SignUpPage/index.ts new file mode 100644 index 00000000000..b956d42fa23 --- /dev/null +++ b/packages/toolpad-core/src/AuthPage/SignUpPage/index.ts @@ -0,0 +1 @@ +export * from './SignUpPage'; diff --git a/packages/toolpad-core/src/AuthPage/authTypes.ts b/packages/toolpad-core/src/AuthPage/authTypes.ts new file mode 100644 index 00000000000..a9ccb03de99 --- /dev/null +++ b/packages/toolpad-core/src/AuthPage/authTypes.ts @@ -0,0 +1,45 @@ +export interface AuthProvider { + /** + * The unique identifier of the authentication provider. + * @default undefined + * @example 'google' + * @example 'github' + */ + id: SupportedAuthProvider; + /** + * The name of the authentication provider. + * @default '' + * @example 'Google' + * @example 'GitHub' + */ + name: string; +} + +export type SupportedAuthProvider = + | SupportedOAuthProvider + | 'credentials' + | 'passkey' + | 'nodemailer' + | string; + +type SupportedOAuthProvider = + | 'github' + | 'google' + | 'facebook' + | 'gitlab' + | 'twitter' + | 'apple' + | 'instagram' + | 'tiktok' + | 'linkedin' + | 'slack' + | 'spotify' + | 'twitch' + | 'discord' + | 'line' + | 'auth0' + | 'cognito' + | 'keycloak' + | 'okta' + | 'fusionauth' + | 'microsoft-entra-id'; diff --git a/packages/toolpad-core/src/AuthPage/index.ts b/packages/toolpad-core/src/AuthPage/index.ts new file mode 100644 index 00000000000..7dc5a05aac1 --- /dev/null +++ b/packages/toolpad-core/src/AuthPage/index.ts @@ -0,0 +1,3 @@ +export * from './SignInPage'; +export * from './SignUpPage'; +export * from './authTypes'; diff --git a/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx new file mode 100644 index 00000000000..be871099868 --- /dev/null +++ b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx @@ -0,0 +1,735 @@ +'use client'; + +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Alert from '@mui/material/Alert'; +import Button, { ButtonProps } from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Container from '@mui/material/Container'; +import Divider from '@mui/material/Divider'; +import Link, { LinkProps } from '@mui/material/Link'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import PasswordIcon from '@mui/icons-material/Password'; +import FingerprintIcon from '@mui/icons-material/Fingerprint'; +import AppleIcon from '@mui/icons-material/Apple'; +import { alpha, useTheme, SxProps, type Theme } from '@mui/material/styles'; +import GoogleIcon from '../shared/icons/Google'; +import FacebookIcon from '../shared/icons/Facebook'; +import TwitterIcon from '../shared/icons/Twitter'; +import InstagramIcon from '../shared/icons/Instagram'; +import TikTokIcon from '../shared/icons/TikTok'; +import LinkedInIcon from '../shared/icons/LinkedIn'; +import SlackIcon from '../shared/icons/Slack'; +import SpotifyIcon from '../shared/icons/Spotify'; +import TwitchIcon from '../shared/icons/Twitch'; +import DiscordIcon from '../shared/icons/Discord'; +import LineIcon from '../shared/icons/Line'; +import Auth0Icon from '../shared/icons/Auth0'; +import MicrosoftEntraIdIcon from '../shared/icons/MicrosoftEntra'; +import CognitoIcon from '../shared/icons/Cognito'; +import GitLabIcon from '../shared/icons/GitLab'; +import KeycloakIcon from '../shared/icons/Keycloak'; +import OktaIcon from '../shared/icons/Okta'; +import FusionAuthIcon from '../shared/icons/FusionAuth'; +import { BrandingContext, RouterContext } from '../shared/context'; +import { LocaleText, useLocaleText } from '../AppProvider/LocalizationProvider'; +import { SupportedAuthProvider, AuthProvider } from '../AuthPage/authTypes'; + +const mergeSlotSx = (defaultSx: SxProps, slotProps?: { sx?: SxProps }) => { + if (Array.isArray(slotProps?.sx)) { + return [defaultSx, ...slotProps.sx]; + } + + if (slotProps?.sx) { + return [defaultSx, slotProps?.sx]; + } + + return [defaultSx]; +}; + +const getCommonTextFieldProps = (theme: Theme, baseProps: TextFieldProps = {}): TextFieldProps => ({ + required: true, + fullWidth: true, + ...baseProps, + slotProps: { + ...baseProps.slotProps, + htmlInput: { + ...baseProps.slotProps?.htmlInput, + sx: mergeSlotSx( + { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, + typeof baseProps.slotProps?.htmlInput === 'function' ? {} : baseProps.slotProps?.htmlInput, + ), + }, + inputLabel: { + ...baseProps.slotProps?.inputLabel, + sx: mergeSlotSx( + { + lineHeight: theme.typography.pxToRem(12), + fontSize: theme.typography.pxToRem(14), + }, + typeof baseProps.slotProps?.inputLabel === 'function' + ? {} + : baseProps.slotProps?.inputLabel, + ), + }, + }, +}); + +const IconProviderMap = new Map([ + ['github', ], + ['credentials', ], + ['google', ], + ['facebook', ], + ['passkey', ], + ['twitter', ], + ['apple', ], + ['instagram', ], + ['tiktok', ], + ['linkedin', ], + ['slack', ], + ['spotify', ], + ['twitch', ], + ['discord', ], + ['line', ], + ['auth0', ], + ['microsoft-entra-id', ], + ['cognito', ], + ['gitlab', ], + ['keycloak', ], + ['okta', ], + ['fusionauth', ], +]); + +interface SignUpPageLocaleText { + signUpTitle: string | ((brandingTitle?: string) => string); + signUpSubtitle: string; + providerSignUpTitle: (provider: string) => string; + email: string; + password: string; + confirmPassword: string; + or: string; + with: string; + passkey: string; + to: string; + terms: string; + privacy: string; + agree: string; + and: string; + passwordsDoNotMatch: string; +} + +export interface SignUpActionResponse { + /** + * The error message if the sign-up failed. + * @default '' + */ + error?: string; + /** + * The success notification if the sign-up was successful. + * @default '' + * Only used for magic link sign-up. + * @example 'Check your email for a magic link.' + */ + success?: string; +} + +export interface SignUpPageSlots { + /** + * The custom email field component used in the credentials form. + * @default TextField + */ + emailField?: React.JSXElementConstructor; + /** + * The custom password field component used in the credentials form. + * @default TextField + */ + passwordField?: React.JSXElementConstructor; + /** + * The custom confirm password field component used in the credentials form. + * @default TextField + */ + confirmPasswordField?: React.JSXElementConstructor; + /** + * The custom submit button component used in the credentials form. + * @default Button + */ + submitButton?: React.JSXElementConstructor; + /** + * The custom terms and conditions link component. + * @default Link + */ + termsLink?: string; + /** + * The custom privacy policy link component. + * @default Link + */ + privacyLink?: string; + /** + * A component to override the default title section + * @default Typography + */ + title?: React.ElementType; + /** + * A component to override the default subtitle section + * @default Typography + */ + subtitle?: React.ElementType; +} + +export interface SignUpPageProps { + /** + * The list of authentication providers to display. + * @default [] + */ + providers?: AuthProvider[]; + /** + * Callback fired when a user signs up. + * @param {AuthProvider} provider The authentication provider. + * @param {FormData} formData The form data if the provider id is 'credentials'.\ + * @param {string} callbackUrl The URL to redirect to after signing up. + * @returns {void|Promise} + * @default undefined + */ + signUp?: ( + provider: AuthProvider, + formData?: any, + callbackUrl?: string, + ) => void | Promise | undefined; + /** + * The components used for each slot inside. + * @default {} + */ + slots?: SignUpPageSlots; + /** + * The props used for each slot inside. + * @default {} + */ + slotProps?: { + emailField?: TextFieldProps; + passwordField?: TextFieldProps; + confirmPasswordField?: TextFieldProps; + submitButton?: ButtonProps; + termsLink?: LinkProps; + privacyLink?: LinkProps; + form?: Partial>; + oAuthButton?: ButtonProps; + }; + /** + * The prop used to customize the styles on the `SignUpPage` container + */ + sx?: SxProps; + /** + * The labels for the account component. + */ + localeText?: Partial; +} + +const defaultLocaleText: Pick = { + signUpTitle: (brandingTitle?: string) => + brandingTitle ? `Sign up to ${brandingTitle}` : 'Sign up', + signUpSubtitle: 'Create your account', + providerSignUpTitle: (provider: string) => `Sign up with ${provider}`, + email: 'Email', + password: 'Password', + confirmPassword: 'Confirm Password', + or: 'or', + with: 'with', + passkey: 'Passkey', + to: 'to', + terms: 'Terms and Conditions', + privacy: 'Privacy Policy', + agree: 'I agree to the', + and: 'and', + passwordsDoNotMatch: 'Passwords do not match', +}; + +/** + * + * Demos: + * + * - [Sign-up Page 🚧](https://mui.com/toolpad/core/react-sign-up-page/) + * + * API: + * + * - [SignUpPage API](https://mui.com/toolpad/core/api/sign-up-page) + */ +function SignUpPage(props: SignUpPageProps) { + const { providers, signUp, slots, slotProps, sx, localeText: propsLocaleText } = props; + const theme = useTheme(); + const branding = React.useContext(BrandingContext); + const router = React.useContext(RouterContext); + const globalLocaleText = useLocaleText(); + const localeText = { ...defaultLocaleText, ...globalLocaleText, ...propsLocaleText }; + + const [{ loading, selectedProviderId, error, success }, setFormStatus] = React.useState<{ + loading: boolean; + selectedProviderId?: SupportedAuthProvider; + error?: string; + success?: string; + }>({ + selectedProviderId: undefined, + loading: false, + error: '', + success: '', + }); + const callbackUrl = router?.searchParams.get('callbackUrl') ?? '/'; + const singleProvider = React.useMemo(() => providers?.length === 1, [providers]); + + const isOauthProvider = React.useCallback( + (provider?: SupportedAuthProvider) => + provider && provider !== 'credentials' && provider !== 'nodemailer' && provider !== 'passkey', + [], + ); + const hasOauthProvider = React.useMemo( + () => providers?.some((provider) => isOauthProvider(provider.id)), + [isOauthProvider, providers], + ); + + const isEmailProvider = React.useCallback( + (provider?: SupportedAuthProvider) => provider && provider === 'nodemailer', + [], + ); + + const isCredentialsProvider = React.useCallback( + (provider?: SupportedAuthProvider) => provider && provider === 'credentials', + [], + ); + + const [currentInputedPassword, setPassword] = React.useState(); + const [currentInputedConfirmedPassword, setConfirmPassword] = React.useState(); + + return ( + + + + {branding?.logo} + + {slots?.title ? ( + + ) : ( + + {typeof localeText.signUpTitle === 'string' + ? localeText.signUpTitle + : localeText.signUpTitle(branding?.title)} + + )} + {slots?.subtitle ? ( + + ) : ( + + {localeText?.signUpSubtitle} + + )} + + + {error && isOauthProvider(selectedProviderId) ? ( + {error} + ) : null} + {Object.values(providers ?? {}) + .filter((provider) => isOauthProvider(provider.id)) + .map((provider: AuthProvider) => { + return ( +
    { + event.preventDefault(); + setFormStatus({ + error: '', + selectedProviderId: provider.id, + loading: true, + }); + const oauthResponse = signUp + ? await signUp(provider, undefined, callbackUrl) + : { error: 'No signUp function provided' }; + setFormStatus((prev) => ({ + ...prev, + loading: oauthResponse?.error ? false : prev.loading, + error: oauthResponse?.error, + })); + }} + {...slotProps?.form} + > + +
    + ); + })} +
    + + {Object.values(providers ?? {}) + .filter((provider) => !isOauthProvider(provider.id)) + .map((provider: AuthProvider, index: number) => { + return ( + + {isEmailProvider(provider.id) ? ( + + {hasOauthProvider || index > 0 ? ( + {localeText.or} + ) : null} + {error && selectedProviderId === 'nodemailer' ? ( + + {error} + + ) : null} + {success && selectedProviderId === 'nodemailer' ? ( + + {success} + + ) : null} + { + event.preventDefault(); + setFormStatus({ + error: '', + selectedProviderId: provider.id, + loading: true, + }); + const formData = new FormData(event.currentTarget); + const emailResponse = await signUp?.(provider, formData, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: emailResponse?.error, + success: emailResponse?.success, + })); + }} + {...slotProps?.form} + > + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.submitButton ? ( + + ) : ( + + )} + + + ) : null} + + {isCredentialsProvider(provider.id) ? ( + + {hasOauthProvider || index > 0 ? ( + {localeText.or} + ) : null} + {error && selectedProviderId === 'credentials' ? ( + + {error} + + ) : null} + { + setFormStatus({ + error: '', + selectedProviderId: provider.id, + loading: true, + }); + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const credentialsResponse = await signUp?.( + provider, + formData, + callbackUrl, + ); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: credentialsResponse?.error, + })); + }} + {...slotProps?.form} + > + + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.passwordField ? ( + + ) : ( + ) => { + setPassword(event.target.value); + }} + {...getCommonTextFieldProps(theme, { + name: 'password', + type: 'password', + label: localeText.password, + id: 'password', + placeholder: '*****', + autoComplete: 'new-password', + ...slotProps?.passwordField, + })} + /> + )} + {slots?.confirmPasswordField ? ( + + ) : ( + ) => { + setConfirmPassword(event.target.value); + }} + error={currentInputedConfirmedPassword !== currentInputedPassword} + helperText={ + currentInputedConfirmedPassword !== currentInputedPassword + ? localeText.passwordsDoNotMatch + : undefined + } + {...getCommonTextFieldProps(theme, { + name: 'confirmPassword', + type: 'password', + label: localeText.confirmPassword, + id: 'confirm-password', + placeholder: '*****', + autoComplete: 'new-password', + ...slotProps?.confirmPasswordField, + })} + {...getCommonTextFieldProps(theme, { + name: 'confirmPassword', + type: 'password', + label: localeText.confirmPassword, + id: 'confirm-password', + placeholder: '*****', + autoComplete: 'new-password', + ...slotProps?.confirmPasswordField, + })} + /> + )} + + {slots?.termsLink || slots?.privacyLink ? ( + + + + {localeText.agree} + + {slots?.termsLink ? ( + + {localeText.terms} + + ) : null} + {slots?.termsLink && slots?.privacyLink ? ( + + {localeText.and} + + ) : null} + {slots?.privacyLink ? ( + + {localeText.privacy} + + ) : null} + + + ) : null} + + {slots?.submitButton ? ( + + ) : ( + + )} + + + ) : null} + + ); + })} +
    +
    +
    +
    + ); +} + +SignUpPage.propTypes /* remove-proptypes */ = { + // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Warning ──────────────────────────────┐ + // β”‚ These PropTypes are generated from the TypeScript type definitions. β”‚ + // β”‚ To update them, edit the TypeScript types and run `pnpm proptypes`. β”‚ + // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + /** + * The labels for the account component. + */ + localeText: PropTypes.object, + /** + * The list of authentication providers to display. + * @default [] + */ + providers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ), + /** + * Callback fired when a user signs up. + * @param {AuthProvider} provider The authentication provider. + * @param {FormData} formData The form data if the provider id is 'credentials'.\ + * @param {string} callbackUrl The URL to redirect to after signing up. + * @returns {void|Promise} + * @default undefined + */ + signUp: PropTypes.func, + /** + * The props used for each slot inside. + * @default {} + */ + slotProps: PropTypes.shape({ + confirmPasswordField: PropTypes.object, + emailField: PropTypes.object, + form: PropTypes.object, + oAuthButton: PropTypes.object, + passwordField: PropTypes.object, + privacyLink: PropTypes.object, + submitButton: PropTypes.object, + termsLink: PropTypes.object, + }), + /** + * The components used for each slot inside. + * @default {} + */ + slots: PropTypes.shape({ + confirmPasswordField: PropTypes.elementType, + emailField: PropTypes.elementType, + passwordField: PropTypes.elementType, + privacyLink: PropTypes.string, + submitButton: PropTypes.elementType, + subtitle: PropTypes.elementType, + termsLink: PropTypes.string, + title: PropTypes.elementType, + }), + /** + * The prop used to customize the styles on the `SignUpPage` container + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), +} as any; + +export { SignUpPage }; diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts index aad6a5b4b85..9a1a4bbb016 100644 --- a/packages/toolpad-core/src/index.ts +++ b/packages/toolpad-core/src/index.ts @@ -2,7 +2,7 @@ export * from './AppProvider'; export * from './DashboardLayout'; -export * from './SignInPage'; +export * from './AuthPage'; export * from './Account'; diff --git a/packages/toolpad-core/src/locales/en.tsx b/packages/toolpad-core/src/locales/en.tsx index 0b706cf0247..806dd5b75cd 100644 --- a/packages/toolpad-core/src/locales/en.tsx +++ b/packages/toolpad-core/src/locales/en.tsx @@ -17,6 +17,17 @@ const enLabels: LocaleText = { signInRememberMe: 'Remember Me', providerSignInTitle: (provider: string) => `Sign in with ${provider}`, + // SignUpPage + signUpTitle: (brandingTitle?: string) => + brandingTitle ? `Sign up to ${brandingTitle}` : 'Sign up', + signUpSubtitle: 'Welcome user, please sign up to continue', + providerSignUpTitle: (provider: string) => `Sign up with ${provider}`, + passwordsDoNotMatch: 'Passwords do not match', + confirmPassword: 'Confirm Password', + terms: 'Terms of Service', + privacy: 'Privacy Policy', + agree: 'I agree to the', + // Common authentication labels email: 'Email', password: 'Password', @@ -35,6 +46,7 @@ const enLabels: LocaleText = { alert: 'Alert', confirm: 'Confirm', loading: 'Loading...', + and: 'And', // CRUD createNewButtonLabel: 'Create new', diff --git a/packages/toolpad-core/src/SignInPage/icons/Auth0.tsx b/packages/toolpad-core/src/shared/icons/Auth0.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Auth0.tsx rename to packages/toolpad-core/src/shared/icons/Auth0.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Cognito.tsx b/packages/toolpad-core/src/shared/icons/Cognito.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Cognito.tsx rename to packages/toolpad-core/src/shared/icons/Cognito.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Discord.tsx b/packages/toolpad-core/src/shared/icons/Discord.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Discord.tsx rename to packages/toolpad-core/src/shared/icons/Discord.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Facebook.tsx b/packages/toolpad-core/src/shared/icons/Facebook.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Facebook.tsx rename to packages/toolpad-core/src/shared/icons/Facebook.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/FusionAuth.tsx b/packages/toolpad-core/src/shared/icons/FusionAuth.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/FusionAuth.tsx rename to packages/toolpad-core/src/shared/icons/FusionAuth.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/GitLab.tsx b/packages/toolpad-core/src/shared/icons/GitLab.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/GitLab.tsx rename to packages/toolpad-core/src/shared/icons/GitLab.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Google.tsx b/packages/toolpad-core/src/shared/icons/Google.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Google.tsx rename to packages/toolpad-core/src/shared/icons/Google.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Instagram.tsx b/packages/toolpad-core/src/shared/icons/Instagram.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Instagram.tsx rename to packages/toolpad-core/src/shared/icons/Instagram.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Keycloak.tsx b/packages/toolpad-core/src/shared/icons/Keycloak.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Keycloak.tsx rename to packages/toolpad-core/src/shared/icons/Keycloak.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Line.tsx b/packages/toolpad-core/src/shared/icons/Line.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Line.tsx rename to packages/toolpad-core/src/shared/icons/Line.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/LinkedIn.tsx b/packages/toolpad-core/src/shared/icons/LinkedIn.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/LinkedIn.tsx rename to packages/toolpad-core/src/shared/icons/LinkedIn.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/MicrosoftEntra.tsx b/packages/toolpad-core/src/shared/icons/MicrosoftEntra.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/MicrosoftEntra.tsx rename to packages/toolpad-core/src/shared/icons/MicrosoftEntra.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Okta.tsx b/packages/toolpad-core/src/shared/icons/Okta.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Okta.tsx rename to packages/toolpad-core/src/shared/icons/Okta.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Slack.tsx b/packages/toolpad-core/src/shared/icons/Slack.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Slack.tsx rename to packages/toolpad-core/src/shared/icons/Slack.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Spotify.tsx b/packages/toolpad-core/src/shared/icons/Spotify.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Spotify.tsx rename to packages/toolpad-core/src/shared/icons/Spotify.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/TikTok.tsx b/packages/toolpad-core/src/shared/icons/TikTok.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/TikTok.tsx rename to packages/toolpad-core/src/shared/icons/TikTok.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Twitch.tsx b/packages/toolpad-core/src/shared/icons/Twitch.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Twitch.tsx rename to packages/toolpad-core/src/shared/icons/Twitch.tsx diff --git a/packages/toolpad-core/src/SignInPage/icons/Twitter.tsx b/packages/toolpad-core/src/shared/icons/Twitter.tsx similarity index 100% rename from packages/toolpad-core/src/SignInPage/icons/Twitter.tsx rename to packages/toolpad-core/src/shared/icons/Twitter.tsx diff --git a/playground/nextjs-pages/src/pages/auth/signin.tsx b/playground/nextjs-pages/src/pages/auth/signin.tsx index de3ad580db8..4838c7bcb66 100644 --- a/playground/nextjs-pages/src/pages/auth/signin.tsx +++ b/playground/nextjs-pages/src/pages/auth/signin.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import Link from '@mui/material/Link'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { signIn } from 'next-auth/react'; import { useRouter } from 'next/router'; import { auth, providerMap } from '../../auth'; diff --git a/playground/nextjs/src/app/auth/signin/actions.ts b/playground/nextjs/src/app/auth/signin/actions.ts index e79fd0b7541..806b40bc3a7 100644 --- a/playground/nextjs/src/app/auth/signin/actions.ts +++ b/playground/nextjs/src/app/auth/signin/actions.ts @@ -1,6 +1,6 @@ 'use server'; import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/AuthPage'; import { signIn as signInAction } from '../../../auth'; async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { diff --git a/playground/nextjs/src/app/auth/signin/page.tsx b/playground/nextjs/src/app/auth/signin/page.tsx index 0347eda917b..2c241c1f2c2 100644 --- a/playground/nextjs/src/app/auth/signin/page.tsx +++ b/playground/nextjs/src/app/auth/signin/page.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import Link from '@mui/material/Link'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/AuthPage'; import { providerMap } from '../../../auth'; import signIn from './actions'; diff --git a/playground/nextjs/src/app/auth/signup/actions.ts b/playground/nextjs/src/app/auth/signup/actions.ts new file mode 100644 index 00000000000..4b79099486d --- /dev/null +++ b/playground/nextjs/src/app/auth/signup/actions.ts @@ -0,0 +1,32 @@ +'use server'; + +import { AuthProvider, SignUpActionResponse } from '@toolpad/core/AuthPage'; + +const signupAction = async ( + provider: AuthProvider, + formData?: FormData, + _callbackUrl?: string, +): Promise => { + if (provider.id !== 'credentials') { + return { error: 'Now supporting email authentication only.' }; + } + + const email = formData?.get('email')?.toString(); + const password = formData?.get('password')?.toString(); + + if (!email || !password) { + return { error: 'Email and password are required!' }; + } + + try { + // Would normally call an API to register the user + // For example: await api.register({ email, password }); + // Simulating a successful registration + // await new Promise((resolve) => setTimeout(resolve, 1000)); + return { success: 'OK' }; + } catch (err: any) { + return { error: err.message || 'Failed to register.' }; + } +}; + +export default signupAction; diff --git a/playground/nextjs/src/app/auth/signup/page.tsx b/playground/nextjs/src/app/auth/signup/page.tsx new file mode 100644 index 00000000000..814ca9575bd --- /dev/null +++ b/playground/nextjs/src/app/auth/signup/page.tsx @@ -0,0 +1,15 @@ +'use client'; +import * as React from 'react'; +import { SignUpPage } from '@toolpad/core/AuthPage'; +import { providerMap } from '../../../auth'; +import signUp from './actions'; + +export default function SignIn() { + return ( + + ); +} diff --git a/playground/nextjs/src/middleware.ts b/playground/nextjs/src/middleware.ts index 02c48f4db60..dfe541b6be8 100644 --- a/playground/nextjs/src/middleware.ts +++ b/playground/nextjs/src/middleware.ts @@ -2,5 +2,5 @@ export { auth as middleware } from './auth'; export const config = { // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], + matcher: ['/((?!api|auth/signup|_next/static|_next/image|.*\\.png$).*)'], };