From 3ec67951265b2197f249bfb8076e44c4e4b7ac70 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 27 Dec 2024 17:05:26 +0530 Subject: [PATCH 1/5] wip: Add "SignUpPage" --- .../sign-in-page/MagicLinkAlertSignInPage.js | 1 + .../sign-in-page/MagicLinkAlertSignInPage.tsx | 8 +- .../NotificationsSignInPageError.js | 1 + .../NotificationsSignInPageError.tsx | 7 +- .../sign-in-page/OAuthSignInPage.js | 1 + .../sign-in-page/OAuthSignInPage.tsx | 7 +- .../sign-in-page/ThemeSignInPage.js | 1 + .../sign-in-page/ThemeSignInPage.tsx | 7 +- .../components/sign-up-page/sign-up-page.md | 197 ++++- docs/data/toolpad/core/pagesApi.js | 1 + docs/pages/toolpad/core/api/sign-up-page.js | 23 + docs/pages/toolpad/core/api/sign-up-page.json | 87 +++ .../api-docs/sign-up-page/sign-up-page.json | 28 + .../src/SignInPage/SignInPage.tsx | 161 +--- .../src/SignUpPage/SignUpPage.tsx | 546 ++++++++++++++ packages/toolpad-core/src/SignUpPage/index.ts | 1 + packages/toolpad-core/src/auth/index.ts | 1 + packages/toolpad-core/src/auth/types.ts | 65 ++ packages/toolpad-core/src/index.ts | 4 + .../{SignInPage => shared}/icons/Auth0.tsx | 0 .../{SignInPage => shared}/icons/Cognito.tsx | 0 .../{SignInPage => shared}/icons/Discord.tsx | 0 .../{SignInPage => shared}/icons/Facebook.tsx | 0 .../icons/FusionAuth.tsx | 0 .../{SignInPage => shared}/icons/GitLab.tsx | 0 .../{SignInPage => shared}/icons/Google.tsx | 0 .../icons/Instagram.tsx | 0 .../{SignInPage => shared}/icons/Keycloak.tsx | 0 .../src/{SignInPage => shared}/icons/Line.tsx | 0 .../{SignInPage => shared}/icons/LinkedIn.tsx | 0 .../icons/MicrosoftEntra.tsx | 0 .../src/{SignInPage => shared}/icons/Okta.tsx | 0 .../{SignInPage => shared}/icons/Slack.tsx | 0 .../{SignInPage => shared}/icons/Spotify.tsx | 0 .../{SignInPage => shared}/icons/TikTok.tsx | 0 .../{SignInPage => shared}/icons/Twitch.tsx | 0 .../{SignInPage => shared}/icons/Twitter.tsx | 0 .../src/shared/icons/iconProviderMap.tsx | 51 ++ .../toolpad-core/src/shared/utils/index.ts | 47 ++ playground/nextjs/next-env.d.ts | 2 +- playground/vite/package.json | 1 + playground/vite/src/App.tsx | 54 +- playground/vite/src/SessionContext.tsx | 25 + playground/vite/src/firebase/auth.ts | 191 +++++ .../vite/src/firebase/firebaseConfig.ts | 14 + playground/vite/src/layouts/dashboard.tsx | 35 +- playground/vite/src/main.tsx | 10 + playground/vite/src/pages/signin.tsx | 93 +++ playground/vite/src/pages/signup.tsx | 108 +++ pnpm-lock.yaml | 693 ++++++++++++++++++ 50 files changed, 2282 insertions(+), 189 deletions(-) create mode 100644 docs/pages/toolpad/core/api/sign-up-page.js create mode 100644 docs/pages/toolpad/core/api/sign-up-page.json create mode 100644 docs/translations/api-docs/sign-up-page/sign-up-page.json create mode 100644 packages/toolpad-core/src/SignUpPage/SignUpPage.tsx create mode 100644 packages/toolpad-core/src/SignUpPage/index.ts create mode 100644 packages/toolpad-core/src/auth/index.ts create mode 100644 packages/toolpad-core/src/auth/types.ts rename packages/toolpad-core/src/{SignInPage => shared}/icons/Auth0.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Cognito.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Discord.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Facebook.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/FusionAuth.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/GitLab.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Google.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Instagram.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Keycloak.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Line.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/LinkedIn.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/MicrosoftEntra.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Okta.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Slack.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Spotify.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/TikTok.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Twitch.tsx (100%) rename packages/toolpad-core/src/{SignInPage => shared}/icons/Twitter.tsx (100%) create mode 100644 packages/toolpad-core/src/shared/icons/iconProviderMap.tsx create mode 100644 packages/toolpad-core/src/shared/utils/index.ts create mode 100644 playground/vite/src/SessionContext.tsx create mode 100644 playground/vite/src/firebase/auth.ts create mode 100644 playground/vite/src/firebase/firebaseConfig.ts create mode 100644 playground/vite/src/pages/signin.tsx create mode 100644 playground/vite/src/pages/signup.tsx 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 f9c46a79762..f0faa99d6e6 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,6 @@ import * as React from 'react'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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 1097b0bda9f..3d2679eb3cf 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import { +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { + AuthResponse, AuthProvider, - SignInPage, SupportedAuthProvider, - AuthResponse, -} from '@toolpad/core/SignInPage'; +} from '@toolpad/core/auth'; 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 326abcc3c64..b85d8df42ab 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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 8035b294386..fb3207c3c34 100644 --- a/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/NotificationsSignInPageError.tsx @@ -1,10 +1,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { - SignInPage, - type AuthProvider, - type AuthResponse, -} from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthResponse, AuthProvider } from '@toolpad/core/auth'; 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..d3d8a669d9e 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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..27595be1b5c 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { - AuthResponse, - SignInPage, - type AuthProvider, -} from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthResponse, AuthProvider } from '@toolpad/core/auth'; import { useTheme } from '@mui/material/styles'; // preview-start 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 38d573b4453..8127ebd4bc3 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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 d9ce9dc1702..dd2b1cbe448 100644 --- a/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/ThemeSignInPage.tsx @@ -1,10 +1,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { - SignInPage, - type AuthProvider, - type AuthResponse, -} from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthProvider, AuthResponse } from '@toolpad/core/auth'; 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-up-page/sign-up-page.md b/docs/data/toolpad/core/components/sign-up-page/sign-up-page.md index ae978f3779a..5d142f868d6 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,198 @@ --- productId: toolpad-core title: Sign-up Page +components: SignUpPage --- -# Sign-up Page 🚧 +# 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.

-:::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. - -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. +:::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. ::: + +## Basic usage + +```jsx +import { SignUpPage } from '@toolpad/core'; + +export default function App() { + return ( + console.log(data)} + providers={['google', 'github']} + /> + ); +} +``` + +## Props + +### Required props + +| Name | Type | Description | +| ---------- | ---------------------------- | ------------------------------------------ | +| `onSubmit` | `(data: SignUpData) => void` | Callback fired when the form is submitted. | + +### Optional props + +| Name | Type | Default | Description | +| ------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------ | +| `providers` | `string[]` | `[]` | List of OAuth providers to display. Supported values: 'google', 'github', 'twitter', 'facebook'. | +| `title` | `string` | `'Sign up'` | The title displayed at the top of the page. | +| `logo` | `ReactNode` | `undefined` | Custom logo to display above the form. | +| `theme` | `Theme` | `undefined` | Custom theme object to override default styles. | +| `redirectUrl` | `string` | `'/'` | URL to redirect to after successful sign-up. | +| `loading` | `boolean` | `false` | Whether to show loading state. | +| `error` | `string` | `undefined` | Error message to display. | + +## Examples + +### Basic sign-up with email/password + +```jsx +import { SignUpPage } from '@toolpad/core'; + +export default function SignUp() { + const handleSignUp = async (data) => { + try { + await createUser(data); + // Handle successful sign-up + } catch (error) { + // Handle error + } + }; + + return ; +} +``` + +### With OAuth providers + +```jsx +import { SignUpPage } from '@toolpad/core'; + +export default function SignUp() { + return ( + + ); +} +``` + +### Custom styling + +```jsx +import { SignUpPage } from '@toolpad/core'; + +const customTheme = { + colors: { + primary: '#1976d2', + background: '#f5f5f5', + }, + borderRadius: '8px', +}; + +export default function SignUp() { + return ( + } + /> + ); +} +``` + +## Form Fields + +The default sign-up form includes the following fields: + +- Email (required) +- Password (required) +- Confirm Password (required) +- Name (optional) + +## TypeScript + +The component includes full TypeScript support. Here are the main types you'll work with: + +```ts +interface SignUpData { + email: string; + password: string; + name?: string; +} + +interface SignUpPageProps { + onSubmit: (data: SignUpData) => void; + providers?: string[]; + title?: string; + logo?: ReactNode; + theme?: Theme; + redirectUrl?: string; + loading?: boolean; + error?: string; +} +``` + +## Customization + +### Custom Form Fields + +You can extend the default form fields using the `fields` prop: + +```jsx +const customFields = [ + { + name: 'company', + label: 'Company Name', + type: 'text', + required: true, + }, + { + name: 'role', + label: 'Job Role', + type: 'select', + options: ['Developer', 'Designer', 'Manager'], + }, +]; + +; +``` + +### Custom Validation + +The component uses Yup for form validation. You can provide custom validation rules: + +```jsx +const validationSchema = { + email: (value) => /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value), + password: (value) => value.length >= 8, +}; + +; +``` + +## Best Practices + +1. Always handle errors gracefully and display meaningful error messages to users. +2. Implement proper security measures on your backend to validate and sanitize user input. +3. Consider implementing rate limiting to prevent abuse. +4. Use HTTPS to secure data transmission. +5. Follow accessibility guidelines by maintaining proper contrast and keyboard navigation. + +## Related Components + +- [`SignInPage`](/toolpad/core/react-sign-in-page/) - For user login +- [`PasswordResetPage`](/toolpad/core/react-password-reset-page/) - For password recovery +- [`AuthProvider`](/toolpad/core/react-auth-provider/) - For managing authentication state + +## API Reference + +For a complete list of props and methods, see the [API Reference](/toolpad/core/api-reference/#sign-up-page). diff --git a/docs/data/toolpad/core/pagesApi.js b/docs/data/toolpad/core/pagesApi.js index c36b8e189db..3dc7561491e 100644 --- a/docs/data/toolpad/core/pagesApi.js +++ b/docs/data/toolpad/core/pagesApi.js @@ -13,6 +13,7 @@ module.exports = [ { pathname: '/toolpad/core/api/sign-in-button' }, { pathname: '/toolpad/core/api/sign-in-page' }, { pathname: '/toolpad/core/api/sign-out-button' }, + { pathname: '/toolpad/core/api/sign-up-page' }, { pathname: '/toolpad/core/api/theme-switcher' }, { pathname: '/toolpad/core/api/toolbar-actions' }, ]; 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..c82c4229581 --- /dev/null +++ b/docs/pages/toolpad/core/api/sign-up-page.json @@ -0,0 +1,87 @@ +{ + "props": { + "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": "{ emailField?: object, forgotPasswordLink?: object, passwordField?: object, signInLink?: object, submitButton?: object }" + }, + "default": "{}" + }, + "slots": { + "type": { + "name": "shape", + "description": "{ emailField?: elementType, passwordField?: elementType, signInLink?: elementType, submitButton?: elementType, subtitle?: elementType, 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": "submitButton", + "description": "The custom submit button component used in the credentials form.", + "default": "LoadingButton", + "class": null + }, + { + "name": "signInLink", + "description": "The custom sign in link component used in the credentials form.", + "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/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..aef663c840e --- /dev/null +++ b/docs/translations/api-docs/sign-up-page/sign-up-page.json @@ -0,0 +1,28 @@ +{ + "componentDescription": "", + "propDescriptions": { + "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 in." + } + }, + "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 SignInPage container" + } + }, + "classDescriptions": {}, + "slotDescriptions": { + "emailField": "The custom email field component used in the credentials form.", + "passwordField": "The custom password field component used in the credentials form.", + "signInLink": "The custom sign in link component used in the credentials form.", + "submitButton": "The custom submit button component used in the credentials form.", + "subtitle": "A component to override the default subtitle section", + "title": "A component to override the default title section" + } +} diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index 5a5c489d992..dcb0bd73df9 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -12,165 +12,12 @@ import FormControlLabel, { FormControlLabelProps } from '@mui/material/FormContr import TextField, { TextFieldProps } from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import LoadingButton, { LoadingButtonProps } from '@mui/lab/LoadingButton'; -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 { alpha, useTheme, SxProps } 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'; - -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, - ), - }, - }, -}); - -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', ], - ['google', ], - ['facebook', ], - ['passkey', ], - ['twitter', ], - ['apple', ], - ['instagram', ], - ['tiktok', ], - ['linkedin', ], - ['slack', ], - ['spotify', ], - ['twitch', ], - ['discord', ], - ['line', ], - ['auth0', ], - ['microsoft-entra-id', ], - ['cognito', ], - ['gitlab', ], - ['keycloak', ], - ['okta', ], - ['fusionauth', ], -]); - -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. - * @default '' - */ - error?: string; - /** - * The type of error if the sign-in failed. - * @default '' - */ - type?: string; - /** - * The success notification if the sign-in was successful. - * @default '' - * Only used for magic link sign-in. - * @example 'Check your email for a magic link.' - */ - success?: string; -} +import IconProviderMap from '../shared/icons/iconProviderMap'; +import { getCommonTextFieldProps } from '../shared/utils'; +import type { AuthResponse, AuthProvider, SupportedAuthProvider } from '../auth/types'; export interface SignInPageSlots { /** diff --git a/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx new file mode 100644 index 00000000000..f3ddbebb33d --- /dev/null +++ b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx @@ -0,0 +1,546 @@ +'use client'; + +import * as React from 'react'; +import PropTypes from 'prop-types'; + +import Alert from '@mui/material/Alert'; +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 TextField, { TextFieldProps } from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import LoadingButton, { LoadingButtonProps } from '@mui/lab/LoadingButton'; +import { alpha, useTheme, SxProps } from '@mui/material/styles'; +import { LinkProps } from '@mui/material/Link'; +import { BrandingContext, RouterContext } from '../shared/context'; +import IconProviderMap from '../shared/icons/iconProviderMap'; +import { getCommonTextFieldProps } from '../shared/utils'; +import type { AuthProvider, AuthResponse, SupportedAuthProvider } from '../auth/types'; + +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 submit button component used in the credentials form. + * @default LoadingButton + */ + submitButton?: React.JSXElementConstructor; + + /** + * The custom sign in link component used in the credentials form. + * @default Link + */ + signInLink?: React.JSXElementConstructor; + /** + * 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 in. + * @returns {void|Promise} + * @default undefined + */ + signUp?: ( + provider: AuthProvider, + formData?: any, + callbackUrl?: string, + ) => void | Promise | undefined; + /** + * The components used for each slot inside. + * @default {} + * @example { signInLink: Sign In } + */ + slots?: SignUpPageSlots; + /** + * The props used for each slot inside. + * @default {} + * @example { emailField: { autoFocus: false } } + * @example { passwordField: { variant: 'outlined' } } + * @example { emailField: { autoFocus: false }, passwordField: { variant: 'outlined' } } + */ + slotProps?: { + emailField?: TextFieldProps; + passwordField?: TextFieldProps; + submitButton?: LoadingButtonProps; + forgotPasswordLink?: LinkProps; + signInLink?: LinkProps; + }; + /** + * The prop used to customize the styles on the `SignInPage` container + */ + sx?: SxProps; +} +/** + * + * 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 } = props; + const theme = useTheme(); + const branding = React.useContext(BrandingContext); + const router = React.useContext(RouterContext); + const passkeyProvider = providers?.find((provider) => provider.id === 'passkey'); + const credentialsProvider = providers?.find((provider) => provider.id === 'credentials'); + const emailProvider = providers?.find( + (provider) => provider.id === 'nodemailer' || provider.id === 'email', + ); + 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 !== 'email' && + provider !== 'passkey', + [], + ); + + return ( + + + + {branding?.logo} + + {slots?.title ? ( + + ) : ( + + Sign up {branding?.title ? `to ${branding.title}` : null} + + )} + {slots?.subtitle ? ( + + ) : ( + + Welcome, please sign up to continue + + )} + + + {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 = await signUp?.(provider, undefined, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: oauthResponse?.error ? false : prev.loading, + error: oauthResponse?.error, + })); + }} + > + + Sign up with {provider.name} + +
+ ); + })} +
+ + {passkeyProvider ? ( + + {singleProvider ? null : or} + {error && selectedProviderId === 'passkey' ? ( + + {error} + + ) : null} + { + setFormStatus({ + error: '', + selectedProviderId: passkeyProvider.id, + loading: true, + }); + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const passkeyResponse = await signUp?.(passkeyProvider, formData, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: passkeyResponse?.error, + })); + }} + > + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.submitButton ? ( + + ) : ( + + Sign up with {passkeyProvider.name || 'Passkey'} + + )} + + + ) : null} + + {emailProvider ? ( + + {singleProvider ? null : or} + {error && + (selectedProviderId === 'nodemailer' || selectedProviderId === 'email') ? ( + + {error} + + ) : null} + {success && + (selectedProviderId === 'nodemailer' || selectedProviderId === 'email') ? ( + + {success} + + ) : null} + { + event.preventDefault(); + setFormStatus({ + error: '', + selectedProviderId: emailProvider.id, + loading: true, + }); + const formData = new FormData(event.currentTarget); + const emailResponse = await signUp?.(emailProvider, formData, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: emailResponse?.error, + success: emailResponse?.success, + })); + }} + > + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.submitButton ? ( + + ) : ( + + Sign up with Email + + )} + + + ) : null} + + {credentialsProvider ? ( + + {singleProvider ? null : or} + {error && selectedProviderId === 'credentials' ? ( + + {error} + + ) : null} + { + setFormStatus({ + error: '', + selectedProviderId: credentialsProvider.id, + loading: true, + }); + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const credentialsResponse = await signUp?.( + credentialsProvider, + formData, + callbackUrl, + ); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: credentialsResponse?.error, + })); + }} + > + + {slots?.emailField ? ( + + ) : ( + + )} + {slots?.passwordField ? ( + + ) : ( + + )} + + {slots?.submitButton ? ( + + ) : ( + + Sign up + + )} + + {slots?.signInLink ? ( + + {slots?.signInLink ? : null} + + ) : null} + + + ) : 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 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 in. + * @returns {void|Promise} + * @default undefined + */ + signUp: PropTypes.func, + /** + * The props used for each slot inside. + * @default {} + * @example { emailField: { autoFocus: false } } + * @example { passwordField: { variant: 'outlined' } } + * @example { emailField: { autoFocus: false }, passwordField: { variant: 'outlined' } } + */ + slotProps: PropTypes.shape({ + emailField: PropTypes.object, + forgotPasswordLink: PropTypes.object, + passwordField: PropTypes.object, + signInLink: PropTypes.object, + submitButton: PropTypes.object, + }), + /** + * The components used for each slot inside. + * @default {} + * @example { signInLink: Sign In } + */ + slots: PropTypes.shape({ + emailField: PropTypes.elementType, + passwordField: PropTypes.elementType, + signInLink: PropTypes.elementType, + submitButton: PropTypes.elementType, + subtitle: PropTypes.elementType, + title: PropTypes.elementType, + }), + /** + * The prop used to customize the styles on the `SignInPage` 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/SignUpPage/index.ts b/packages/toolpad-core/src/SignUpPage/index.ts new file mode 100644 index 00000000000..b956d42fa23 --- /dev/null +++ b/packages/toolpad-core/src/SignUpPage/index.ts @@ -0,0 +1 @@ +export * from './SignUpPage'; diff --git a/packages/toolpad-core/src/auth/index.ts b/packages/toolpad-core/src/auth/index.ts new file mode 100644 index 00000000000..fcb073fefcd --- /dev/null +++ b/packages/toolpad-core/src/auth/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/packages/toolpad-core/src/auth/types.ts b/packages/toolpad-core/src/auth/types.ts new file mode 100644 index 00000000000..ca293fa0c64 --- /dev/null +++ b/packages/toolpad-core/src/auth/types.ts @@ -0,0 +1,65 @@ +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 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. + * @default '' + */ + error?: string; + /** + * The type of error if the sign-in failed. + * @default '' + */ + type?: string; + /** + * The success notification if the sign-in was successful. + * @default '' + * Only used for magic link sign-in. + * @example 'Check your email for a magic link.' + */ + success?: string; +} + +export type SupportedAuthProvider = + | SupportedOAuthProvider + | 'credentials' + | 'passkey' + | 'nodemailer' + | string; diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts index 123c479d6c3..fc7511225ae 100644 --- a/packages/toolpad-core/src/index.ts +++ b/packages/toolpad-core/src/index.ts @@ -4,6 +4,10 @@ export * from './DashboardLayout'; export * from './SignInPage'; +export * from './SignUpPage'; + +export * from './auth'; + export * from './Account'; export * from './PageContainer'; 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/packages/toolpad-core/src/shared/icons/iconProviderMap.tsx b/packages/toolpad-core/src/shared/icons/iconProviderMap.tsx new file mode 100644 index 00000000000..63f015cc79c --- /dev/null +++ b/packages/toolpad-core/src/shared/icons/iconProviderMap.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +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 type { SupportedAuthProvider } from '../../auth'; +import GoogleIcon from './Google'; +import FacebookIcon from './Facebook'; +import TwitterIcon from './Twitter'; +import InstagramIcon from './Instagram'; +import TikTokIcon from './TikTok'; +import LinkedInIcon from './LinkedIn'; +import SlackIcon from './Slack'; +import SpotifyIcon from './Spotify'; +import TwitchIcon from './Twitch'; +import DiscordIcon from './Discord'; +import LineIcon from './Line'; +import Auth0Icon from './Auth0'; +import MicrosoftEntraIdIcon from './MicrosoftEntra'; +import CognitoIcon from './Cognito'; +import GitLabIcon from './GitLab'; +import KeycloakIcon from './Keycloak'; +import OktaIcon from './Okta'; +import FusionAuthIcon from './FusionAuth'; + +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', ], +]); + +export default IconProviderMap; diff --git a/packages/toolpad-core/src/shared/utils/index.ts b/packages/toolpad-core/src/shared/utils/index.ts new file mode 100644 index 00000000000..ba0c2e9b54f --- /dev/null +++ b/packages/toolpad-core/src/shared/utils/index.ts @@ -0,0 +1,47 @@ +import type { SxProps, Theme } from '@mui/material/styles'; +import type { TextFieldProps } from '@mui/material/TextField'; + +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, + ), + }, + }, +}); + +export { getCommonTextFieldProps }; diff --git a/playground/nextjs/next-env.d.ts b/playground/nextjs/next-env.d.ts index 40c3d68096c..1b3be0840f3 100644 --- a/playground/nextjs/next-env.d.ts +++ b/playground/nextjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/playground/vite/package.json b/playground/vite/package.json index d6d9d6af4d5..49766243852 100644 --- a/playground/vite/package.json +++ b/playground/vite/package.json @@ -8,6 +8,7 @@ "preview": "vite preview" }, "devDependencies": { + "firebase": "^11", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@mui/icons-material": "6.2.1", diff --git a/playground/vite/src/App.tsx b/playground/vite/src/App.tsx index 2c6147c8788..617a1d0b0c6 100644 --- a/playground/vite/src/App.tsx +++ b/playground/vite/src/App.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import DashboardIcon from '@mui/icons-material/Dashboard'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Outlet } from 'react-router-dom'; -import { AppProvider, type Navigation } from '@toolpad/core/react-router-dom'; +import type { User } from 'firebase/auth'; +import { AppProvider } from '@toolpad/core/react-router-dom'; +import type { Navigation, Authentication } from '@toolpad/core/AppProvider'; +import { firebaseSignOut, signInWithGoogle, onAuthStateChanged } from './firebase/auth'; +import SessionContext, { type Session } from './SessionContext'; const NAVIGATION: Navigation = [ { @@ -24,10 +28,54 @@ const BRANDING = { title: 'My Toolpad Core App', }; +const AUTHENTICATION: Authentication = { + signIn: signInWithGoogle, + signOut: firebaseSignOut, +}; + export default function App() { + const [session, setSession] = React.useState(null); + const [loading, setLoading] = React.useState(true); + + const sessionContextValue = React.useMemo( + () => ({ + session, + setSession, + loading, + }), + [session, loading], + ); + + React.useEffect(() => { + // Returns an `unsubscribe` function to be called during teardown + const unsubscribe = onAuthStateChanged((user: User | null) => { + if (user) { + setSession({ + user: { + name: user.displayName || '', + email: user.email || '', + image: user.photoURL || '', + }, + }); + } else { + setSession(null); + } + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + return ( - - + + + + ); } diff --git a/playground/vite/src/SessionContext.tsx b/playground/vite/src/SessionContext.tsx new file mode 100644 index 00000000000..bf7f187abfa --- /dev/null +++ b/playground/vite/src/SessionContext.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +export interface Session { + user: { + name?: string; + email?: string; + image?: string; + }; +} + +interface SessionContextType { + session: Session | null; + setSession: (session: Session) => void; + loading: boolean; +} + +const SessionContext = React.createContext({ + session: null, + setSession: () => {}, + loading: true, +}); + +export default SessionContext; + +export const useSession = () => React.useContext(SessionContext); diff --git a/playground/vite/src/firebase/auth.ts b/playground/vite/src/firebase/auth.ts new file mode 100644 index 00000000000..3941b2f099d --- /dev/null +++ b/playground/vite/src/firebase/auth.ts @@ -0,0 +1,191 @@ +import { + GoogleAuthProvider, + GithubAuthProvider, + signInWithPopup, + setPersistence, + browserSessionPersistence, + signInWithEmailAndPassword, + createUserWithEmailAndPassword, + sendSignInLinkToEmail, + signInWithEmailLink, + isSignInWithEmailLink, + getAdditionalUserInfo, + signOut, +} from 'firebase/auth'; +import { firebaseAuth } from './firebaseConfig'; + +const googleProvider = new GoogleAuthProvider(); +const githubProvider = new GithubAuthProvider(); + +// Sign in with Google functionality +export const signInWithGoogle = async () => { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const result = await signInWithPopup(firebaseAuth, googleProvider); + return { + success: true, + user: result.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +}; + +// Sign in with GitHub functionality +export const signInWithGithub = async () => { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const result = await signInWithPopup(firebaseAuth, githubProvider); + return { + success: true, + user: result.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +}; + +// Sign in with email and password + +export async function signInWithCredentials(email: string, password: string) { + try { + return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { + const userCredential = await signInWithEmailAndPassword(firebaseAuth, email, password); + return { + success: true, + user: userCredential.user, + error: null, + }; + }); + } catch (error: any) { + return { + success: false, + user: null, + error: error.message || 'Failed to sign in with email/password', + }; + } +} + +// Sign up with email and password +const actionCodeSettings = { + // URL you want to redirect back to. The domain (www.example.com) for this + // URL must be in the authorized domains list in the Firebase Console. + url: 'http://localhost:5173/sign-up?partial=true&provider=email', + // This must be true. + handleCodeInApp: true, +}; + +export async function signUpWithCredentials(email: string, password: string) { + try { + const userCredential = await createUserWithEmailAndPassword(firebaseAuth, email, password); + return { + success: true, + user: userCredential.user, + error: null, + }; + } catch (error: any) { + return { + success: false, + user: null, + error: error.message || 'Failed to sign in with email/password', + }; + } +} + +// Sign up with email link +export async function signUpWithEmailLink(email: string, callbackUrl?: string) { + try { + if (callbackUrl) { + actionCodeSettings.url += new URLSearchParams({ callbackUrl }).toString(); + } + await sendSignInLinkToEmail(firebaseAuth, email, actionCodeSettings); + + // The link was successfully sent. Inform the user. + // Save the email locally so you don't need to ask the user for it again + // if they open the link on the same device. + localStorage.setItem('toolpad-firebase-signup-partial', email); + + return { + success: true, + }; + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +} + +export async function completeSignUpWithEmailLink() { + // Confirm the link is a sign-in with email link. + if (isSignInWithEmailLink(firebaseAuth, window.location.href)) { + // Additional state parameters can also be passed via URL. + // This can be used to continue the user's intended action before triggering + // the sign-in operation. + // Get the email if available. This should be available if the user completes + // the flow on the same device where they started it. + const email = window.localStorage.getItem('toolpad-firebase-signup-partial'); + + // The client SDK will parse the code from the link for you. + try { + if (email) { + const result = await signInWithEmailLink(firebaseAuth, email, window.location.href); + + // Clear email from storage. + window.localStorage.removeItem('emailForSignIn'); + // You can access the new user by importing getAdditionalUserInfo + // and calling it with result: + const user = getAdditionalUserInfo(result); + return { + success: true, + user, + error: null, + }; + } + return { + error: "Error accessing the user's email address for sign up verification", + success: false, + user: null, + }; + } catch (error: any) { + // Some error occurred, you can inspect the code: error.code + // Common errors could be invalid email and invalid or expired OTPs. + return { + success: false, + error: error.message, + user: null, + }; + } + } + return {}; +} +// Sign out functionality +export const firebaseSignOut = async () => { + try { + await signOut(firebaseAuth); + return { success: true }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } +}; + +// Auth state observer +export const onAuthStateChanged = (callback: (user: any) => void) => { + return firebaseAuth.onAuthStateChanged(callback); +}; diff --git a/playground/vite/src/firebase/firebaseConfig.ts b/playground/vite/src/firebase/firebaseConfig.ts new file mode 100644 index 00000000000..bc753697b56 --- /dev/null +++ b/playground/vite/src/firebase/firebaseConfig.ts @@ -0,0 +1,14 @@ +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; + +const app = initializeApp({ + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGE_SENDER_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID, +}); + +export const firebaseAuth = getAuth(app); +export default app; diff --git a/playground/vite/src/layouts/dashboard.tsx b/playground/vite/src/layouts/dashboard.tsx index c540feb6e0f..4649de63084 100644 --- a/playground/vite/src/layouts/dashboard.tsx +++ b/playground/vite/src/layouts/dashboard.tsx @@ -1,11 +1,42 @@ import * as React from 'react'; -import { Outlet } from 'react-router-dom'; +import LinearProgress from '@mui/material/LinearProgress'; +import { Outlet, Navigate, useLocation } from 'react-router-dom'; import { DashboardLayout } from '@toolpad/core/DashboardLayout'; import { PageContainer } from '@toolpad/core/PageContainer'; +import { Account } from '@toolpad/core/Account'; +import { useSession } from '../SessionContext'; + +function CustomAccount() { + return ( + + ); +} export default function Layout() { + const { session, loading } = useSession(); + const location = useLocation(); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!session) { + // Add the `callbackUrl` search parameter + const redirectTo = `/sign-in?callbackUrl=${encodeURIComponent(location.pathname)}`; + + return ; + } + return ( - + diff --git a/playground/vite/src/main.tsx b/playground/vite/src/main.tsx index 882c91c0bff..75cbb8f87ae 100644 --- a/playground/vite/src/main.tsx +++ b/playground/vite/src/main.tsx @@ -5,6 +5,8 @@ import App from './App'; import Layout from './layouts/dashboard'; import DashboardPage from './pages'; import OrdersPage from './pages/orders'; +import SignInPage from './pages/signin'; +import SignUpPage from './pages/signup'; const router = createBrowserRouter([ { @@ -24,6 +26,14 @@ const router = createBrowserRouter([ }, ], }, + { + path: '/sign-in', + Component: SignInPage, + }, + { + path: '/sign-up', + Component: SignUpPage, + }, ], }, ]); diff --git a/playground/vite/src/pages/signin.tsx b/playground/vite/src/pages/signin.tsx new file mode 100644 index 00000000000..9d165afc558 --- /dev/null +++ b/playground/vite/src/pages/signin.tsx @@ -0,0 +1,93 @@ +'use client'; +import * as React from 'react'; +import Alert from '@mui/material/Alert'; + +import Link from '@mui/material/Link'; +import LinearProgress from '@mui/material/LinearProgress'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { useSession, type Session } from '../SessionContext'; +import { signInWithGoogle, signInWithGithub, signInWithCredentials } from '../firebase/auth'; + +function DemoInfo() { + return ( + + You can use toolpad-demo@mui.com with the password @demo1 to + test + + ); +} + +function SignUpLink() { + return Sign Up; +} + +export default function SignIn() { + const { session, setSession, loading } = useSession(); + const navigate = useNavigate(); + + if (loading) { + return ; + } + + if (session) { + return ; + } + + return ( + { + let result; + try { + if (provider.id === 'google') { + result = await signInWithGoogle(); + } + if (provider.id === 'github') { + result = await signInWithGithub(); + } + if (provider.id === 'credentials') { + const email = formData?.get('email') as string; + const password = formData?.get('password') as string; + + if (!email || !password) { + return { error: 'Email and password are required' }; + } + + result = await signInWithCredentials(email, password); + } + + if (result?.success && result?.user) { + // Convert Firebase user to Session format + const userSession: Session = { + user: { + name: result.user.displayName || '', + email: result.user.email || '', + image: result.user.photoURL || '', + }, + }; + setSession(userSession); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + return { error: result?.error || 'Failed to sign in' }; + } catch (error) { + return { error: error instanceof Error ? error.message : 'An error occurred' }; + } + }} + slots={{ subtitle: DemoInfo, signUpLink: SignUpLink }} + slotProps={{ + emailField: { + defaultValue: 'toolpad-demo@mui.com', + }, + passwordField: { + defaultValue: '@demo1', + }, + }} + /> + ); +} diff --git a/playground/vite/src/pages/signup.tsx b/playground/vite/src/pages/signup.tsx new file mode 100644 index 00000000000..90a5fa4424e --- /dev/null +++ b/playground/vite/src/pages/signup.tsx @@ -0,0 +1,108 @@ +'use client'; +import * as React from 'react'; + +import Link from '@mui/material/Link'; +import LinearProgress from '@mui/material/LinearProgress'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import { Navigate, useNavigate, useLocation } from 'react-router-dom'; +import { useSession, type Session } from '../SessionContext'; +import { + signUpWithCredentials, + signUpWithEmailLink, + signInWithGoogle, + signInWithGithub, + completeSignUpWithEmailLink, +} from '../firebase/auth'; + +function SignInLink() { + return Sign In; +} + +export default function SignUp() { + const { session, setSession, loading } = useSession(); + const [completing, setCompleting] = React.useState(false); + const navigate = useNavigate(); + const location = useLocation(); + + React.useEffect(() => { + async function completeSignUp() { + if (!loading && !session && location.search) { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get('provider') === 'email' && searchParams.get('partial') === 'true') { + setCompleting(true); + await completeSignUpWithEmailLink(); + setCompleting(false); + } + } + } + completeSignUp(); + }, [loading, session, location]); + + if (loading || completing) { + return ; + } + + if (session) { + return ; + } + + return ( + { + let result; + try { + if (provider.id === 'google') { + result = await signInWithGoogle(); + } + if (provider.id === 'github') { + result = await signInWithGithub(); + } + if (provider.id === 'credentials') { + const email = formData?.get('email') as string; + const password = formData?.get('password') as string; + + if (!email || !password) { + return { error: 'Email and password are required' }; + } + + result = await signUpWithCredentials(email, password); + } + if (provider.id === 'email') { + const email = formData?.get('email') as string; + if (!email) { + return { error: 'Email is required' }; + } + result = await signUpWithEmailLink(email); + if (result.success) { + return { success: 'Check your inbox for a verification link.' }; + } + } + + if (result?.success && result?.user) { + // Convert Firebase user to Session format + const userSession: Session = { + user: { + name: result.user.displayName || '', + email: result.user.email || '', + image: result.user.photoURL || '', + }, + }; + setSession(userSession); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + return { error: result?.error || 'Failed to sign in' }; + } catch (error) { + return { error: error instanceof Error ? error.message : 'An error occurred' }; + } + }} + slots={{ signInLink: SignInLink }} + /> + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73c24e6388a..1b6775ace81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1309,6 +1309,9 @@ importers: '@vitejs/plugin-react': specifier: 4.3.4 version: 4.3.4(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0)) + firebase: + specifier: ^11 + version: 11.1.0 react: specifier: 18.3.1 version: 18.3.1 @@ -2675,6 +2678,216 @@ packages: '@fast-csv/parse@4.3.6': resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@firebase/analytics-compat@0.2.16': + resolution: {integrity: sha512-Q/s+u/TEMSb2EDJFQMGsOzpSosybBl8HuoSEMyGZ99+0Pu7SIR9MPDGUjc8PKiCFQWDJ3QXxgqh1d/rujyAMbA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.3': + resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} + + '@firebase/analytics@0.10.10': + resolution: {integrity: sha512-Psdo7c9g2SLAYh6u1XRA+RZ7ab2JfBVuAt/kLzXkhKZL/gS2cQUCMsOW5p0RIlDPRKqpdNSmvujd2TeRWLKOkQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.3.17': + resolution: {integrity: sha512-a/eadrGsY0MVCBPhrNbKUhoYpms4UKTYLKO7nswwSFVsm3Rw6NslQQCNLfvljcDqP4E7alQDRGJXjkxd/5gJ+Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-check-types@0.5.3': + resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} + + '@firebase/app-check@0.8.10': + resolution: {integrity: sha512-DWFfxxif/t+Ow4MmRUevDX+A3hVxm1rUf6y5ZP4sIomfnVCO1NNahqtsv9rb1/tKGkTeoVT40weiTS/WjQG1mA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.2.47': + resolution: {integrity: sha512-TdEWGDp6kSwuO1mxiM2Fe39eLWygfyzqTZcoU3aPV0viqqphPCbBBnVjPbFJErZ4+yaS7uCWXEbFEP9m5/COKA==} + engines: {node: '>=18.0.0'} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/app@0.10.17': + resolution: {integrity: sha512-53sIYyAnYEPIZdaxuyq5OST7j4KBc2pqmktz+tEb1BIUSbXh8Gp4k/o6qzLelLpm4ngrBz7SRN0PZJqNRAyPog==} + engines: {node: '>=18.0.0'} + + '@firebase/auth-compat@0.5.16': + resolution: {integrity: sha512-YlYwJMBqAyv0ESy3jDUyshMhZlbUiwAm6B6+uUmigNDHU+uq7j4SFiDJEZlFFIz397yBzKn06SUdqutdQzGnCA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/auth-types@0.12.3': + resolution: {integrity: sha512-Zq9zI0o5hqXDtKg6yDkSnvMCMuLU6qAVS51PANQx+ZZX5xnzyNLEBO3GZgBUPsV5qIMFhjhqmLDxUqCbnAYy2A==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.8.1': + resolution: {integrity: sha512-LX9N/Cf5Z35r5yqm2+5M3+2bRRe/+RFaa/+u4HDni7TA27C/Xm4XHLKcWcLg1BzjrS4zngSaBEOSODvp6RFOqQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@react-native-async-storage/async-storage': ^1.18.1 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@firebase/component@0.6.11': + resolution: {integrity: sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==} + engines: {node: '>=18.0.0'} + + '@firebase/data-connect@0.1.3': + resolution: {integrity: sha512-FbAQpWNHownJx1VTCQI4ydbWGOZmSWXoFlirQn3ItHqsLJYSywqxSgDafzvyooifFh3J/2WqaM8y9hInnPcsTw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.0.1': + resolution: {integrity: sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==} + engines: {node: '>=18.0.0'} + + '@firebase/database-types@1.0.7': + resolution: {integrity: sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==} + + '@firebase/database@1.0.10': + resolution: {integrity: sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==} + engines: {node: '>=18.0.0'} + + '@firebase/firestore-compat@0.3.40': + resolution: {integrity: sha512-18HopMN811KYBc9Ptpr1Rewwio0XF09FF3jc5wtV6rGyAs815SlFFw5vW7ZeLd43zv9tlEc2FzM0H+5Vr9ZRxw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.3': + resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.7.5': + resolution: {integrity: sha512-OO3rHvjC07jL2ITN255xH/UzCVSvh6xG8oTzQdFScQvFbcm1fjCL1hgAdpDZcx3vVcKMV+6ktr8wbllkB8r+FQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.3.17': + resolution: {integrity: sha512-oj2XV8YsJYutyPCRYUfbN6swmfrL6zar0/qtqZsKT7P7btOiYRl+lD6fxtQaT+pKE5YgOBGZW//kLPZfY0jWhw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.3': + resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} + + '@firebase/functions@0.12.0': + resolution: {integrity: sha512-plTtzY/nT0jOgHzT0vB9qch4FpHFOhCnR8HhYBqqdArG6GOQMIruKZbiTyLybO8bcaaNgQ6kSm9yohGUwxHcIw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.11': + resolution: {integrity: sha512-SHRgw5LTa6v8LubmJZxcOCwEd1MfWQPUtKdiuCx2VMWnapX54skZd1PkQg0K4l3k+4ujbI2cn7FE6Li9hbChBw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.3': + resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.11': + resolution: {integrity: sha512-w8fY8mw6fxJzsZM2ufmTtomopXl1+bn/syYon+Gpn+0p0nO1cIUEVEFrFazTLaaL9q1CaVhc3HmseRTsI3igAA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.4.4': + resolution: {integrity: sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==} + engines: {node: '>=18.0.0'} + + '@firebase/messaging-compat@0.2.15': + resolution: {integrity: sha512-mEKKASRvRWq1aBNHgioGsOYR2c5nBZpO7k90K794zjKe0WkGNf0k7PLs5SlCf8FKnzumEkhTAp/SjYxovuxa8A==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.3': + resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} + + '@firebase/messaging@0.12.15': + resolution: {integrity: sha512-Bz+qvWNEwEWAbYtG4An8hgcNco6NWNoNLuLbGVwPL2fAoCF1zz+dcaBp+iTR2+K199JyRyDT9yDPAXhNHNDaKQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.11': + resolution: {integrity: sha512-DqeNBy51W2xzlklyC7Ht9JQ94HhTA08PCcM4MDeyG/ol3fqum/+YgtHWQ2IQuduqH9afETthZqLwCZiSgY7hiA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.3': + resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} + + '@firebase/performance@0.6.11': + resolution: {integrity: sha512-FlkJFeqLlIeh5T4Am3uE38HVzggliDIEFy/fErEc1faINOUFCb6vQBEoNZGaXvRnTR8lh3X/hP7tv37C7BsK9g==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.11': + resolution: {integrity: sha512-zfIjpwPrGuIOZDmduukN086qjhZ1LnbJi/iYzgua+2qeTlO0XdlE1v66gJPwygGB3TOhT0yb9EiUZ3nBNttMqg==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.3.3': + resolution: {integrity: sha512-YlRI9CHxrk3lpQuFup9N1eohpwdWayKZUNZ/YeQ0PZoncJ66P32UsKUKqVXOaieTjJIOh7yH8JEzRdht5s+d6g==} + + '@firebase/remote-config@0.4.11': + resolution: {integrity: sha512-9z0rgKuws2nj+7cdiqF+NY1QR4na6KnuOvP+jQvgilDOhGtKOcCMq5XHiu66i73A9kFhyU6QQ2pHXxcmaq1pBw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.3.14': + resolution: {integrity: sha512-Ok5FmXJiapaNAOQ8W8qppnfwgP8540jw2B8M0c4TFZqF4BD+CoKBxW0dRtOuLNGadLhzqqkDZZZtkexxrveQqA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.3': + resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.13.4': + resolution: {integrity: sha512-b1KaTTRiMupFurIhpGIbReaWev0k5O3ouTHkAPcEssT+FvU3q/1JwzvkX4+ZdB60Fc43Mbp8qQ1gWfT0Z2FP9Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.10.2': + resolution: {integrity: sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==} + engines: {node: '>=18.0.0'} + + '@firebase/vertexai@1.0.2': + resolution: {integrity: sha512-4dC9m2nD0tkfKJT5v+i27tELrmUePjFXW3CDAxhVHUEv647B2R7kqpGQnyPkNEeaXkCr76THe7GGg35EWn4lDw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/webchannel-wrapper@1.0.3': + resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} + '@floating-ui/core@1.6.8': resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} @@ -2698,6 +2911,15 @@ packages: resolution: {integrity: sha512-nPgzOiDs/FSFhE+dX2KfkmsmkXM3WfXYP06FoW8cXvHshwxHSI3FbXwe5XJYstDAWXP9YA7AMSvmwnuD4OAl2w==} engines: {node: '>=12.0.0'} + '@grpc/grpc-js@1.9.15': + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.7.13': + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -3853,6 +4075,36 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@react-spring/animated@9.7.5': resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==} peerDependencies: @@ -6261,6 +6513,10 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fdir@6.4.2: resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} peerDependencies: @@ -6338,6 +6594,9 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} + firebase@11.1.0: + resolution: {integrity: sha512-3OoNW3vBXmBLYJvcwbPCwfluptbDVp2zZYjrfHPVFAXfPgmyy/LWjidt+Sw2WNvRelsG0v++WN2Wor6J3OwDRg==} + flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6740,6 +6999,9 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -6787,6 +7049,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -8820,6 +9085,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + protocols@2.0.1: resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} @@ -10330,6 +10599,14 @@ packages: webpack-cli: optional: true + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -11927,6 +12204,323 @@ snapshots: lodash.isundefined: 3.0.1 lodash.uniq: 4.5.0 + '@firebase/analytics-compat@0.2.16(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17)': + dependencies: + '@firebase/analytics': 0.10.10(@firebase/app@0.10.17) + '@firebase/analytics-types': 0.8.3 + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/analytics-types@0.8.3': {} + + '@firebase/analytics@0.10.10(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/installations': 0.6.11(@firebase/app@0.10.17) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.3.17(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-check': 0.8.10(@firebase/app@0.10.17) + '@firebase/app-check-types': 0.5.3 + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-check-types@0.5.3': {} + + '@firebase/app-check@0.8.10(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/app-compat@0.2.47': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/app-types@0.9.3': {} + + '@firebase/app@0.10.17': + dependencies: + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.5.16(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/auth': 1.8.1(@firebase/app@0.10.17) + '@firebase/auth-types': 0.12.3(@firebase/app-types@0.9.3)(@firebase/util@1.10.2) + '@firebase/component': 0.6.11 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/auth-types@0.12.3(@firebase/app-types@0.9.3)(@firebase/util@1.10.2)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.10.2 + + '@firebase/auth@1.8.1(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/component@0.6.11': + dependencies: + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/data-connect@0.1.3(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/database-compat@2.0.1': + dependencies: + '@firebase/component': 0.6.11 + '@firebase/database': 1.0.10 + '@firebase/database-types': 1.0.7 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/database-types@1.0.7': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.10.2 + + '@firebase/database@1.0.10': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.3.40(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/firestore': 4.7.5(@firebase/app@0.10.17) + '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.10.2) + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.10.2)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.10.2 + + '@firebase/firestore@4.7.5(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + '@firebase/webchannel-wrapper': 1.0.3 + '@grpc/grpc-js': 1.9.15 + '@grpc/proto-loader': 0.7.13 + tslib: 2.8.1 + + '@firebase/functions-compat@0.3.17(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/functions': 0.12.0(@firebase/app@0.10.17) + '@firebase/functions-types': 0.6.3 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/functions-types@0.6.3': {} + + '@firebase/functions@0.12.0(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.11 + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.11(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/installations': 0.6.11(@firebase/app@0.10.17) + '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': + dependencies: + '@firebase/app-types': 0.9.3 + + '@firebase/installations@0.6.11(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/util': 1.10.2 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.4.4': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.15(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/messaging': 0.12.15(@firebase/app@0.10.17) + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.3': {} + + '@firebase/messaging@0.12.15(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/installations': 0.6.11(@firebase/app@0.10.17) + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.10.2 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.11(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/performance': 0.6.11(@firebase/app@0.10.17) + '@firebase/performance-types': 0.2.3 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.3': {} + + '@firebase/performance@0.6.11(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/installations': 0.6.11(@firebase/app@0.10.17) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/remote-config-compat@0.2.11(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/remote-config': 0.4.11(@firebase/app@0.10.17) + '@firebase/remote-config-types': 0.3.3 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.3.3': {} + + '@firebase/remote-config@0.4.11(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/installations': 0.6.11(@firebase/app@0.10.17) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/storage-compat@0.3.14(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app-compat': 0.2.47 + '@firebase/component': 0.6.11 + '@firebase/storage': 0.13.4(@firebase/app@0.10.17) + '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.10.2) + '@firebase/util': 1.10.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.10.2)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.10.2 + + '@firebase/storage@0.13.4(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/component': 0.6.11 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/util@1.10.2': + dependencies: + tslib: 2.8.1 + + '@firebase/vertexai@1.0.2(@firebase/app-types@0.9.3)(@firebase/app@0.10.17)': + dependencies: + '@firebase/app': 0.10.17 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/app-types': 0.9.3 + '@firebase/component': 0.6.11 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.10.2 + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.3': {} + '@floating-ui/core@1.6.8': dependencies: '@floating-ui/utils': 0.2.8 @@ -11958,6 +12552,18 @@ snapshots: - encoding - supports-color + '@grpc/grpc-js@1.9.15': + dependencies: + '@grpc/proto-loader': 0.7.13 + '@types/node': 20.17.6 + + '@grpc/proto-loader@0.7.13': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.4.0 + yargs: 17.7.2 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -13257,6 +13863,29 @@ snapshots: '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@react-spring/animated@9.7.5(react@18.3.1)': dependencies: '@react-spring/shared': 9.7.5(react@18.3.1) @@ -16279,6 +16908,10 @@ snapshots: dependencies: reusify: 1.0.4 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fdir@6.4.2(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -16373,6 +17006,39 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 + firebase@11.1.0: + dependencies: + '@firebase/analytics': 0.10.10(@firebase/app@0.10.17) + '@firebase/analytics-compat': 0.2.16(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17) + '@firebase/app': 0.10.17 + '@firebase/app-check': 0.8.10(@firebase/app@0.10.17) + '@firebase/app-check-compat': 0.3.17(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17) + '@firebase/app-compat': 0.2.47 + '@firebase/app-types': 0.9.3 + '@firebase/auth': 1.8.1(@firebase/app@0.10.17) + '@firebase/auth-compat': 0.5.16(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17) + '@firebase/data-connect': 0.1.3(@firebase/app@0.10.17) + '@firebase/database': 1.0.10 + '@firebase/database-compat': 2.0.1 + '@firebase/firestore': 4.7.5(@firebase/app@0.10.17) + '@firebase/firestore-compat': 0.3.40(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17) + '@firebase/functions': 0.12.0(@firebase/app@0.10.17) + '@firebase/functions-compat': 0.3.17(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17) + '@firebase/installations': 0.6.11(@firebase/app@0.10.17) + '@firebase/installations-compat': 0.2.11(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17) + '@firebase/messaging': 0.12.15(@firebase/app@0.10.17) + '@firebase/messaging-compat': 0.2.15(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17) + '@firebase/performance': 0.6.11(@firebase/app@0.10.17) + '@firebase/performance-compat': 0.2.11(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17) + '@firebase/remote-config': 0.4.11(@firebase/app@0.10.17) + '@firebase/remote-config-compat': 0.2.11(@firebase/app-compat@0.2.47)(@firebase/app@0.10.17) + '@firebase/storage': 0.13.4(@firebase/app@0.10.17) + '@firebase/storage-compat': 0.3.14(@firebase/app-compat@0.2.47)(@firebase/app-types@0.9.3)(@firebase/app@0.10.17) + '@firebase/util': 1.10.2 + '@firebase/vertexai': 1.0.2(@firebase/app-types@0.9.3)(@firebase/app@0.10.17) + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + flat-cache@3.2.0: dependencies: flatted: 3.3.1 @@ -16837,6 +17503,8 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-parser-js@0.5.8: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 @@ -16900,6 +17568,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@7.1.1: {} + ieee754@1.2.1: {} ignore-walk@6.0.5: @@ -19109,6 +19779,21 @@ snapshots: proto-list@1.2.4: {} + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.17.6 + long: 5.2.3 + protocols@2.0.1: {} proxy-addr@2.0.7: @@ -20860,6 +21545,14 @@ snapshots: - esbuild - uglify-js + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 From cba1e8d728748000ea7f4ff4c244ac9926918ba4 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Mon, 30 Dec 2024 17:10:54 +0530 Subject: [PATCH 2/5] wip: Move types --- .../sign-in-page/BrandingSignInPage.tsx | 3 +- .../sign-in-page/CredentialsSignInPage.tsx | 3 +- .../sign-in-page/PasskeySignInPage.tsx | 3 +- .../components/sign-in-page/sign-in-page.md | 3 +- .../core/integrations/nextjs-approuter.md | 3 +- .../components/landing/ToolpadAuthDemo.tsx | 3 +- .../src/app/auth/signin/actions.ts | 2 +- .../src/pages/auth/signin.tsx | 3 +- .../src/app/auth/signin/actions.ts | 2 +- .../src/app/auth/signin/page.tsx | 2 +- .../app/auth/signin/actions.ts | 2 +- .../auth-nextjs/src/app/auth/signin/page.tsx | 3 +- packages/create-toolpad-app/src/index.ts | 2 +- .../src/templates/auth/auth.ts | 2 +- .../src/templates/auth/nextjs-app/actions.ts | 2 +- .../templates/auth/nextjs-app/signInPage.ts | 2 +- .../src/templates/auth/utils.ts | 2 +- packages/create-toolpad-app/src/types.ts | 2 +- pnpm-lock.yaml | 126 ++++++++++++++---- 19 files changed, 127 insertions(+), 43 deletions(-) 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 61fcf280ade..e1c1d00c09b 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'credentials', name: 'Credentials' }]; 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 7ebe5d41a12..97f3ffc969e 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; import { useTheme } from '@mui/material/styles'; // preview-start 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 c69a3758ecf..03812d00af5 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage, AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; 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/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index cddc44d4daf..89319ae3244 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 @@ -157,7 +157,8 @@ 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 } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; import { AuthError } from 'next-auth'; import { providerMap, signIn } from '../../../auth'; diff --git a/docs/data/toolpad/core/integrations/nextjs-approuter.md b/docs/data/toolpad/core/integrations/nextjs-approuter.md index 46726d69424..5cd1bd1f92a 100644 --- a/docs/data/toolpad/core/integrations/nextjs-approuter.md +++ b/docs/data/toolpad/core/integrations/nextjs-approuter.md @@ -181,7 +181,8 @@ 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 } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; import { AuthError } from 'next-auth'; import { providerMap, signIn } from '../../../auth'; diff --git a/docs/src/components/landing/ToolpadAuthDemo.tsx b/docs/src/components/landing/ToolpadAuthDemo.tsx index 69d4c9e78a6..15069e1c45b 100644 --- a/docs/src/components/landing/ToolpadAuthDemo.tsx +++ b/docs/src/components/landing/ToolpadAuthDemo.tsx @@ -3,7 +3,8 @@ 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 } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; import Frame from '../../modules/components/Frame'; const NOOP = () => {}; diff --git a/examples/core/auth-nextjs-email/src/app/auth/signin/actions.ts b/examples/core/auth-nextjs-email/src/app/auth/signin/actions.ts index 7672bd32886..592eaee7557 100644 --- a/examples/core/auth-nextjs-email/src/app/auth/signin/actions.ts +++ b/examples/core/auth-nextjs-email/src/app/auth/signin/actions.ts @@ -1,6 +1,6 @@ 'use server'; import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core'; +import type { AuthProvider } from '@toolpad/core/auth'; import { signIn as signInAction } from '../../../auth'; async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { 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..97cfcac930a 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,8 @@ 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 } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; 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-passkey/src/app/auth/signin/actions.ts b/examples/core/auth-nextjs-passkey/src/app/auth/signin/actions.ts index 947c8c04907..eb85cb4a301 100644 --- a/examples/core/auth-nextjs-passkey/src/app/auth/signin/actions.ts +++ b/examples/core/auth-nextjs-passkey/src/app/auth/signin/actions.ts @@ -1,6 +1,6 @@ 'use server'; import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core'; +import type { AuthProvider } from '@toolpad/core/auth'; import { signIn as signInAction } 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..4dccd772d89 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,6 +1,6 @@ 'use client'; import * as React from 'react'; -import type { AuthProvider } from '@toolpad/core'; +import type { AuthProvider } from '@toolpad/core/auth'; import { SignInPage } from '@toolpad/core/SignInPage'; import { signIn as webauthnSignIn } from 'next-auth/webauthn'; import { providerMap } from '../../../auth'; diff --git a/examples/core/auth-nextjs-themed/app/auth/signin/actions.ts b/examples/core/auth-nextjs-themed/app/auth/signin/actions.ts index 5565a91cfa9..fbc8e4d20e6 100644 --- a/examples/core/auth-nextjs-themed/app/auth/signin/actions.ts +++ b/examples/core/auth-nextjs-themed/app/auth/signin/actions.ts @@ -1,6 +1,6 @@ 'use server'; import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core'; +import type { AuthProvider } from '@toolpad/core/auth'; import { signIn as signInAction } from '../../../auth'; async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { 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..2d198b85b9f 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,6 @@ import * as React from 'react'; -import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { AuthProvider } from '@toolpad/core/auth'; import { AuthError } from 'next-auth'; import { providerMap, signIn } from '../../../auth'; diff --git a/packages/create-toolpad-app/src/index.ts b/packages/create-toolpad-app/src/index.ts index 632570eb7ba..103ec8b929a 100644 --- a/packages/create-toolpad-app/src/index.ts +++ b/packages/create-toolpad-app/src/index.ts @@ -12,7 +12,7 @@ import { satisfies } from 'semver'; import { readJsonFile } from '@toolpad/utils/fs'; import invariant from 'invariant'; import { bashResolvePath } from '@toolpad/utils/cli'; -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/auth'; import generateProject from './generateProject'; import generateStudioProject from './generateStudioProject'; import writeFiles from './writeFiles'; diff --git a/packages/create-toolpad-app/src/templates/auth/auth.ts b/packages/create-toolpad-app/src/templates/auth/auth.ts index 421365a9348..2d208f32113 100644 --- a/packages/create-toolpad-app/src/templates/auth/auth.ts +++ b/packages/create-toolpad-app/src/templates/auth/auth.ts @@ -1,4 +1,4 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/auth'; 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/auth/nextjs-app/actions.ts b/packages/create-toolpad-app/src/templates/auth/nextjs-app/actions.ts index 6d85ee1373f..659880c34da 100644 --- a/packages/create-toolpad-app/src/templates/auth/nextjs-app/actions.ts +++ b/packages/create-toolpad-app/src/templates/auth/nextjs-app/actions.ts @@ -5,7 +5,7 @@ const actionsTemplate: Template = (options) => { return `'use server'; import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core'; +import type { AuthProvider } from '@toolpad/core/auth'; import { signIn } from '../../../auth'; export default async function serverSignIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { diff --git a/packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts b/packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts index a1d4b43d0c2..16825b0dcc0 100644 --- a/packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts +++ b/packages/create-toolpad-app/src/templates/auth/nextjs-app/signInPage.ts @@ -8,7 +8,7 @@ const signInPage: Template = (options) => { import { SignInPage } from '@toolpad/core/SignInPage'; ${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';` : ''} +${hasPasskeyProvider ? `import type { AuthProvider } from '@toolpad/core/auth';` : ''} ${hasPasskeyProvider ? `import serverSignIn from './actions';` : `import signIn from './actions';`} ${ diff --git a/packages/create-toolpad-app/src/templates/auth/utils.ts b/packages/create-toolpad-app/src/templates/auth/utils.ts index 719277b1d92..6ca43e658eb 100644 --- a/packages/create-toolpad-app/src/templates/auth/utils.ts +++ b/packages/create-toolpad-app/src/templates/auth/utils.ts @@ -1,4 +1,4 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; +import type { SupportedAuthProvider } from '@toolpad/core/auth'; export function requiresIssuer(provider: SupportedAuthProvider) { return ( diff --git a/packages/create-toolpad-app/src/types.ts b/packages/create-toolpad-app/src/types.ts index 0533f9aa433..739ae45b894 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/auth'; import { PackageJson } from './templates/packageType'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b6775ace81..dc143bc3ba2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.37.2(eslint@8.57.1) eslint-plugin-react-compiler: specifier: latest - version: 19.0.0-beta-b2e8e9c-20241220(eslint@8.57.1) + version: 19.0.0-beta-55955c9-20241229(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 5.0.0 version: 5.0.0(eslint@8.57.1) @@ -1356,7 +1356,7 @@ importers: version: 18.3.1(react@18.3.1) recharts: specifier: alpha - version: 2.13.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.0.0-alpha.0(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@5.0.1) packages: @@ -4132,6 +4132,17 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@reduxjs/toolkit@2.5.0': + resolution: {integrity: sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@remix-run/router@1.19.2': resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==} engines: {node: '>=14.0.0'} @@ -4664,6 +4675,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -6318,8 +6332,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@19.0.0-beta-b2e8e9c-20241220: - resolution: {integrity: sha512-STVaOQyivSBv0un6/ujYOPntKcCaD0qXIG8siBEs9QcWmQ7q3J3ozuAE86SlSc7ElIZgPoL9HoSN3EONS47nqQ==} + eslint-plugin-react-compiler@19.0.0-beta-55955c9-20241229: + resolution: {integrity: sha512-KsE6bQrNvtPDbMb9EolZ2C+Z2/uv2Y5cAUgN+pzbriXjKlf1FkbgiyE153m2mIT6gEZq2OrtFvX7uJj5IrNQlg==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -7066,6 +7080,9 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -9214,6 +9231,18 @@ packages: react-is@19.0.0: resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -9343,15 +9372,13 @@ packages: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} - recharts-scale@0.4.5: - resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - - recharts@2.13.0-alpha.5: - resolution: {integrity: sha512-mm8ORfDusDhyWlrY/2NntUAsNeYukteplvRqKGkBEmqNPwqYq9GoEzaVsVDYj8bjGSKJynWGhjEO1NFcntl29g==} - engines: {node: '>=14'} + recharts@3.0.0-alpha.0: + resolution: {integrity: sha512-phxJucbwKseB0fwvu2JDhFsXValIcTe5PdzJEOZH2e+mL6zAAWMMZjp4MN8yrHMsfSiK2T63xQLp5HL6HQC3pA==} + engines: {node: '>=18'} peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 recursive-readdir@2.2.3: resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} @@ -9365,6 +9392,14 @@ packages: resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} engines: {node: '>=12'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -10427,6 +10462,11 @@ packages: urlpattern-polyfill@8.0.2: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -10475,8 +10515,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + victory-vendor@37.3.5: + resolution: {integrity: sha512-+K2VBMmB7peKG3Gjp79XjgsbfsYgD0eZRSmKz7p5a4V0NhYq43eM/b0gpSLq+Dhwag96QaWsU75/6bFVBjVE7A==} vite-node@2.1.7: resolution: {integrity: sha512-b/5MxSWd0ftWt1B1LHfzCw0ASzaxHztUwP0rcsBhkDSGy9ZDEDieSIjFG3I78nI9dUN0eSeD6LtuKPZGjwwpZQ==} @@ -13918,6 +13958,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@reduxjs/toolkit@2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 18.3.1 + react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) + '@remix-run/router@1.19.2': {} '@rollup/rollup-android-arm-eabi@4.24.3': @@ -14506,6 +14556,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/webidl-conversions@7.0.3': {} '@types/webpack-dev-server@3.11.6': @@ -16536,7 +16588,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@19.0.0-beta-b2e8e9c-20241220(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-55955c9-20241229(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2 @@ -17580,6 +17632,8 @@ snapshots: immediate@3.0.6: {} + immer@10.1.1: {} + immer@9.0.21: {} import-fresh@3.3.0: @@ -19954,6 +20008,15 @@ snapshots: react-is@19.0.0: {} + react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + redux: 5.0.1 + react-refresh@0.14.2: {} react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -20119,22 +20182,25 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 - recharts-scale@0.4.5: - dependencies: - decimal.js-light: 2.5.1 - - recharts@2.13.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + recharts@3.0.0-alpha.0(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@5.0.1): dependencies: + '@reduxjs/toolkit': 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1) clsx: 2.1.1 - eventemitter3: 4.0.7 + decimal.js-light: 2.5.1 + eventemitter3: 5.0.1 + immer: 10.1.1 lodash: 4.17.21 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-is: 18.3.1 + react-is: 19.0.0 + react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - recharts-scale: 0.4.5 tiny-invariant: 1.3.3 - victory-vendor: 36.9.2 + use-sync-external-store: 1.4.0(react@18.3.1) + victory-vendor: 37.3.5 + transitivePeerDependencies: + - '@types/react' + - redux recursive-readdir@2.2.3: dependencies: @@ -20150,6 +20216,12 @@ snapshots: indent-string: 5.0.0 strip-indent: 4.0.0 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -21329,6 +21401,10 @@ snapshots: urlpattern-polyfill@8.0.2: {} + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -21370,7 +21446,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - victory-vendor@36.9.2: + victory-vendor@37.3.5: dependencies: '@types/d3-array': 3.2.1 '@types/d3-ease': 3.0.2 From 9b508b22675bf4ba66a151c4da06aac180d65f4c Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Mon, 6 Jan 2025 14:18:30 +0530 Subject: [PATCH 3/5] wip: v1 of Sign Up docs --- .../components/sign-up-page/sign-up-page.md | 224 ++++++------------ 1 file changed, 79 insertions(+), 145 deletions(-) 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 5d142f868d6..a10ea600170 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 @@ -6,193 +6,127 @@ 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 UI component that abstracts away the pain needed to wire together a secure authentication 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. ::: -## Basic usage +The `SignUpPage` component is a quick way to generate a ready-to-use registration page with multiple OAuth providers, or a credentials form. -```jsx -import { SignUpPage } from '@toolpad/core'; +## OAuth -export default function App() { - return ( - console.log(data)} - providers={['google', 'github']} - /> - ); -} -``` +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. -## Props +{{"demo": "OAuthSignUpPage.js", "iframe": true, "height": 600}} -### Required props - -| Name | Type | Description | -| ---------- | ---------------------------- | ------------------------------------------ | -| `onSubmit` | `(data: SignUpData) => void` | Callback fired when the form is submitted. | +:::info +The same OAuth providers supported by SignInPage are available for SignUpPage. See the [Auth.js documentation](https://authjs.dev/getting-started/authentication/oauth) for setup details. +::: -### Optional props +## Credentials -| Name | Type | Default | Description | -| ------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------ | -| `providers` | `string[]` | `[]` | List of OAuth providers to display. Supported values: 'google', 'github', 'twitter', 'facebook'. | -| `title` | `string` | `'Sign up'` | The title displayed at the top of the page. | -| `logo` | `ReactNode` | `undefined` | Custom logo to display above the form. | -| `theme` | `Theme` | `undefined` | Custom theme object to override default styles. | -| `redirectUrl` | `string` | `'/'` | URL to redirect to after successful sign-up. | -| `loading` | `boolean` | `false` | Whether to show loading state. | -| `error` | `string` | `undefined` | Error message to display. | +To render a registration form with email/password, pass in a provider with `credentials` as the `id` property. The `signUp` function accepts a `formData` parameter in this case. -## Examples +{{"demo": "CredentialsSignUpPage.js", "iframe": true, "height": 600}} -### Basic sign-up with email/password +## Usage with authentication libraries -```jsx -import { SignUpPage } from '@toolpad/core'; +### Firebase -export default function SignUp() { - const handleSignUp = async (data) => { - try { - await createUser(data); - // Handle successful sign-up - } catch (error) { - // Handle error - } - }; - - return ; -} -``` +The component is composable with any authentication library. Here's an example using Firebase with Vite: -### With OAuth providers +```tsx title="src/pages/signup.tsx" +'use client'; +import * as React from 'react'; -```jsx -import { SignUpPage } from '@toolpad/core'; +import Link from '@mui/material/Link'; +import LinearProgress from '@mui/material/LinearProgress'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import { Navigate, useNavigate, useLocation } from 'react-router-dom'; +import { useSession, type Session } from '../SessionContext'; +import { signInWithGoogle } from '../firebase/auth'; export default function SignUp() { - return ( - - ); -} -``` + const { session, setSession, loading } = useSession(); + const [completing, setCompleting] = React.useState(false); + const navigate = useNavigate(); + const location = useLocation(); -### Custom styling + if (loading) { + return ; + } -```jsx -import { SignUpPage } from '@toolpad/core'; + if (session) { + return ; + } -const customTheme = { - colors: { - primary: '#1976d2', - background: '#f5f5f5', - }, - borderRadius: '8px', -}; - -export default function SignUp() { return ( } + providers={[{ id: 'google', name: 'Google' }]} + signUp={async (provider, formData, callbackUrl) => { + let result; + try { + if (provider.id === 'google') { + result = await signInWithGoogle(); + } + if (result?.success && result?.user) { + // Convert Firebase user to Session format + const userSession: Session = { + user: { + name: result.user.displayName || '', + email: result.user.email || '', + image: result.user.photoURL || '', + }, + }; + setSession(userSession); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + return { error: result?.error || 'Failed to sign in' }; + } catch (error) { + return { + error: error instanceof Error ? error.message : 'An error occurred', + }; + } + }} /> ); } ``` -## Form Fields - -The default sign-up form includes the following fields: - -- Email (required) -- Password (required) -- Confirm Password (required) -- Name (optional) +:::info +The [Firebase Vite example app](https://github.com/mui/mui-toolpad/tree/master/examples/core/firebase-vite/) comes with a working app using Firebase including Sign Up and Sign In flows. +::: -## TypeScript +## Customization -The component includes full TypeScript support. Here are the main types you'll work with: +### Branding -```ts -interface SignUpData { - email: string; - password: string; - name?: string; -} +You can add your own branding elements through the `branding` prop in the AppProvider: -interface SignUpPageProps { - onSubmit: (data: SignUpData) => void; - providers?: string[]; - title?: string; - logo?: ReactNode; - theme?: Theme; - redirectUrl?: string; - loading?: boolean; - error?: string; -} -``` +{{"demo": "BrandingSignUpPage.js", "iframe": true, "height": 600 }} -## Customization +### Theme -### Custom Form Fields - -You can extend the default form fields using the `fields` prop: - -```jsx -const customFields = [ - { - name: 'company', - label: 'Company Name', - type: 'text', - required: true, - }, - { - name: 'role', - label: 'Job Role', - type: 'select', - options: ['Developer', 'Designer', 'Manager'], - }, -]; - -; -``` +The `SignUpPage` can be deeply customized to match any theme through the AppProvider's theme prop: -### Custom Validation +{{"demo": "ThemeSignUpPage.js", "iframe": true, "height": 700 }} -The component uses Yup for form validation. You can provide custom validation rules: +### Slots -```jsx -const validationSchema = { - email: (value) => /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value), - password: (value) => value.length >= 8, -}; +To enable deep customization, the `SignUpPage` component allows bringing your own custom granular components: -; -``` +{{"demo": "SlotsSignUp.js", "iframe": true, "height": 540 }} -## Best Practices +You can use the `slotProps` prop to pass props to the underlying components of each slot: -1. Always handle errors gracefully and display meaningful error messages to users. -2. Implement proper security measures on your backend to validate and sanitize user input. -3. Consider implementing rate limiting to prevent abuse. -4. Use HTTPS to secure data transmission. -5. Follow accessibility guidelines by maintaining proper contrast and keyboard navigation. +{{"demo": "SlotPropsSignUp.js", "iframe": true, "height": 600 }} -## Related Components +### 🚧 Layouts -- [`SignInPage`](/toolpad/core/react-sign-in-page/) - For user login -- [`PasswordResetPage`](/toolpad/core/react-password-reset-page/) - For password recovery -- [`AuthProvider`](/toolpad/core/react-auth-provider/) - For managing authentication state +The `SignUpPage` component will support different layouts for authentication - one column, two column and others. The APIs of these components will be identical. This is in progress. -## API Reference +## 🚧 Other authentication flows -For a complete list of props and methods, see the [API Reference](/toolpad/core/api-reference/#sign-up-page). +Besides the `SignUpPage`, the team is planning work on several other components that enable new workflows such as [forgot password](https://github.com/mui/toolpad/issues/4265) and [one-time code verification](https://github.com/mui/toolpad/issues/4292). From e23ee9a2e554ce7e07bfa0ee094f93806db70a3e Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Thu, 9 Jan 2025 17:35:55 +0530 Subject: [PATCH 4/5] wip: SignUpPage demos --- .../sign-up-page/CredentialsSignUpPage.js | 36 ++++++ .../sign-up-page/CredentialsSignUpPage.tsx | 39 ++++++ .../CredentialsSignUpPage.tsx.preview | 11 ++ .../sign-up-page/OAuthSignUpPage.js | 39 ++++++ .../sign-up-page/OAuthSignUpPage.tsx | 40 +++++++ .../sign-up-page/OAuthSignUpPage.tsx.preview | 22 ++++ .../components/sign-up-page/sign-up-page.md | 8 +- docs/data/toolpad/core/pages.ts | 1 - .../src/SignUpPage/SignUpPage.tsx | 6 + pnpm-lock.yaml | 111 ++++++++++++------ 10 files changed, 269 insertions(+), 44 deletions(-) create mode 100644 docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.js create mode 100644 docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx create mode 100644 docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx.preview create mode 100644 docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js create mode 100644 docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx create mode 100644 docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx.preview 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..b6b38c8e161 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.js @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; + +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [{ id: 'credentials', name: 'Email and Password' }]; +// preview-end + +const signUp = async (provider, formData) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + alert( + `Signing up 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..b16c438a8f8 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import type { AuthProvider } from '@toolpad/core/auth'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [{ id: 'credentials', name: 'Email and Password' }]; +// preview-end + +const signUp: (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..ec0827255f8 --- /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/OAuthSignUpPage.js b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js new file mode 100644 index 00000000000..d3d8a669d9e --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignInPage } from '@toolpad/core/SignInPage'; + +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 signIn = async (provider) => { + // preview-start + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign in with ${provider.id}`); + resolve({ error: 'This is a fake error' }); + }, 500); + }); + // preview-end + return promise; +}; + +export default function OAuthSignInPage() { + 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..3091faf382e --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import type { AuthResponse, AuthProvider } from '@toolpad/core/auth'; +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: (provider: AuthProvider) => void | Promise = 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 OAuthSigUPnPage() { + 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..5bb9242ec8b --- /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 in with ${provider.id}`); + resolve({ error: 'This is a fake error' }); + }, 500); +}); + +// ... + + + + \ 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 a10ea600170..1065609fe91 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 @@ -105,23 +105,23 @@ The [Firebase Vite example app](https://github.com/mui/mui-toolpad/tree/master/e You can add your own branding elements through the `branding` prop in the AppProvider: -{{"demo": "BrandingSignUpPage.js", "iframe": true, "height": 600 }} + ### Theme The `SignUpPage` can be deeply customized to match any theme through the AppProvider's theme prop: -{{"demo": "ThemeSignUpPage.js", "iframe": true, "height": 700 }} + ### Slots To enable deep customization, the `SignUpPage` component allows bringing your own custom granular components: -{{"demo": "SlotsSignUp.js", "iframe": true, "height": 540 }} + You can use the `slotProps` prop to pass props to the underlying components of each slot: -{{"demo": "SlotPropsSignUp.js", "iframe": true, "height": 600 }} + ### 🚧 Layouts diff --git a/docs/data/toolpad/core/pages.ts b/docs/data/toolpad/core/pages.ts index 4e8d6831433..d40e2fc7c2b 100644 --- a/docs/data/toolpad/core/pages.ts +++ b/docs/data/toolpad/core/pages.ts @@ -106,7 +106,6 @@ const pages: MuiPage[] = [ { pathname: '/toolpad/core/react-sign-up-page', title: 'Sign-up Page', - planned: true, }, { pathname: '/toolpad/core/react-rbac', diff --git a/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx index f3ddbebb33d..6dbb900e255 100644 --- a/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx +++ b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx @@ -50,6 +50,11 @@ export interface SignUpPageSlots { * @default Typography */ subtitle?: React.ElementType; + /* + * A component to add additional fields to the sign in form + * @default null + */ + formFields?: React.ReactNode[]; } export interface SignUpPageProps { @@ -444,6 +449,7 @@ function SignUpPage(props: SignUpPageProps) { })} /> )} + {slots?.formFields ?? null} {slots?.submitButton ? ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df0bff0a87d..06c3ea4a3dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,7 +198,7 @@ importers: version: 7.37.2(eslint@8.57.1) eslint-plugin-react-compiler: specifier: latest - version: 19.0.0-beta-55955c9-20241229(eslint@8.57.1) + version: 19.0.0-beta-63e3235-20250105(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 5.0.0 version: 5.0.0(eslint@8.57.1) @@ -1359,7 +1359,7 @@ importers: version: 18.3.1(react@18.3.1) recharts: specifier: alpha - version: 3.0.0-alpha.0(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@5.0.1) + version: 3.0.0-alpha.1(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@4.2.1) packages: @@ -4184,11 +4184,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@reduxjs/toolkit@2.5.0': - resolution: {integrity: sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==} + '@reduxjs/toolkit@1.9.7': + resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==} peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 peerDependenciesMeta: react: optional: true @@ -4558,6 +4558,9 @@ packages: '@types/history@4.7.11': resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + '@types/hoist-non-react-statics@3.3.6': + resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/html-minifier@4.0.5': resolution: {integrity: sha512-LfE7f7MFd+YUfZnlBz8W43P4NgSObWiqyKapANsWCj63Aqeqli8/9gVsGP4CwC8jPpTTYlTopKCk9rJSuht/ew==} @@ -4731,8 +4734,8 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/use-sync-external-store@0.0.3': + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -6418,8 +6421,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@19.0.0-beta-55955c9-20241229: - resolution: {integrity: sha512-KsE6bQrNvtPDbMb9EolZ2C+Z2/uv2Y5cAUgN+pzbriXjKlf1FkbgiyE153m2mIT6gEZq2OrtFvX7uJj5IrNQlg==} + eslint-plugin-react-compiler@19.0.0-beta-63e3235-20250105: + resolution: {integrity: sha512-Smts5x+u+rRopr0926jCXFPkS8D8hFJexDvTW41V0Xu/xHgd4pnGWiJQRBsvTEARzOdJ6NdlmYs4n+O4Thn2iA==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -9411,15 +9414,24 @@ packages: react-is@19.0.0: resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==} - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + react-redux@8.1.3: + resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==} peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 + '@types/react': ^16.8 || ^17.0 || ^18.0 + '@types/react-dom': ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: '>=0.59' + redux: ^4 || ^5.0.0-beta.0 peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true + react-native: + optional: true redux: optional: true @@ -9556,8 +9568,8 @@ packages: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} - recharts@3.0.0-alpha.0: - resolution: {integrity: sha512-phxJucbwKseB0fwvu2JDhFsXValIcTe5PdzJEOZH2e+mL6zAAWMMZjp4MN8yrHMsfSiK2T63xQLp5HL6HQC3pA==} + recharts@3.0.0-alpha.1: + resolution: {integrity: sha512-qg6txFvYEyGqJSdjIx/pfu5eUdM/4n13GIarEcdZG6eumcXITn7JWPkaCr/orycXbnUXjuGEADIU45176fuV+Q==} engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -9576,13 +9588,13 @@ packages: resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} engines: {node: '>=12'} - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + redux-thunk@2.4.2: + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} peerDependencies: - redux: ^5.0.0 + redux: ^4 - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} @@ -9653,6 +9665,9 @@ packages: reselect@4.1.8: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -14249,15 +14264,15 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@reduxjs/toolkit@2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + '@reduxjs/toolkit@1.9.7(react-redux@8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1))(react@18.3.1)': dependencies: - immer: 10.1.1 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 + immer: 9.0.21 + redux: 4.2.1 + redux-thunk: 2.4.2(redux@4.2.1) + reselect: 4.1.8 optionalDependencies: react: 18.3.1 - react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) + react-redux: 8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) '@remix-run/router@1.19.2': {} @@ -14658,6 +14673,11 @@ snapshots: '@types/history@4.7.11': {} + '@types/hoist-non-react-statics@3.3.6': + dependencies: + '@types/react': 18.3.18 + hoist-non-react-statics: 3.3.2 + '@types/html-minifier@4.0.5': dependencies: '@types/clean-css': 4.2.11 @@ -14849,7 +14869,7 @@ snapshots: '@types/unist@3.0.3': {} - '@types/use-sync-external-store@0.0.6': {} + '@types/use-sync-external-store@0.0.3': {} '@types/webidl-conversions@7.0.3': {} @@ -16952,7 +16972,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@19.0.0-beta-55955c9-20241229(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-63e3235-20250105(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2 @@ -20456,14 +20476,20 @@ snapshots: react-is@19.0.0: {} - react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1): + react-redux@8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1): dependencies: - '@types/use-sync-external-store': 0.0.6 + '@babel/runtime': 7.26.0 + '@types/hoist-non-react-statics': 3.3.6 + '@types/use-sync-external-store': 0.0.3 + hoist-non-react-statics: 3.3.2 react: 18.3.1 + react-is: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) optionalDependencies: '@types/react': 18.3.18 - redux: 5.0.1 + '@types/react-dom': 18.3.1 + react-dom: 18.3.1(react@18.3.1) + redux: 4.2.1 react-refresh@0.14.2: {} @@ -20632,9 +20658,9 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 - recharts@3.0.0-alpha.0(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@5.0.1): + recharts@3.0.0-alpha.1(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react-is@19.0.0)(react@18.3.1)(redux@4.2.1): dependencies: - '@reduxjs/toolkit': 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@reduxjs/toolkit': 1.9.7(react-redux@8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1))(react@18.3.1) clsx: 2.1.1 decimal.js-light: 2.5.1 eventemitter3: 5.0.1 @@ -20643,13 +20669,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-is: 19.0.0 - react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) + react-redux: 8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + reselect: 5.1.0 tiny-invariant: 1.3.3 use-sync-external-store: 1.4.0(react@18.3.1) victory-vendor: 37.3.5 transitivePeerDependencies: - '@types/react' + - '@types/react-dom' + - react-native - redux recursive-readdir@2.2.3: @@ -20666,11 +20695,13 @@ snapshots: indent-string: 5.0.0 strip-indent: 4.0.0 - redux-thunk@3.1.0(redux@5.0.1): + redux-thunk@2.4.2(redux@4.2.1): dependencies: - redux: 5.0.1 + redux: 4.2.1 - redux@5.0.1: {} + redux@4.2.1: + dependencies: + '@babel/runtime': 7.26.0 reflect.getprototypeof@1.0.6: dependencies: @@ -20760,6 +20791,8 @@ snapshots: reselect@4.1.8: {} + reselect@5.1.0: {} + reselect@5.1.1: {} resolve-cwd@3.0.0: From ca69a5e5463f7a817b145d1ab8e714f779eb3a9e Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Thu, 9 Jan 2025 19:02:56 +0530 Subject: [PATCH 5/5] wip: More SignUpPage docs, align with new proposed slots --- .../sign-in-page/BrandingSignInPage.js | 1 + .../sign-in-page/CredentialsSignInPage.js | 1 + .../sign-in-page/PasskeySignInPage.js | 1 + .../components/sign-in-page/SlotsSignUp.tsx | 182 ++++++++++++++++++ .../sign-up-page/BrandingSignUpPage.js | 44 +++++ .../sign-up-page/BrandingSignUpPage.tsx | 44 +++++ .../BrandingSignUpPage.tsx.preview | 20 ++ .../sign-up-page/CredentialsSignUpPage.tsx | 2 +- .../sign-up-page/OAuthSignUpPage.js | 10 +- .../sign-up-page/OAuthSignUpPage.tsx.preview | 4 +- .../sign-up-page/ThemeSignUpPage.js | 58 ++++++ .../sign-up-page/ThemeSignUpPage.tsx | 60 ++++++ .../sign-up-page/ThemeSignUpPage.tsx.preview | 25 +++ .../components/sign-up-page/sign-up-page.md | 42 +++- docs/data/toolpad/core/pages.ts | 9 +- examples/core/firebase-vite/package.json | 18 +- .../core/firebase-vite/src/firebase/auth.ts | 100 ++++++++++ examples/core/firebase-vite/src/main.tsx | 7 +- .../core/firebase-vite/src/pages/signup.tsx | 112 +++++++++++ .../src/SignUpPage/SignUpPage.tsx | 87 +++++---- playground/vite/src/firebase/auth.ts | 1 + playground/vite/src/pages/signup.tsx | 12 +- 22 files changed, 767 insertions(+), 73 deletions(-) create mode 100644 docs/data/toolpad/core/components/sign-in-page/SlotsSignUp.tsx create mode 100644 docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.js create mode 100644 docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx create mode 100644 docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx.preview create mode 100644 docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.js create mode 100644 docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx create mode 100644 docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx.preview create mode 100644 examples/core/firebase-vite/src/pages/signup.tsx 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 801f21ff7af..eb6bd5c3462 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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 379664289ff..c1be306a486 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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 f15f1cceded..03abb108968 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,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; + 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/SlotsSignUp.tsx b/docs/data/toolpad/core/components/sign-in-page/SlotsSignUp.tsx new file mode 100644 index 00000000000..d0f70f650c2 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/SlotsSignUp.tsx @@ -0,0 +1,182 @@ +import * as React from 'react'; +import { + Box, + Button, + FormControl, + FormControlLabel, + Checkbox, + InputLabel, + OutlinedInput, + TextField, + InputAdornment, + Link, + Alert, + IconButton, +} from '@mui/material'; +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 { SignUpPage } from '@toolpad/core/SignUpPage'; +import { useTheme } from '@mui/material/styles'; + +const providers = [{ id: 'credentials', name: 'Email and Password' }]; + +function CustomEmailField() { + return ( + + + + ), + }, + }} + variant="outlined" + /> + ); +} + +function CustomPasswordField() { + const [showPassword, setShowPassword] = React.useState(false); + + const handleClickShowPassword = () => setShowPassword((show) => !show); + + const handleMouseDownPassword = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( + + + Password + + + + {showPassword ? ( + + ) : ( + + )} + + + } + label="Password" + /> + + ); +} + +function CustomButton() { + return ( + + ); +} + +function SignInLink() { + return ( + + + Sign up + + + ); +} + +function ForgotPasswordLink() { + return ( + + Forgot password? + + ); +} + +function Title() { + return

Login

; +} + +function Subtitle() { + return ( + + We are investigating an ongoing outage. + + ); +} + +function AgreeWithTerms() { + return ( + + } + slotProps={{ + typography: { + fontSize: 14, + }, + }} + color="textSecondary" + label="I agree with the T&C" + /> + ); +} + +export default function SlotsSignIn() { + const theme = useTheme(); + return ( + + + alert( + `Logging in with "${provider.name}" and credentials: ${formData.get('email')}, ${formData.get('password')}, and checkbox value: ${formData.get('tandc')}`, + ) + } + slots={{ + title: Title, + subtitle: Subtitle, + emailField: CustomEmailField, + passwordField: CustomPasswordField, + submitButton: CustomButton, + formFooter: FormFooter, + footer: SignInLink, + }} + providers={providers} + /> + + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.js b/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.js new file mode 100644 index 00000000000..ad26be30549 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.js @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; + +import { useTheme } from '@mui/material/styles'; + +const providers = [{ id: 'credentials', name: 'Credentials' }]; +// preview-start +const BRANDING = { + logo: ( + MUI logo + ), + title: 'MUI', +}; +// preview-end + +const signUp = async (provider) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve(); + }, 500); + }); + return promise; +}; + +export default function BrandingSignInPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx b/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx new file mode 100644 index 00000000000..b15ab43c5c0 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import type { AuthProvider } from '@toolpad/core/auth'; +import { useTheme } from '@mui/material/styles'; + +const providers = [{ id: 'credentials', name: 'Credentials' }]; +// preview-start +const BRANDING = { + logo: ( + MUI logo + ), + title: 'MUI', +}; +// preview-end + +const signUp: (provider: AuthProvider) => void = async (provider) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve(); + }, 500); + }); + return promise; +}; + +export default function BrandingSignInPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx.preview b/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx.preview new file mode 100644 index 00000000000..05bc592b013 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/BrandingSignUpPage.tsx.preview @@ -0,0 +1,20 @@ +const BRANDING = { + logo: ( + MUI logo + ), + title: 'MUI', +}; + +// ... + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx index b16c438a8f8..6899fb3f7b8 100644 --- a/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx +++ b/docs/data/toolpad/core/components/sign-up-page/CredentialsSignUpPage.tsx @@ -15,7 +15,7 @@ const signUp: (provider: AuthProvider, formData: FormData) => void = async ( const promise = new Promise((resolve) => { setTimeout(() => { alert( - `Signing in with "${provider.name}" and credentials: ${formData.get('email')}, ${formData.get('password')}`, + `Signing up with "${provider.name}" and credentials: ${formData.get('email')}, ${formData.get('password')}`, ); resolve(); }, 300); diff --git a/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js index d3d8a669d9e..c465fccafbc 100644 --- a/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { SignInPage } from '@toolpad/core/SignInPage'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; import { useTheme } from '@mui/material/styles'; @@ -15,11 +15,11 @@ const providers = [ // preview-end -const signIn = async (provider) => { +const signUp = async (provider) => { // preview-start const promise = new Promise((resolve) => { setTimeout(() => { - console.log(`Sign in with ${provider.id}`); + console.log(`Sign up with ${provider.id}`); resolve({ error: 'This is a fake error' }); }, 500); }); @@ -27,12 +27,12 @@ const signIn = async (provider) => { return promise; }; -export default function OAuthSignInPage() { +export default function OAuthSigUPnPage() { 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 index 5bb9242ec8b..92ca9562d85 100644 --- a/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx.preview +++ b/docs/data/toolpad/core/components/sign-up-page/OAuthSignUpPage.tsx.preview @@ -10,7 +10,7 @@ const providers = [ const promise = new Promise((resolve) => { setTimeout(() => { - console.log(`Sign in with ${provider.id}`); + console.log(`Sign up with ${provider.id}`); resolve({ error: 'This is a fake error' }); }, 500); }); @@ -18,5 +18,5 @@ const promise = new Promise((resolve) => { // ... - + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.js b/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.js new file mode 100644 index 00000000000..98a444b8dd7 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.js @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; + +import { createTheme } from '@mui/material/styles'; +import { useColorSchemeShim } from 'docs/src/modules/components/ThemeContext'; +import { getDesignTokens, inputsCustomizations } from '../sign-in-page/customTheme'; + +const providers = [ + { id: 'github', name: 'GitHub' }, + { id: 'google', name: 'Google' }, + { id: 'credentials', name: 'Email and Password' }, +]; + +const signUp = async (provider) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve({ error: 'This is a mock error message.' }); + }, 500); + }); + return promise; +}; + +export default function ThemeSignUpPage() { + const { mode, systemMode } = useColorSchemeShim(); + const calculatedMode = (mode === 'system' ? systemMode : mode) ?? 'light'; + const brandingDesignTokens = getDesignTokens(calculatedMode); + // preview-start + const THEME = createTheme({ + ...brandingDesignTokens, + palette: { + ...brandingDesignTokens.palette, + mode: calculatedMode, + }, + components: { + ...inputsCustomizations, + }, + }); + // preview-end + + return ( + // preview-start + + .MuiStack-root': { + marginTop: '2rem', + rowGap: '0.5rem', + }, + }} + /> + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx b/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx new file mode 100644 index 00000000000..0edd980e80f --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import type { AuthProvider, AuthResponse } from '@toolpad/core/auth'; +import { createTheme } from '@mui/material/styles'; +import { useColorSchemeShim } from 'docs/src/modules/components/ThemeContext'; +import { getDesignTokens, inputsCustomizations } from '../sign-in-page/customTheme'; + +const providers = [ + { id: 'github', name: 'GitHub' }, + { id: 'google', name: 'Google' }, + { id: 'credentials', name: 'Email and Password' }, +]; + +const signUp: (provider: AuthProvider) => void | Promise = async ( + provider, +) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign up with ${provider.id}`); + resolve({ error: 'This is a mock error message.' }); + }, 500); + }); + return promise; +}; + +export default function ThemeSignUpPage() { + const { mode, systemMode } = useColorSchemeShim(); + const calculatedMode = (mode === 'system' ? systemMode : mode) ?? 'light'; + const brandingDesignTokens = getDesignTokens(calculatedMode); + // preview-start + const THEME = createTheme({ + ...brandingDesignTokens, + palette: { + ...brandingDesignTokens.palette, + mode: calculatedMode, + }, + components: { + ...inputsCustomizations, + }, + }); + // preview-end + + return ( + // preview-start + + .MuiStack-root': { + marginTop: '2rem', + rowGap: '0.5rem', + }, + }} + /> + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx.preview b/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx.preview new file mode 100644 index 00000000000..dfe431f5f1a --- /dev/null +++ b/docs/data/toolpad/core/components/sign-up-page/ThemeSignUpPage.tsx.preview @@ -0,0 +1,25 @@ +const THEME = createTheme({ + ...brandingDesignTokens, + palette: { + ...brandingDesignTokens.palette, + mode: calculatedMode, + }, + components: { + ...inputsCustomizations, + }, +}); + +// ... + + + .MuiStack-root': { + marginTop: '2rem', + rowGap: '0.5rem', + }, + }} + /> + \ 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 1065609fe91..45408ebae45 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 @@ -28,7 +28,7 @@ The same OAuth providers supported by SignInPage are available for SignUpPage. S To render a registration form with email/password, pass in a provider with `credentials` as the `id` property. The `signUp` function accepts a `formData` parameter in this case. -{{"demo": "CredentialsSignUpPage.js", "iframe": true, "height": 600}} +{{"demo": "CredentialsSignUpPage.js", "iframe": true, "height": 500}} ## Usage with authentication libraries @@ -36,6 +36,23 @@ To render a registration form with email/password, pass in a provider with `cred The component is composable with any authentication library. Here's an example using Firebase with Vite: +#### Setup + +To use Firebase as your authentication library, you need to setup a new Firebase project and obtain the following environment variables from your project console: + +```bash +VITE_FIREBASE_API_KEY= +VITE_FIREBASE_AUTH_DOMAIN= +VITE_FIREBASE_PROJECT_ID= +VITE_FIREBASE_STORAGE_BUCKET= +VITE_FIREBASE_MESSAGE_SENDER_ID= +VITE_FIREBASE_APP_ID= +``` + +Visit the [Firebase docs](https://firebase.google.com/docs/auth) for more details on setting up and adding authentication to your project. + +#### Usage + ```tsx title="src/pages/signup.tsx" 'use client'; import * as React from 'react'; @@ -43,9 +60,9 @@ import * as React from 'react'; import Link from '@mui/material/Link'; import LinearProgress from '@mui/material/LinearProgress'; import { SignUpPage } from '@toolpad/core/SignUpPage'; -import { Navigate, useNavigate, useLocation } from 'react-router-dom'; +import { Navigate, useNavigate, useLocation } from 'react-router'; import { useSession, type Session } from '../SessionContext'; -import { signInWithGoogle } from '../firebase/auth'; +import { signUpWithCredentials } from '../firebase/auth'; export default function SignUp() { const { session, setSession, loading } = useSession(); @@ -63,12 +80,19 @@ export default function SignUp() { return ( { let result; try { - if (provider.id === 'google') { - result = await signInWithGoogle(); + if (provider.id === 'credentials') { + const email = formData?.get('email') as string; + const password = formData?.get('password') as string; + + if (!email || !password) { + return { error: 'Email and password are required' }; + } + + result = await signUpWithCredentials(email, password); } if (result?.success && result?.user) { // Convert Firebase user to Session format @@ -96,7 +120,7 @@ export default function SignUp() { ``` :::info -The [Firebase Vite example app](https://github.com/mui/mui-toolpad/tree/master/examples/core/firebase-vite/) comes with a working app using Firebase including Sign Up and Sign In flows. +The [Firebase Vite example app](https://github.com/mui/mui-toolpad/tree/master/examples/core/firebase-vite/) comes with a working app using Firebase including both `SignUpPage` and `SignInPage` flows. ::: ## Customization @@ -105,13 +129,13 @@ The [Firebase Vite example app](https://github.com/mui/mui-toolpad/tree/master/e You can add your own branding elements through the `branding` prop in the AppProvider: - +{{"demo": "BrandingSignUpPage.js", "iframe": true, "height": 600 }} ### Theme The `SignUpPage` can be deeply customized to match any theme through the AppProvider's theme prop: - +{{"demo": "ThemeSignUpPage.js", "iframe": true, "height": 700 }} ### Slots diff --git a/docs/data/toolpad/core/pages.ts b/docs/data/toolpad/core/pages.ts index d40e2fc7c2b..08b94bbd74b 100644 --- a/docs/data/toolpad/core/pages.ts +++ b/docs/data/toolpad/core/pages.ts @@ -99,14 +99,15 @@ const pages: MuiPage[] = [ pathname: '/toolpad/core/react-sign-in-page', title: 'Sign-in Page', }, - { - pathname: '/toolpad/core/react-account', - title: 'Account', - }, { pathname: '/toolpad/core/react-sign-up-page', title: 'Sign-up Page', }, + { + pathname: '/toolpad/core/react-account', + title: 'Account', + }, + { pathname: '/toolpad/core/react-rbac', title: 'RBAC', diff --git a/examples/core/firebase-vite/package.json b/examples/core/firebase-vite/package.json index 029bb677f2b..c9de7e5895b 100644 --- a/examples/core/firebase-vite/package.json +++ b/examples/core/firebase-vite/package.json @@ -9,25 +9,17 @@ "dependencies": { "firebase": "^11", "@emotion/react": "^11", - "@fontsource-variable/inter": "^5.1.0", "@emotion/styled": "^11", "@mui/icons-material": "^6", - "@mui/x-charts": "^7", - "@mui/x-tree-view": "^7", - "@mui/x-data-grid": "^7", - "@mui/x-date-pickers": "^7", - "dayjs": "^1", - "clsx": "^2", - "@react-spring/web": "^9", "@mui/material": "^6", "@toolpad/core": "latest", - "react": "^18", - "react-dom": "^18", - "react-router-dom": "^6" + "react": "^19", + "react-dom": "^19", + "react-router": "^7" }, "devDependencies": { - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^19", + "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.3.2", "typescript": "^5", "vite": "^5.4.8" diff --git a/examples/core/firebase-vite/src/firebase/auth.ts b/examples/core/firebase-vite/src/firebase/auth.ts index 014956544b3..381067c4b88 100644 --- a/examples/core/firebase-vite/src/firebase/auth.ts +++ b/examples/core/firebase-vite/src/firebase/auth.ts @@ -5,6 +5,11 @@ import { setPersistence, browserSessionPersistence, signInWithEmailAndPassword, + createUserWithEmailAndPassword, + sendSignInLinkToEmail, + signInWithEmailLink, + isSignInWithEmailLink, + getAdditionalUserInfo, signOut, } from 'firebase/auth'; import { firebaseAuth } from './firebaseConfig'; @@ -73,6 +78,101 @@ export async function signInWithCredentials(email: string, password: string) { } } +// Sign up with email and password +const actionCodeSettings = { + // URL you want to redirect back to. The domain (www.example.com) for this + // URL must be in the authorized domains list in the Firebase Console. + url: 'http://localhost:5173/sign-up?partial=true&provider=email', + // This must be true. + handleCodeInApp: true, +}; + +export async function signUpWithCredentials(email: string, password: string) { + try { + const userCredential = await createUserWithEmailAndPassword(firebaseAuth, email, password); + return { + success: true, + user: userCredential.user, + error: null, + }; + } catch (error: any) { + return { + success: false, + user: null, + error: error.message || 'Failed to sign in with email/password', + }; + } +} + +// Sign up with email link +export async function signUpWithEmailLink(email: string, callbackUrl?: string) { + try { + if (callbackUrl) { + actionCodeSettings.url += new URLSearchParams({ callbackUrl }).toString(); + } + await sendSignInLinkToEmail(firebaseAuth, email, actionCodeSettings); + + // The link was successfully sent. Inform the user. + // Save the email locally so you don't need to ask the user for it again + // if they open the link on the same device. + localStorage.setItem('toolpad-firebase-signup-partial', email); + + return { + success: true, + }; + } catch (error: any) { + return { + success: false, + user: null, + error: error.message, + }; + } +} + +export async function completeSignUpWithEmailLink() { + // Confirm the link is a sign-in with email link. + if (isSignInWithEmailLink(firebaseAuth, window.location.href)) { + // Additional state parameters can also be passed via URL. + // This can be used to continue the user's intended action before triggering + // the sign-in operation. + // Get the email if available. This should be available if the user completes + // the flow on the same device where they started it. + const email = window.localStorage.getItem('toolpad-firebase-signup-partial'); + + // The client SDK will parse the code from the link for you. + try { + if (email) { + const result = await signInWithEmailLink(firebaseAuth, email, window.location.href); + + // Clear email from storage. + window.localStorage.removeItem('emailForSignIn'); + // You can access the new user by importing getAdditionalUserInfo + // and calling it with result: + const user = getAdditionalUserInfo(result); + return { + success: true, + user, + error: null, + }; + } + return { + error: "Error accessing the user's email address for sign up verification", + success: false, + user: null, + }; + } catch (error: any) { + // Some error occurred, you can inspect the code: error.code + // Common errors could be invalid email and invalid or expired OTPs. + return { + success: false, + error: error.message, + user: null, + }; + } + } + return {}; +} + // Sign out functionality export const firebaseSignOut = async () => { try { diff --git a/examples/core/firebase-vite/src/main.tsx b/examples/core/firebase-vite/src/main.tsx index 1ccc9f6b4f5..e135c290694 100644 --- a/examples/core/firebase-vite/src/main.tsx +++ b/examples/core/firebase-vite/src/main.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { createBrowserRouter, RouterProvider } from 'react-router'; import App from './App'; import Layout from './layouts/dashboard'; import DashboardPage from './pages'; import OrdersPage from './pages/orders'; import SignInPage from './pages/signin'; +import SignUpPage from './pages/signup'; const router = createBrowserRouter([ { @@ -29,6 +30,10 @@ const router = createBrowserRouter([ path: '/sign-in', Component: SignInPage, }, + { + path: '/sign-up', + component: SignUpPage, + }, ], }, ]); diff --git a/examples/core/firebase-vite/src/pages/signup.tsx b/examples/core/firebase-vite/src/pages/signup.tsx new file mode 100644 index 00000000000..776db3b11e5 --- /dev/null +++ b/examples/core/firebase-vite/src/pages/signup.tsx @@ -0,0 +1,112 @@ +'use client'; +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import LinearProgress from '@mui/material/LinearProgress'; +import { SignUpPage } from '@toolpad/core/SignUpPage'; +import { Navigate, useNavigate, useLocation } from 'react-router'; +import { useSession, type Session } from '../SessionContext'; +import { + signUpWithCredentials, + signUpWithEmailLink, + signInWithGoogle, + signInWithGithub, + completeSignUpWithEmailLink, +} from '../firebase/auth'; + +function SignInLink() { + return ( + + Sign In + + ); +} + +export default function SignUp() { + const { session, setSession, loading } = useSession(); + const [completing, setCompleting] = React.useState(false); + const navigate = useNavigate(); + const location = useLocation(); + + React.useEffect(() => { + async function completeSignUp() { + if (!loading && !session && location.search) { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get('provider') === 'email' && searchParams.get('partial') === 'true') { + setCompleting(true); + await completeSignUpWithEmailLink(); + setCompleting(false); + } + } + } + completeSignUp(); + }, [loading, session, location]); + + if (loading || completing) { + return ; + } + + if (session) { + return ; + } + + return ( + { + let result; + try { + if (provider.id === 'google') { + result = await signInWithGoogle(); + } + if (provider.id === 'github') { + result = await signInWithGithub(); + } + if (provider.id === 'credentials') { + const email = formData?.get('email') as string; + const password = formData?.get('password') as string; + + if (!email || !password) { + return { error: 'Email and password are required' }; + } + + result = await signUpWithCredentials(email, password); + } + if (provider.id === 'email') { + const email = formData?.get('email') as string; + if (!email) { + return { error: 'Email is required' }; + } + result = await signUpWithEmailLink(email); + if (result.success) { + return { success: 'Check your inbox for a verification link.' }; + } + } + + if (result?.success && result?.user) { + // Convert Firebase user to Session format + const userSession: Session = { + user: { + name: result.user.displayName || '', + email: result.user.email || '', + image: result.user.photoURL || '', + }, + }; + setSession(userSession); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + return { error: result?.error || 'Failed to sign in' }; + } catch (error) { + return { error: error instanceof Error ? error.message : 'An error occurred' }; + } + }} + slots={{ footer: SignInLink }} + /> + ); +} diff --git a/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx index 6dbb900e255..2ab744389c8 100644 --- a/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx +++ b/packages/toolpad-core/src/SignUpPage/SignUpPage.tsx @@ -12,7 +12,6 @@ import TextField, { TextFieldProps } from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import LoadingButton, { LoadingButtonProps } from '@mui/lab/LoadingButton'; import { alpha, useTheme, SxProps } from '@mui/material/styles'; -import { LinkProps } from '@mui/material/Link'; import { BrandingContext, RouterContext } from '../shared/context'; import IconProviderMap from '../shared/icons/iconProviderMap'; import { getCommonTextFieldProps } from '../shared/utils'; @@ -36,10 +35,10 @@ export interface SignUpPageSlots { submitButton?: React.JSXElementConstructor; /** - * The custom sign in link component used in the credentials form. - * @default Link + * The custom component for the page footer. + * @default null */ - signInLink?: React.JSXElementConstructor; + footer?: React.ElementType; /** * A component to override the default title section * @default Typography @@ -50,11 +49,21 @@ export interface SignUpPageSlots { * @default Typography */ subtitle?: React.ElementType; - /* - * A component to add additional fields to the sign in form + /** + * A component to override the entire form section + * @default form + */ + form?: React.JSXElementConstructor; + /** + * A component to add content to the space between the form fields and the submit button + * @default null + */ + formFooter?: React.ElementType; + /** + * A component to add additional fields to the form * @default null */ - formFields?: React.ReactNode[]; + formFields?: React.ElementType; } export interface SignUpPageProps { @@ -79,7 +88,7 @@ export interface SignUpPageProps { /** * The components used for each slot inside. * @default {} - * @example { signInLink: Sign In } + * @example { footer: Sign In } */ slots?: SignUpPageSlots; /** @@ -93,8 +102,10 @@ export interface SignUpPageProps { emailField?: TextFieldProps; passwordField?: TextFieldProps; submitButton?: LoadingButtonProps; - forgotPasswordLink?: LinkProps; - signInLink?: LinkProps; + form?: Partial>; + formFields?: React.ComponentProps; + formFooter?: React.ComponentProps; + footer?: React.ComponentProps; }; /** * The prop used to customize the styles on the `SignInPage` container @@ -119,7 +130,7 @@ function SignUpPage(props: SignUpPageProps) { const passkeyProvider = providers?.find((provider) => provider.id === 'passkey'); const credentialsProvider = providers?.find((provider) => provider.id === 'credentials'); const emailProvider = providers?.find( - (provider) => provider.id === 'nodemailer' || provider.id === 'email', + (provider) => provider.id === 'nodemailer' || provider.id === 'firebase-email', ); const [{ loading, selectedProviderId, error, success }, setFormStatus] = React.useState<{ loading: boolean; @@ -140,7 +151,7 @@ function SignUpPage(props: SignUpPageProps) { provider && provider !== 'credentials' && provider !== 'nodemailer' && - provider !== 'email' && + provider !== 'firebase-email' && provider !== 'passkey', [], ); @@ -157,7 +168,7 @@ function SignUpPage(props: SignUpPageProps) { }} > - )} - + {error && isOauthProvider(selectedProviderId) ? ( {error} @@ -219,7 +231,9 @@ function SignUpPage(props: SignUpPageProps) { error: oauthResponse?.error, })); }} + {...slotProps?.form} > + {slots?.formFooter ? : null} {singleProvider ? null : or} {error && selectedProviderId === 'passkey' ? ( - + {error} ) : null} @@ -268,6 +282,7 @@ function SignUpPage(props: SignUpPageProps) { error: passkeyResponse?.error, })); }} + {...slotProps?.form} > {slots?.emailField ? ( @@ -285,6 +300,8 @@ function SignUpPage(props: SignUpPageProps) { })} /> )} + {slots?.formFields ? : null} + {slots?.formFooter ? : null} {slots?.submitButton ? ( ) : ( @@ -315,14 +332,14 @@ function SignUpPage(props: SignUpPageProps) { {singleProvider ? null : or} {error && - (selectedProviderId === 'nodemailer' || selectedProviderId === 'email') ? ( - + (selectedProviderId === 'nodemailer' || selectedProviderId === 'firebase-email') ? ( + {error} ) : null} {success && - (selectedProviderId === 'nodemailer' || selectedProviderId === 'email') ? ( - + (selectedProviderId === 'nodemailer' || selectedProviderId === 'firebase-email') ? ( + {success} ) : null} @@ -344,6 +361,7 @@ function SignUpPage(props: SignUpPageProps) { success: emailResponse?.success, })); }} + {...slotProps?.form} > {slots?.emailField ? ( @@ -353,14 +371,16 @@ function SignUpPage(props: SignUpPageProps) { label: 'Email', placeholder: 'your@email.com', name: 'email', - id: `email-nodemailer`, + id: `email-magicLink`, type: 'email', - autoComplete: `email-nodemailer`, + autoComplete: `email-magicLink`, autoFocus: singleProvider, ...slotProps?.emailField, })} /> )} + {slots?.formFields ? : null} + {slots?.formFooter ? : null} {slots?.submitButton ? ( ) : ( @@ -370,7 +390,7 @@ function SignUpPage(props: SignUpPageProps) { size="large" variant="outlined" disableElevation - id="submit-nodemailer" + id="submit-email" color="inherit" loading={loading && selectedProviderId === emailProvider.id} sx={{ @@ -391,7 +411,7 @@ function SignUpPage(props: SignUpPageProps) { {singleProvider ? null : or} {error && selectedProviderId === 'credentials' ? ( - + {error} ) : null} @@ -416,8 +436,9 @@ function SignUpPage(props: SignUpPageProps) { error: credentialsResponse?.error, })); }} + {...slotProps?.form} > - + {slots?.emailField ? ( ) : ( @@ -449,8 +470,10 @@ function SignUpPage(props: SignUpPageProps) { })} /> )} - {slots?.formFields ?? null} + {slots?.formFields ? : null} + {slots?.formFields ? : null} + {slots?.formFooter ? : null} {slots?.submitButton ? ( ) : ( @@ -473,16 +496,12 @@ function SignUpPage(props: SignUpPageProps) { )} - {slots?.signInLink ? ( - - {slots?.signInLink ? : null} - - ) : null} + {slots?.footer ? : null} ) : null} - + ); @@ -523,18 +542,18 @@ SignUpPage.propTypes /* remove-proptypes */ = { emailField: PropTypes.object, forgotPasswordLink: PropTypes.object, passwordField: PropTypes.object, - signInLink: PropTypes.object, + footer: PropTypes.object, submitButton: PropTypes.object, }), /** * The components used for each slot inside. * @default {} - * @example { signInLink: Sign In } + * @example { footer: Sign In } */ slots: PropTypes.shape({ emailField: PropTypes.elementType, passwordField: PropTypes.elementType, - signInLink: PropTypes.elementType, + footer: PropTypes.elementType, submitButton: PropTypes.elementType, subtitle: PropTypes.elementType, title: PropTypes.elementType, diff --git a/playground/vite/src/firebase/auth.ts b/playground/vite/src/firebase/auth.ts index 3941b2f099d..381067c4b88 100644 --- a/playground/vite/src/firebase/auth.ts +++ b/playground/vite/src/firebase/auth.ts @@ -172,6 +172,7 @@ export async function completeSignUpWithEmailLink() { } return {}; } + // Sign out functionality export const firebaseSignOut = async () => { try { diff --git a/playground/vite/src/pages/signup.tsx b/playground/vite/src/pages/signup.tsx index 90a5fa4424e..f399a08ec4d 100644 --- a/playground/vite/src/pages/signup.tsx +++ b/playground/vite/src/pages/signup.tsx @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; - +import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import LinearProgress from '@mui/material/LinearProgress'; import { SignUpPage } from '@toolpad/core/SignUpPage'; @@ -15,7 +15,11 @@ import { } from '../firebase/auth'; function SignInLink() { - return Sign In; + return ( + + Sign In + + ); } export default function SignUp() { @@ -51,7 +55,7 @@ export default function SignUp() { providers={[ { id: 'google', name: 'Google' }, { id: 'github', name: 'GitHub' }, - { id: 'email', name: 'Email' }, + { id: 'firebase-email', name: 'Email' }, { id: 'credentials', name: 'Credentials' }, ]} signUp={async (provider, formData, callbackUrl) => { @@ -102,7 +106,7 @@ export default function SignUp() { return { error: error instanceof Error ? error.message : 'An error occurred' }; } }} - slots={{ signInLink: SignInLink }} + slots={{ footer: SignInLink }} /> ); }