LoginModal

Login and signup modal with email, OTP verification, and social authentication options.

Installation

pnpm add @wandercom/design-system-web

Usage

Loading example...
17 lines
import { LoginModal } from '@wandercom/design-system-web/blocks/login-modal';
import { Button } from '@wandercom/design-system-web/ui/button';

export function Example() {
  return (
    <LoginModal
      trigger={<Button>Log in</Button>}
      onEmailSubmit={(email) => sendOtp(email)}
      onCodeSubmit={(code) => verifyCode(code)}
      onResendCode={() => resendOtp()}
      onGoogleLogin={() => signInWithGoogle()}
      onAppleLogin={() => signInWithApple()}
      onFacebookLogin={() => signInWithFacebook()}
      onPhoneSubmit={(phone) => sendOtp(phone)}
    />
  );
}

Submitting an email transitions directly to a 6-digit OTP verification step. Clicking the phone button first prompts for a phone number, then transitions to OTP verification. The verification description adapts based on the method used.

Async callbacks

Callbacks that return a Promise are handled automatically — the modal shows a loading state during the async operation, advances to the next step on resolve, and displays the error message on reject.

11 lines
<LoginModal
  trigger={<Button>Log in</Button>}
  onEmailSubmit={async (email) => {
    const { digitsKey } = await sendMagicEmail(email);
    store.setDigitsKey(digitsKey);
  }}
  onCodeSubmit={async (code) => {
    const session = await checkMagicDigits(code, digitsKey);
    setUserSession(session);
  }}
/>

Resending the code

On the verification step, clicking "Resend code." confirms the request in place — the prefix becomes "New code sent." and the button counts down, staying disabled until the cooldown expires so the endpoint can't be spammed. If onResendCode returns a Promise, the button reads "Sending…" while it is in flight, and the confirmation and cooldown only start once it resolves; a rejection is shown as an error instead.

5 lines
<LoginModal
  onCodeSubmit={(code) => verifyCode(code)}
  onResendCode={async () => await resendOtp(email)}
  resendCooldownSeconds={60}
/>

All of the strings involved (codeSent, sendingCode, resendCooldownTemplate) are overridable via the labels prop.

The cooldown resets when the user goes back from the verification step or the modal closes, alongside the rest of the resend state — a resend still in flight at that point no longer applies its confirmation or cooldown. Without an onResendCode handler the button is inert: nothing is confirmed and no cooldown starts.

Non-closable mode

For forced authentication flows, set closable={false} to prevent the user from dismissing the modal.

6 lines
<LoginModal
  open={mustAuthenticate}
  closable={false}
  onEmailSubmit={(email) => sendOtp(email)}
  onCodeSubmit={(code) => verifyCode(code)}
/>

Custom title

5 lines
<LoginModal
  trigger={<Button>Join now</Button>}
  title="Welcome to Wander"
  description="Join thousands of travelers worldwide."
/>

Props

open?:

boolean
Controlled open state of the modal.

onOpenChange?:

(open: boolean) => void
Callback when the open state changes.

defaultOpen?:

boolean
Uncontrolled default open state.

trigger?:

ReactNode
Optional trigger element that opens the modal when clicked.

title?:

string
Title text displayed in the modal header. Defaults to "Log in or sign up".

description?:

string
Description text displayed below the title.

onEmailSubmit?:

(email: string) => void | Promise<void>
Callback when the email form is submitted. Returns a Promise for async loading and error handling. Transitions to OTP verification on resolve.

onCodeSubmit?:

(code: string) => void | Promise<void>
Callback when the OTP verification code is submitted. Returns a Promise for async loading and error handling. Closes modal on resolve.

onResendCode?:

() => void | Promise<void>
Callback when the resend code link is clicked. Returns a Promise for a sending state and error handling. Shows a confirmation and starts the resend cooldown on resolve.

resendCooldownSeconds?:

number
Seconds the resend button stays disabled after each successful resend, counting down in the button label. Set to 0 to disable the cooldown. Defaults to 30.

onGoogleLogin?:

() => void
Callback when the Google login button is clicked.

onAppleLogin?:

() => void
Callback when the Apple login button is clicked.

onFacebookLogin?:

() => void
Callback when the Facebook login button is clicked.

onPhoneSubmit?:

(phone: string) => void | Promise<void>
Callback when the phone number is submitted. Returns a Promise for async loading and error handling. Transitions to OTP verification on resolve.

companyName?:

string
Company name displayed in the terms footer. Falls back to "our" if not provided. Defaults to "Wander".

termsUrl?:

string
URL for the Terms of Service link. Defaults to "/terms".

privacyUrl?:

string
URL for the Privacy Policy link. Defaults to "/privacy".

className?:

string
Additional CSS classes to apply to the modal content.

error?:

string
Error message to display. Takes precedence over internally managed errors from rejected async callbacks.

closable?:

boolean
Whether the modal can be dismissed via close button, overlay click, or escape key. Set to false for forced authentication flows. Defaults to true.

onBack?:

() => void
Callback when the user navigates back from the verification step.
LoginModal