diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md
index 8ec099d30..1a6a2e031 100644
--- a/waspc/ChangeLog.md
+++ b/waspc/ChangeLog.md
@@ -2,6 +2,72 @@
## 0.11.4
+### 🎉 [New Feature] Signup Fields Customization
+
+We added an API for extending the default signup form with custom fields. This allows you to add fields like `age`, `address`, etc. to your signup form.
+
+You first need to define the `auth.signup.additionalFields` property in your `.wasp` file:
+```wasp
+app crudTesting {
+ // ...
+ auth: {
+ userEntity: User,
+ methods: {
+ usernameAndPassword: {},
+ },
+ onAuthFailedRedirectTo: "/login",
+ signup: {
+ additionalFields: import { fields } from "@server/auth.js",
+ },
+ },
+}
+```
+
+Then, you need to define the `fields` object in your `auth.js` file:
+```js
+import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
+
+export const fields = defineAdditionalSignupFields({
+ address: (data) => {
+ // Validate the address field
+ if (typeof data.address !== 'string') {
+ throw new Error('Address is required.')
+ }
+ if (data.address.length < 10) {
+ throw new Error('Address must be at least 10 characters long.')
+ }
+ // Return the address field
+ return data.address
+ },
+})
+```
+
+Finally, you can extend the `SignupForm` component on the client:
+```jsx
+import { SignupForm } from "@wasp/auth/forms/Signup";
+
+export const SignupPage = () => {
+ return (
+
+
+ Signup
+
+
+
+ );
+};
+```
### 🎉 [New Feature] Support for PostgreSQL Extensions
Wasp now supports PostgreSQL extensions! You can enable them in your `main.wasp` file:
diff --git a/waspc/data/Generator/templates/react-app/src/auth/forms/Auth.tsx b/waspc/data/Generator/templates/react-app/src/auth/forms/Auth.tsx
index 9d3911e9b..5c50ba410 100644
--- a/waspc/data/Generator/templates/react-app/src/auth/forms/Auth.tsx
+++ b/waspc/data/Generator/templates/react-app/src/auth/forms/Auth.tsx
@@ -7,6 +7,7 @@ import {
type State,
type CustomizationOptions,
type ErrorMessage,
+ type AdditionalSignupFields,
} from './types'
import { LoginSignupForm } from './internal/common/LoginSignupForm'
import { MessageError, MessageSuccess } from './internal/Message'
@@ -39,9 +40,11 @@ export const AuthContext = createContext({
setSuccessMessage: (successMessage: string | null) => {},
})
-function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
+function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
state: State;
-} & CustomizationOptions) {
+} & CustomizationOptions & {
+ additionalSignupFields?: AdditionalSignupFields;
+}) {
const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const [isLoading, setIsLoading] = useState(false);
@@ -82,6 +85,7 @@ function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
)}
{=# isEmailAuthEnabled =}
diff --git a/waspc/data/Generator/templates/react-app/src/auth/forms/Signup.tsx b/waspc/data/Generator/templates/react-app/src/auth/forms/Signup.tsx
index b48cf4e91..66ffab450 100644
--- a/waspc/data/Generator/templates/react-app/src/auth/forms/Signup.tsx
+++ b/waspc/data/Generator/templates/react-app/src/auth/forms/Signup.tsx
@@ -1,17 +1,23 @@
import Auth from './Auth'
-import { type CustomizationOptions, State } from './types'
+import {
+ type CustomizationOptions,
+ type AdditionalSignupFields,
+ State,
+} from './types'
export function SignupForm({
appearance,
logo,
socialLayout,
-}: CustomizationOptions) {
+ additionalFields,
+}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
return (
)
}
diff --git a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/Form.tsx b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/Form.tsx
index 2095e48b7..24d6c586d 100644
--- a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/Form.tsx
+++ b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/Form.tsx
@@ -14,9 +14,10 @@ export const FormLabel = styled('label', {
display: 'block',
fontSize: '$sm',
fontWeight: '500',
+ marginBottom: '0.5rem',
})
-export const FormInput = styled('input', {
+const commonInputStyles = {
display: 'block',
lineHeight: '1.5rem',
fontSize: '$sm',
@@ -44,7 +45,18 @@ export const FormInput = styled('input', {
paddingBottom: '0.375rem',
paddingLeft: '0.75rem',
paddingRight: '0.75rem',
+ margin: 0,
+}
+export const FormInput = styled('input', commonInputStyles)
+
+export const FormTextarea = styled('textarea', commonInputStyles)
+
+export const FormError = styled('div', {
+ display: 'block',
+ fontSize: '$sm',
+ fontWeight: '500',
+ color: '$formErrorText',
marginTop: '0.5rem',
})
diff --git a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx
index f791b2ab4..bfd4848a1 100644
--- a/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx
+++ b/waspc/data/Generator/templates/react-app/src/auth/forms/internal/common/LoginSignupForm.tsx
@@ -1,10 +1,23 @@
{{={= =}=}}
-import { useContext, type FormEvent } from 'react'
-import { styled } from '../../../../stitches.config'
-import config from '../../../../config.js'
+import { useContext } from 'react'
+import { useForm, UseFormReturn } from 'react-hook-form'
import { AuthContext } from '../../Auth'
-import { Form, FormInput, FormItemGroup, FormLabel, SubmitButton } from '../Form'
+import {
+ Form,
+ FormInput,
+ FormItemGroup,
+ FormLabel,
+ FormError,
+ FormTextarea,
+ SubmitButton,
+} from '../Form'
+import type {
+ AdditionalSignupFields,
+ AdditionalSignupField,
+ AdditionalSignupFieldRenderFn,
+ FormState,
+} from '../../types'
{=# isSocialAuthEnabled =}
import * as SocialIcons from '../social/SocialIcons'
import { SocialButton } from '../social/SocialButton'
@@ -97,12 +110,23 @@ const googleSignInUrl = `${config.apiUrl}{= googleSignInPath =}`
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
{=/ isGitHubAuthEnabled =}
+{=!
+// Since we allow users to add additional fields to the signup form, we don't
+// know the exact shape of the form values. We are assuming that the form values
+// will be a flat object with string values.
+=}
+export type LoginSignupFormFields = {
+ [key: string]: string;
+}
+
export const LoginSignupForm = ({
state,
socialButtonsDirection = 'horizontal',
+ additionalSignupFields,
}: {
- state: 'login' | 'signup',
- socialButtonsDirection?: 'horizontal' | 'vertical';
+ state: 'login' | 'signup'
+ socialButtonsDirection?: 'horizontal' | 'vertical'
+ additionalSignupFields?: AdditionalSignupFields
}) => {
const {
isLoading,
@@ -110,16 +134,19 @@ export const LoginSignupForm = ({
setSuccessMessage,
setIsLoading,
} = useContext(AuthContext)
- const cta = state === 'login' ? 'Log in' : 'Sign up';
+ const isLogin = state === 'login'
+ const cta = isLogin ? 'Log in' : 'Sign up';
{=# isAnyPasswordBasedAuthEnabled =}
const history = useHistory();
const onErrorHandler = (error) => {
setErrorMessage({ title: error.message, description: error.data?.data?.message })
};
{=/ isAnyPasswordBasedAuthEnabled =}
+ const hookForm = useForm()
+ const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
{=# isUsernameAndPasswordAuthEnabled =}
- const { handleSubmit, usernameFieldVal, passwordFieldVal, setUsernameFieldVal, setPasswordFieldVal } = useUsernameAndPassword({
- isLogin: state === 'login',
+ const { handleSubmit } = useUsernameAndPassword({
+ isLogin,
onError: onErrorHandler,
onSuccess() {
history.push('{= onAuthSucceededRedirectTo =}')
@@ -127,10 +154,11 @@ export const LoginSignupForm = ({
});
{=/ isUsernameAndPasswordAuthEnabled =}
{=# isEmailAuthEnabled =}
- const { handleSubmit, emailFieldVal, passwordFieldVal, setEmailFieldVal, setPasswordFieldVal } = useEmail({
- isLogin: state === 'login',
+ const { handleSubmit } = useEmail({
+ isLogin,
onError: onErrorHandler,
showEmailVerificationPending() {
+ hookForm.reset()
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
},
onLoginSuccess() {
@@ -145,13 +173,12 @@ export const LoginSignupForm = ({
});
{=/ isEmailAuthEnabled =}
{=# isAnyPasswordBasedAuthEnabled =}
- async function onSubmit (event: FormEvent) {
- event.preventDefault();
+ async function onSubmit (data) {
setIsLoading(true);
setErrorMessage(null);
setSuccessMessage(null);
try {
- await handleSubmit();
+ await handleSubmit(data);
} finally {
setIsLoading(false);
}
@@ -184,41 +211,49 @@ export const LoginSignupForm = ({
{=/ areBothSocialAndPasswordBasedAuthEnabled =}
{=# isAnyPasswordBasedAuthEnabled =}
-