@main12/auth-login 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -4
- package/dist/components/ui/index.js +37 -8
- package/dist/exports/client.d.ts +3 -0
- package/dist/exports/client.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ plugins: [
|
|
|
36
36
|
- **Smart redirects** — logged-in users are redirected away from auth pages
|
|
37
37
|
- **Google account picker** — always shows account selection by default (`prompt: 'select_account'`)
|
|
38
38
|
- **Zero runtime deps** (Tailwind mode) — Next.js + React are peer dependencies
|
|
39
|
+
- **Multi-language** — built-in English/Spanish, auto-detected per-request, fully overridable via `messages` prop, works with or without next-intl/next-i18next
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
@@ -146,6 +147,41 @@ This redirects:
|
|
|
146
147
|
|
|
147
148
|
---
|
|
148
149
|
|
|
150
|
+
## Testing All Pages
|
|
151
|
+
|
|
152
|
+
Once the catch-all route is set up, every auth page is reachable directly by URL. Some pages require query params to have meaningful content (they're normally reached by clicking through the flow, e.g. signup → verify-otp), so use the URLs below to test each one in isolation:
|
|
153
|
+
|
|
154
|
+
| Page | URL to test | Notes |
|
|
155
|
+
|------|-------------|-------|
|
|
156
|
+
| **Login** | `/auth/login` | 3 sub-states, driven by user interaction: email step (default) → password step or OTP-prompt step, depending on whether the account has a password and `passwordLogin`/`otpLogin` config |
|
|
157
|
+
| **Signup** | `/auth/signup` | Hidden entirely if `allowSignup: false` — falls back to rendering Login instead |
|
|
158
|
+
| **Forgot Password** | `/auth/forgot-password` | No query params required |
|
|
159
|
+
| **Verify OTP** | `/auth/verify-otp?email=test@example.com&purpose=signup` | ⚠️ **Renders a blank page if `email` is missing** — always include `?email=...`. `purpose` can be `login`, `signup`, or `password-reset` (changes the title/subtitle) |
|
|
160
|
+
| **Set Password** | `/auth/set-password` | No query params required |
|
|
161
|
+
|
|
162
|
+
> **Why does `/auth/verify-otp` show a blank page?** The page intentionally renders `null` when there's no `email` in the URL, since in real usage it's always reached via a redirect from signup/login/forgot-password that appends `?email=...&purpose=...`. This is expected — not a bug. Always test it with the full query string above.
|
|
163
|
+
|
|
164
|
+
### Testing the Login page's 3 sub-states
|
|
165
|
+
|
|
166
|
+
The Login page's step is internal UI state, not driven by the URL — to see each one:
|
|
167
|
+
1. **Email step** (default) — just load `/auth/login`.
|
|
168
|
+
2. **Password step** — enter an email that belongs to a user *with* a password set, then submit. Requires `passwordLogin: true` (default).
|
|
169
|
+
3. **OTP-prompt step** — enter an email that belongs to a user *without* a password set (e.g. a Google OAuth-only user), with `otpLogin: true` and `passwordLogin: true`.
|
|
170
|
+
|
|
171
|
+
### Testing Google OAuth
|
|
172
|
+
|
|
173
|
+
Google OAuth buttons only render when `providers.google` is configured with valid credentials (see [Google OAuth](#google-oauth-providers) below). With no credentials, the button is hidden — this is expected in a fresh setup.
|
|
174
|
+
|
|
175
|
+
### Testing different locales
|
|
176
|
+
|
|
177
|
+
Every page also respects the `locale` prop / auto-detection (see [Multi-Language Support](#multi-language-support)). To manually verify a specific language without changing your browser or OS settings, append the plugin's `locale` prop explicitly in your route file, or set the `NEXT_LOCALE` cookie / send an `Accept-Language` header:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
curl -H "Accept-Language: es-MX,es;q=0.9" http://localhost:3000/auth/login
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
149
185
|
## Configuration
|
|
150
186
|
|
|
151
187
|
```ts
|
|
@@ -260,9 +296,91 @@ The `AuthPages` component accepts these props for customization:
|
|
|
260
296
|
showGoogleOAuth={true} // Override Google OAuth visibility
|
|
261
297
|
onPasswordLogin={customLogin} // Custom login handler
|
|
262
298
|
onSignup={customSignup} // Custom signup handler
|
|
299
|
+
locale="es" // Override auto-detected locale (see Multi-Language Support)
|
|
300
|
+
messages={{ es: { login: { title: 'Bienvenido' } } }} // Partial translation overrides
|
|
301
|
+
/>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Multi-Language Support
|
|
307
|
+
|
|
308
|
+
Built-in support for **English (`en`)** and **Spanish (`es`)** — works out of the box, no configuration required. The plugin never depends on next-intl, next-i18next, or any i18n library; it reads the same conventional signals those libraries already write, so it plugs into whatever your app already does automatically.
|
|
309
|
+
|
|
310
|
+
### Automatic locale detection
|
|
311
|
+
|
|
312
|
+
If you don't pass a `locale` prop, it's resolved per-request in this order:
|
|
313
|
+
|
|
314
|
+
1. `NEXT_LOCALE` cookie (written by next-intl, next-i18next, and most i18n routing middlewares by convention)
|
|
315
|
+
2. `Accept-Language` request header
|
|
316
|
+
3. `<html lang="...">` attribute (client-side fallback)
|
|
317
|
+
4. `'en'` (default)
|
|
318
|
+
|
|
319
|
+
This means if your app already uses next-intl or next-i18next, the auth pages automatically match your site's current language — **zero extra code needed**.
|
|
320
|
+
|
|
321
|
+
### Explicit locale override
|
|
322
|
+
|
|
323
|
+
Pass `locale` directly on `<AuthPages />` (or any individual page component) to force a specific language for that render, regardless of auto-detection — useful if you resolve the locale yourself server-side:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
// app/(auth)/auth/[...slug]/page.tsx
|
|
327
|
+
import { AuthPages } from '@main12/auth-login/rsc'
|
|
328
|
+
import { getLocale } from 'next-intl/server' // or however your app resolves it
|
|
329
|
+
|
|
330
|
+
export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
|
|
331
|
+
const { slug } = await params
|
|
332
|
+
const locale = await getLocale()
|
|
333
|
+
return <AuthPages slug={slug} locale={locale} />
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Overriding copy / adding new languages
|
|
338
|
+
|
|
339
|
+
Pass a `messages` object — a partial override, keyed by locale. Any key you don't specify falls back to the built-in English/Spanish copy automatically. You can also introduce entirely new locales this way (e.g. French, Portuguese):
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
<AuthPages
|
|
343
|
+
slug={slug}
|
|
344
|
+
messages={{
|
|
345
|
+
en: {
|
|
346
|
+
login: { title: 'Welcome Back!' }, // override just this one key
|
|
347
|
+
},
|
|
348
|
+
es: {
|
|
349
|
+
login: { title: '¡Bienvenido de nuevo!' },
|
|
350
|
+
},
|
|
351
|
+
fr: { // brand new locale, not built-in — falls back to English for any key not provided
|
|
352
|
+
login: { title: 'Content de vous revoir', subtitle: 'Connectez-vous pour continuer.' },
|
|
353
|
+
signup: { title: 'Créer un compte' },
|
|
354
|
+
},
|
|
355
|
+
}}
|
|
263
356
|
/>
|
|
264
357
|
```
|
|
265
358
|
|
|
359
|
+
Resolution order per key: `messages[locale]` → `messages.en` → built-in `[locale]` dictionary → built-in `en` dictionary.
|
|
360
|
+
|
|
361
|
+
### Available translation keys
|
|
362
|
+
|
|
363
|
+
Each page has its own section — see `UiTranslations` (exported from `@main12/auth-login/client`) for the full shape:
|
|
364
|
+
|
|
365
|
+
| Section | Covers |
|
|
366
|
+
|---------|--------|
|
|
367
|
+
| `login` | Title/subtitle for all 3 sub-states (email, password, OTP-prompt), Google button, form labels, links |
|
|
368
|
+
| `signup` | Title/subtitle, form labels, Google button, terms/privacy links, login link |
|
|
369
|
+
| `forgotPassword` | Title/subtitle, form labels, back-to-login link |
|
|
370
|
+
| `verifyOtp` | Title/subtitle for both variants (code verification vs password-reset), resend button (supports `{seconds}` interpolation) |
|
|
371
|
+
| `setPassword` | Title/subtitle, form labels, password requirements text |
|
|
372
|
+
|
|
373
|
+
### Using translations in custom pages
|
|
374
|
+
|
|
375
|
+
If you're building fully custom pages with the hooks (see [Building Custom Pages](#building-custom-pages)), import `getUiTranslations` directly:
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import { getUiTranslations } from '@main12/auth-login/client'
|
|
379
|
+
|
|
380
|
+
const t = getUiTranslations(locale, messages).login
|
|
381
|
+
// t.title, t.continueWithGoogle, t.emailLabel, etc.
|
|
382
|
+
```
|
|
383
|
+
|
|
266
384
|
---
|
|
267
385
|
|
|
268
386
|
## Individual Page Setup (Advanced)
|
|
@@ -455,15 +573,73 @@ await payload.sendEmail({
|
|
|
455
573
|
|
|
456
574
|
---
|
|
457
575
|
|
|
458
|
-
## Exports
|
|
576
|
+
## Exports & Components Reference
|
|
459
577
|
|
|
460
578
|
| Import path | Contents |
|
|
461
579
|
|-------------|----------|
|
|
462
|
-
| `@main12/auth-login` | Plugin factory, types, server config |
|
|
463
|
-
| `@main12/auth-login/client` | Page components, `AuthPages`, hooks, services, UI utilities |
|
|
464
|
-
| `@main12/auth-login/rsc` |
|
|
580
|
+
| `@main12/auth-login` | Plugin factory (`authLoginPlugin`), types, server config |
|
|
581
|
+
| `@main12/auth-login/client` | Page components, `AuthPages`, hooks, services, UI/locale utilities |
|
|
582
|
+
| `@main12/auth-login/rsc` | Server component `AuthPages` wrapper, email template generators, email translations |
|
|
465
583
|
| `@main12/auth-login/proxy` | Next.js 16 proxy for route redirects |
|
|
466
584
|
|
|
585
|
+
### Components (`@main12/auth-login/client` unless noted)
|
|
586
|
+
|
|
587
|
+
| Component | Description | Typical usage |
|
|
588
|
+
|-----------|-------------|----------------|
|
|
589
|
+
| `AuthPages` (also `@main12/auth-login/rsc`) | Catch-all component — renders the correct page based on `slug`. The `/rsc` version is a server component that auto-resolves plugin config + locale from headers; the `/client` version needs `'use client'` and manual config props | `<AuthPages slug={slug} />` in your `[...slug]/page.tsx` |
|
|
590
|
+
| `LoginPage` | Standalone login page (email → password/OTP flow) | Individual route setup, or full customization via props |
|
|
591
|
+
| `SignupPage` | Standalone signup page | Individual route setup |
|
|
592
|
+
| `ForgotPasswordPage` | Standalone forgot-password page | Individual route setup |
|
|
593
|
+
| `VerifyOtpPage` | Standalone OTP verification page — **requires `?email=...` in the URL** | Reached via redirect from signup/login/forgot-password |
|
|
594
|
+
| `SetPasswordPage` | Standalone set/reset password page | Individual route setup, or post-OTP password creation |
|
|
595
|
+
| `AuthLayout` | Shared card/background/logo/footer chrome used by every page — use it directly when building fully custom pages | `<AuthLayout title="..." subtitle="...">{children}</AuthLayout>` |
|
|
596
|
+
| `AuthClientInit` | Drop into your root layout to sync server plugin config (`style`, Google OAuth flag) to client bundles. Optional — only needed if you hit issues with plugin config not reaching client components in certain bundler setups | `<AuthClientInit />` inside `<body>` |
|
|
597
|
+
| `PoweredBy` | The "Powered by Main 12" footer badge, rendered automatically on every page (configurable via `poweredBy` prop) | Rarely used standalone — mostly internal |
|
|
598
|
+
|
|
599
|
+
### Hooks (`@main12/auth-login/client`)
|
|
600
|
+
|
|
601
|
+
| Hook | Returns | Key inputs |
|
|
602
|
+
|------|---------|------------|
|
|
603
|
+
| `useLoginFlow({ redirectTo, onPasswordLogin })` | `step, email, password, error, isLoading, handleEmailSubmit, handlePasswordSubmit, handleSendOtp, handleEditEmail` | `redirectTo: string`, `onPasswordLogin: (creds) => Promise<void>` |
|
|
604
|
+
| `useForgotPasswordFlow()` | `email, error, isLoading, setEmail, handleSubmit` | none |
|
|
605
|
+
| `useVerifyOtpFlow({ email, purpose, redirectTo })` | `otp, error, isLoading, isResending, resendCooldown, setOtp, handleSubmit, handleResendCode` | `email: string`, `purpose: 'login'\|'signup'\|'password-reset'` |
|
|
606
|
+
| `useSetPasswordFlow({ redirectTo })` | `password, confirmPassword, error, isLoading, strength, setPassword, setConfirmPassword, handleSubmit` | `redirectTo: string` |
|
|
607
|
+
|
|
608
|
+
### Service functions (`@main12/auth-login/client`)
|
|
609
|
+
|
|
610
|
+
Low-level `fetch` wrappers used internally by the hooks — call directly for fully custom flows:
|
|
611
|
+
|
|
612
|
+
| Function | Description |
|
|
613
|
+
|----------|--------------|
|
|
614
|
+
| `checkEmail(email)` | Checks whether an email is already registered |
|
|
615
|
+
| `sendOtp(email, purpose)` | Requests a new OTP code |
|
|
616
|
+
| `verifyOtp(email, otp, purpose)` | Verifies an OTP code, logs the user in on success |
|
|
617
|
+
| `setUserPassword(password)` | Sets/updates the current user's password |
|
|
618
|
+
| `signup(name, email)` | Creates a new account, triggers welcome email + OTP verification |
|
|
619
|
+
| `initiateGoogleLogin()` | Redirects to the Google OAuth flow |
|
|
620
|
+
|
|
621
|
+
### Utilities & config (`@main12/auth-login/client`)
|
|
622
|
+
|
|
623
|
+
| Export | Description |
|
|
624
|
+
|--------|--------------|
|
|
625
|
+
| `initClientConfig(opts)` | Manually sync plugin config into the client bundle (used internally by `AuthPages`/`AuthClientInit`) |
|
|
626
|
+
| `evaluatePasswordStrength(password)` | Returns `{ score, isValid, ... }` — used by the password strength meter |
|
|
627
|
+
| `isPasswordValid(password)` / `MIN_PASSWORD_LENGTH` | Password validation helpers |
|
|
628
|
+
| `getUiTranslations(locale, messages)` | Resolve translated UI copy — see [Multi-Language Support](#multi-language-support) |
|
|
629
|
+
| `uiTranslations` | Raw built-in `{ en, es }` dictionaries, if you need to read them directly |
|
|
630
|
+
| `detectClientLocale()` | Client-side locale auto-detection (cookie → `<html lang>` → `'en'`) |
|
|
631
|
+
|
|
632
|
+
### Email templates (`@main12/auth-login/rsc`)
|
|
633
|
+
|
|
634
|
+
| Export | Description |
|
|
635
|
+
|--------|--------------|
|
|
636
|
+
| `generateWelcomeEmail(params)` | Welcome email after signup |
|
|
637
|
+
| `generateOtpEmail(params)` | OTP code email (login/signup/password-reset) |
|
|
638
|
+
| `generatePasswordResetEmail(params)` | Password reset code email |
|
|
639
|
+
| `generatePasswordChangedEmail(params)` | Confirmation after password change |
|
|
640
|
+
| `getEmailTranslations(locale)` | English/Spanish email copy (separate dictionary from the UI translations above) |
|
|
641
|
+
| `wrapInBaseTemplate(options)` | Wraps any HTML body in the plugin's branded email shell |
|
|
642
|
+
|
|
467
643
|
---
|
|
468
644
|
|
|
469
645
|
## ShadCN Compatibility
|
|
@@ -94,32 +94,61 @@ export const OtpInput = ({ length = 6, value, onChange, onValueChange, isDisable
|
|
|
94
94
|
const inputs = Array.from({
|
|
95
95
|
length
|
|
96
96
|
}, (_, i)=>i);
|
|
97
|
+
const inputRefs = React.useRef([]);
|
|
98
|
+
const focusInput = (index)=>{
|
|
99
|
+
inputRefs.current[index]?.focus();
|
|
100
|
+
inputRefs.current[index]?.select();
|
|
101
|
+
};
|
|
97
102
|
return /*#__PURE__*/ _jsx("div", {
|
|
98
103
|
className: "flex justify-center gap-2",
|
|
99
104
|
children: inputs.map((i)=>/*#__PURE__*/ _jsx("input", {
|
|
105
|
+
ref: (el)=>{
|
|
106
|
+
inputRefs.current[i] = el;
|
|
107
|
+
},
|
|
100
108
|
type: "text",
|
|
101
109
|
inputMode: "numeric",
|
|
110
|
+
autoComplete: "one-time-code",
|
|
102
111
|
maxLength: 1,
|
|
103
112
|
value: value[i] || '',
|
|
104
113
|
disabled: isDisabled,
|
|
105
114
|
autoFocus: autoFocus && i === 0,
|
|
106
115
|
onChange: (e)=>{
|
|
107
|
-
const char = e.target.value.replace(/[^0-9]/g, '').slice(
|
|
116
|
+
const char = e.target.value.replace(/[^0-9]/g, '').slice(-1);
|
|
108
117
|
const newVal = value.split('');
|
|
109
118
|
newVal[i] = char;
|
|
110
|
-
setValue(newVal.join(''));
|
|
111
|
-
// Auto-focus next
|
|
119
|
+
setValue(newVal.join('').slice(0, length));
|
|
120
|
+
// Auto-focus next input once a digit is entered
|
|
112
121
|
if (char && i < length - 1) {
|
|
113
|
-
|
|
114
|
-
next?.focus();
|
|
122
|
+
focusInput(i + 1);
|
|
115
123
|
}
|
|
116
124
|
},
|
|
117
125
|
onKeyDown: (e)=>{
|
|
118
|
-
if (e.key === 'Backspace'
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
if (e.key === 'Backspace') {
|
|
127
|
+
if (!value[i] && i > 0) {
|
|
128
|
+
// Empty box — move to previous box and clear it
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
const newVal = value.split('');
|
|
131
|
+
newVal[i - 1] = '';
|
|
132
|
+
setValue(newVal.join(''));
|
|
133
|
+
focusInput(i - 1);
|
|
134
|
+
}
|
|
135
|
+
} else if (e.key === 'ArrowLeft' && i > 0) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
focusInput(i - 1);
|
|
138
|
+
} else if (e.key === 'ArrowRight' && i < length - 1) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
focusInput(i + 1);
|
|
121
141
|
}
|
|
122
142
|
},
|
|
143
|
+
onPaste: (e)=>{
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
const pasted = e.clipboardData.getData('text').replace(/[^0-9]/g, '').slice(0, length);
|
|
146
|
+
if (!pasted) return;
|
|
147
|
+
setValue(pasted);
|
|
148
|
+
const nextIndex = Math.min(pasted.length, length - 1);
|
|
149
|
+
focusInput(nextIndex);
|
|
150
|
+
},
|
|
151
|
+
onFocus: (e)=>e.target.select(),
|
|
123
152
|
className: "w-12 h-14 text-center text-2xl font-semibold text-gray-900 border-2 border-gray-300 rounded-xl focus:border-[#D5E855] focus:ring-2 focus:ring-[#D5E855]/30 outline-none transition-all bg-white"
|
|
124
153
|
}, i))
|
|
125
154
|
});
|
package/dist/exports/client.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export { useForgotPasswordFlow } from '../auth/application/hooks/useForgotPasswo
|
|
|
7
7
|
export { useSetPasswordFlow } from '../auth/application/hooks/useSetPasswordFlow';
|
|
8
8
|
export { checkEmail, sendOtp, verifyOtp, setUserPassword, signup, initiateGoogleLogin, } from '../auth/application/services/authService';
|
|
9
9
|
export { evaluatePasswordStrength, isPasswordValid, MIN_PASSWORD_LENGTH } from '../auth/domain/passwordRules';
|
|
10
|
+
export { getUiTranslations, uiTranslations } from '../components/ui/translations';
|
|
11
|
+
export type { UiTranslations, DeepPartial } from '../components/ui/translations';
|
|
12
|
+
export { detectClientLocale } from '../components/ui/locale';
|
|
10
13
|
export { default as LoginPage } from '../components/pages/LoginPage';
|
|
11
14
|
export { default as SignupPage } from '../components/pages/SignupPage';
|
|
12
15
|
export { default as ForgotPasswordPage } from '../components/pages/ForgotPasswordPage';
|
package/dist/exports/client.js
CHANGED
|
@@ -14,6 +14,9 @@ export { useSetPasswordFlow } from '../auth/application/hooks/useSetPasswordFlow
|
|
|
14
14
|
export { checkEmail, sendOtp, verifyOtp, setUserPassword, signup, initiateGoogleLogin } from '../auth/application/services/authService.js';
|
|
15
15
|
// Domain utilities
|
|
16
16
|
export { evaluatePasswordStrength, isPasswordValid, MIN_PASSWORD_LENGTH } from '../auth/domain/passwordRules.js';
|
|
17
|
+
// UI translations / locale utilities
|
|
18
|
+
export { getUiTranslations, uiTranslations } from '../components/ui/translations.js';
|
|
19
|
+
export { detectClientLocale } from '../components/ui/locale.js';
|
|
17
20
|
// Page components
|
|
18
21
|
export { default as LoginPage } from '../components/pages/LoginPage.js';
|
|
19
22
|
export { default as SignupPage } from '../components/pages/SignupPage.js';
|
package/package.json
CHANGED