Skip to main content

LoginForm

A modular authentication form component with two operating modes: self-contained (handles login internally via the SDK) and delegation (parent handles authentication). Supports standalone login pages, header dropdowns, checkout flows, and modals.

Usage

The component handles the entire authentication flow internally -- SDK login call, token extraction, GraphQL client header update, viewer fetch, event dispatch, and loading/error states.

import LoginForm from '@/components/propeller/LoginForm';
import { graphqlClient } from '@/lib/api';

<LoginForm
graphqlClient={graphqlClient}
onForgotPasswordClick={() => router.push('/forgot-password')}
onRegisterClick={() => router.push('/register')}
afterLogin={(user, accessToken, refreshToken) => {
// Tokens and user data are already available
router.push('/account');
}}
/>

Delegation mode

When onLoginSubmit is provided, the component does not call the SDK. The parent is responsible for authentication, loading state, and error messages.

const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState('');

<LoginForm
loginLoading={loginLoading}
loginError={loginError}
onForgotPasswordClick={() => router.push('/forgot-password')}
onRegisterClick={() => router.push('/register')}
onLoginSubmit={async (email, password) => {
setLoginLoading(true);
setLoginError('');
try {
await login(email, password);
router.push('/account');
} catch (error) {
setLoginError('Login failed. Please check your credentials.');
} finally {
setLoginLoading(false);
}
}}
/>

Checkout flow

Show a guest checkout option and hide the registration link:

<LoginForm
graphqlClient={graphqlClient}
title="Log in to continue"
displayRegisterLink={false}
displayGuestCheckoutLink={true}
onForgotPasswordClick={() => router.push('/forgot-password')}
onGuestCheckoutClick={() => router.push('/checkout/guest')}
afterLogin={() => router.push('/checkout')}
/>

Header dropdown (compact mode)

When used inside a header dropdown, set accountHeaderLoginForm to render a compact layout with inline forgot-password and register links instead of the full-width sections:

<LoginForm
graphqlClient={graphqlClient}
title=""
accountHeaderLoginForm={true}
onForgotPasswordClick={() => router.push('/forgot-password')}
onRegisterClick={() => router.push('/register')}
afterLogin={() => router.push('/account')}
/>
<LoginForm
graphqlClient={graphqlClient}
title=""
displayForgotPasswordLink={false}
displayRegisterLink={false}
displayGuestCheckoutLink={false}
afterLogin={() => router.push('/account')}
/>

Custom labels

<LoginForm
graphqlClient={graphqlClient}
buttonText="Sign In"
labels={{
email: 'E-mailadres',
password: 'Wachtwoord',
emailPlaceholder: 'naam@voorbeeld.nl',
forgotPassword: 'Wachtwoord vergeten?',
registerText: 'Nog geen account?',
registerLink: 'Registreren',
guestCheckoutLink: 'Ga verder als gast',
}}
onForgotPasswordClick={() => router.push('/forgot-password')}
onRegisterClick={() => router.push('/register')}
/>

Configuration

Core props

PropTypeDefaultDescription
graphqlClientGraphQLClient--SDK client for self-contained login. Required when onLoginSubmit is not provided.
onLoginSubmit(email: string, password: string) => void--Delegation mode: fires on submit, parent handles auth. When provided, graphqlClient is ignored.

Display props

PropTypeDefaultDescription
titlestring"Log in"Form title. Set to "" to hide.
subtitlestring""Subtitle shown below the title.
buttonTextstring"Login"Label for the submit button.
accountHeaderLoginFormbooleantrueWhen true, renders a compact layout suitable for header dropdowns (inline links instead of full-width sections).
displayForgotPasswordLinkbooleantrueShow/hide the forgot password link.
displayRegisterLinkbooleantrueShow/hide the registration link/button.
displayGuestCheckoutLinkbooleantrueShow/hide the guest checkout link.
labelsRecord<string, string>{}Override default label text (see Labels section).

Callback props

PropTypeDescription
onForgotPasswordClick(event?: any) => voidFires when the forgot password link is clicked.
onRegisterClick(event?: any) => voidFires when the register button/link is clicked.
onGuestCheckoutClick(event?: any) => voidFires when the guest checkout link is clicked.
beforeLogin() => voidCalled before the login process starts (both modes).
afterLogin(user: Contact | Customer, accessToken?: string, refreshToken?: string, expiresAt?: string) => voidCalled after successful login with user data and tokens (self-contained mode).

Delegation mode props

These props are only used when onLoginSubmit is provided. In self-contained mode, the component manages its own loading and error states.

PropTypeDefaultDescription
loginLoadingbooleanfalseShows loading spinner on the submit button.
loginErrorstring--Error message displayed in the form.

Labels

KeyDefaultDescription
email"Email"Email field label
password"Password"Password field label
emailPlaceholder"name@example.com"Email input placeholder
passwordPlaceholder"••••••••"Password input placeholder
forgotPassword"Forgot password?"Forgot password link text
registerText"Don't have an account?"Text above the register button
registerLink"Create an Account"Register button text
guestCheckoutLink"Continue as Guest"Guest checkout link text
noAccount"Don't have an account?"Text before register link (compact/header mode only)

Behavior

Self-contained authentication flow

When graphqlClient is provided and onLoginSubmit is absent, the component executes this sequence on form submit:

  1. Calls beforeLogin() callback if provided.
  2. Sets internal loading state to true and clears any previous error.
  3. Creates LoginService and UserService instances from the provided graphqlClient.
  4. Calls loginService.login({ email, password }) which executes the login GraphQL mutation.
  5. Extracts accessToken and refreshToken from the response session.
  6. Updates the graphqlClient headers with Authorization: Bearer {accessToken} so subsequent requests are authenticated.
  7. Calls userService.getViewer({}) to fetch the authenticated user profile (Contact or Customer).
  8. Dispatches a userLoggedIn custom event on window -- this is picked up by AuthContext to update global auth state.
  9. Resets the email and password form fields.
  10. Calls afterLogin(user, accessToken, refreshToken, expirationTime) callback if provided.

If any step fails, the component displays a generic error message: "The credentials you entered don't match our records. Please try again."

Delegation flow

When onLoginSubmit is provided:

  1. Calls beforeLogin() callback if provided.
  2. Calls onLoginSubmit(email, password) -- the parent handles authentication.
  3. Loading state is controlled via loginLoading prop.
  4. Error messages are controlled via loginError prop.

Compact header mode

When accountHeaderLoginForm is true:

  • The forgot password link and register section below the form are hidden.
  • Instead, compact inline links for forgot password and register appear below the submit button.
  • The full-width register button and guest checkout link are not rendered.

Form states

  • Idle: Form fields enabled, submit button shows buttonText.
  • Loading: Form fields disabled, submit button shows a spinner and "Logging in..." text.
  • Error: Red error banner appears between the password field and the submit button.

GraphQL Queries and Mutations

login mutation

The SDK executes this mutation internally via LoginService.login():

mutation login($input: LoginInput!) {
login(input: $input) {
session {
accessToken
refreshToken
expirationTime
email
uid
displayName
}
}
}

Variables:

{
"input": {
"email": "user@example.com",
"password": "password123"
}
}

Response:

{
"data": {
"login": {
"session": {
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"refreshToken": "AMf-vBxG3...",
"expirationTime": "2026-03-25T14:30:00Z",
"email": "user@example.com",
"uid": "abc123",
"displayName": "John Doe"
}
}
}
}

viewer query

The SDK executes this query internally via UserService.getViewer():

query viewer {
viewer {
__typename
... on Contact {
contactId
firstName
lastName
email
phone
mobile
company {
companyId
name
taxNumber
cocNumber
}
addresses {
id
firstName
lastName
street
number
postalCode
city
country
type
isDefault
}
}
... on Customer {
customerId
firstName
lastName
email
phone
mobile
addresses {
id
firstName
lastName
street
number
postalCode
city
country
type
isDefault
}
}
}
}

SDK Services

The component uses two services from propeller-sdk-v2 in self-contained mode:

LoginService

Handles user authentication via the login GraphQL mutation.

import { LoginService, LoginInput, GraphQLClient } from 'propeller-sdk-v2';

const loginService = new LoginService(graphqlClient);

const loginInput: LoginInput = {
email: 'user@example.com',
password: 'password123',
};

const loginResponse = await loginService.login(loginInput);

// loginResponse is a Login object with:
// - loginResponse.session.accessToken (string)
// - loginResponse.session.refreshToken (string)
// - loginResponse.session.expirationTime (string | undefined)

UserService

Fetches the authenticated user's profile after login via the viewer GraphQL query.

import { UserService, GraphQLClient } from 'propeller-sdk-v2';

const userService = new UserService(graphqlClient);

// After setting the Authorization header on graphqlClient:
const user = await userService.getViewer({});

// user is either a Contact (B2B) or Customer (B2C) based on __typename
// Contact has: company, addresses, firstName, lastName, email, etc.
// Customer has: addresses, firstName, lastName, email, etc.