keystone-design-bootstrap 1.0.82 → 1.0.84

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.
@@ -60,10 +60,12 @@ export function LoginModalController() {
60
60
  // Close the modal once the router transition has fully completed and the
61
61
  // server component has re-rendered with the new authenticated state.
62
62
  useEffect(() => {
63
- if (refreshing && !isPending) {
63
+ if (!(refreshing && !isPending)) return;
64
+ const timeout = window.setTimeout(() => {
64
65
  setOpen(false);
65
66
  setRefreshing(false);
66
- }
67
+ }, 0);
68
+ return () => window.clearTimeout(timeout);
67
69
  }, [refreshing, isPending]);
68
70
 
69
71
  return (
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { Metadata } from 'next';
3
+ import Script from 'next/script';
3
4
 
4
5
  import { HeaderNavigation, FooterHome } from '../../design_system/sections';
5
6
  import { ThemeProvider } from '../../contexts';
@@ -239,12 +240,12 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
239
240
 
240
241
  return (
241
242
  <html lang="en" data-theme={theme}>
242
- <head>
243
+ <body>
243
244
  {gtmBootstrapScript ? (
244
- <script id="google-tag-manager" dangerouslySetInnerHTML={{ __html: gtmBootstrapScript }} />
245
+ <Script id="google-tag-manager">
246
+ {gtmBootstrapScript}
247
+ </Script>
245
248
  ) : null}
246
- </head>
247
- <body>
248
249
  {gtmContainerId ? (
249
250
  <noscript>
250
251
  <iframe
@@ -7,7 +7,7 @@
7
7
  * import { createConsumerAuthHandlers } from 'keystone-design-bootstrap/next/routes/consumer-auth';
8
8
  * export const { POST } = createConsumerAuthHandlers({ NextResponse });
9
9
  *
10
- * Handles: initiate, login, signup, logout
10
+ * Handles: initiate, login, signup, send_code, verify_code, passwordless_auth, logout
11
11
  *
12
12
  * Env (server-side only):
13
13
  * - API_URL (default: http://localhost:3000/api/v1)
@@ -151,6 +151,108 @@ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Res
151
151
  return response;
152
152
  }
153
153
 
154
+ // POST /api/consumer/send_code
155
+ // Sends a 4-digit SMS verification code for passwordless login/signup.
156
+ async function handleSendCode(request: Request, NR: NextResponseLike): Promise<Response> {
157
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
158
+ const { phone } = body;
159
+ if (!phone) return NR.json({ error: 'Phone is required.' }, { status: 422 });
160
+
161
+ const res = await fetch(`${getApiUrl()}/consumer/auth/send_code`, {
162
+ method: 'POST',
163
+ headers: apiHeaders(request),
164
+ body: JSON.stringify({ phone }),
165
+ });
166
+
167
+ const json = await res.json().catch(() => ({}));
168
+ if (!res.ok) {
169
+ return NR.json(
170
+ {
171
+ error: json.error || 'Failed to send verification code. Please try again.',
172
+ code: json.code,
173
+ retry_in_seconds: json.retry_in_seconds,
174
+ },
175
+ { status: res.status }
176
+ );
177
+ }
178
+
179
+ return NR.json({
180
+ resend_available_at: json.data?.resend_available_at ?? null,
181
+ });
182
+ }
183
+
184
+ // POST /api/consumer/verify_code
185
+ // Verifies the SMS code and returns a short-lived verification token.
186
+ async function handleVerifyCode(request: Request, NR: NextResponseLike): Promise<Response> {
187
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
188
+ const { phone, code } = body;
189
+ if (!phone || !code) return NR.json({ error: 'Phone and code are required.' }, { status: 422 });
190
+
191
+ const res = await fetch(`${getApiUrl()}/consumer/auth/verify_code`, {
192
+ method: 'POST',
193
+ headers: apiHeaders(request),
194
+ body: JSON.stringify({ phone, code }),
195
+ });
196
+
197
+ const json = await res.json().catch(() => ({}));
198
+ if (!res.ok) {
199
+ return NR.json(
200
+ { error: json.error || 'Verification failed. Please try again.', code: json.code },
201
+ { status: res.status }
202
+ );
203
+ }
204
+
205
+ return NR.json({
206
+ verification_token: json.data?.verification_token,
207
+ first_name: json.data?.first_name ?? '',
208
+ last_name: json.data?.last_name ?? '',
209
+ email: json.data?.email ?? '',
210
+ });
211
+ }
212
+
213
+ // POST /api/consumer/passwordless_auth
214
+ // Completes passwordless auth and sets HttpOnly JWT cookie.
215
+ async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): Promise<Response> {
216
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
217
+ const { phone, verification_token, first_name, last_name, email } = body;
218
+
219
+ if (!phone) return NR.json({ error: 'Phone is required.' }, { status: 422 });
220
+ if (!verification_token) return NR.json({ error: 'Verification token is required.' }, { status: 422 });
221
+ if (!first_name || !last_name || !email) {
222
+ return NR.json({ error: 'First name, last name, and email are required.' }, { status: 422 });
223
+ }
224
+
225
+ const res = await fetch(`${getApiUrl()}/consumer/auth/passwordless_auth`, {
226
+ method: 'POST',
227
+ headers: apiHeaders(request),
228
+ body: JSON.stringify({
229
+ phone,
230
+ verification_token,
231
+ first_name,
232
+ last_name,
233
+ email,
234
+ }),
235
+ });
236
+
237
+ const json = await res.json().catch(() => ({}));
238
+ if (!res.ok) {
239
+ return NR.json({ error: json.error || 'Authentication failed. Please try again.' }, { status: res.status });
240
+ }
241
+
242
+ const token = json.data?.token;
243
+ if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
244
+
245
+ const response = NR.json({});
246
+ response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
247
+ httpOnly: true,
248
+ secure: process.env.NODE_ENV === 'production',
249
+ sameSite: 'lax',
250
+ path: '/',
251
+ maxAge: COOKIE_MAX_AGE,
252
+ });
253
+ return response;
254
+ }
255
+
154
256
  // POST /api/consumer/logout
155
257
  // Clears the JWT cookie.
156
258
  async function handleLogout(_request: Request, NR: NextResponseLike): Promise<Response> {
@@ -173,6 +275,9 @@ export function createConsumerAuthHandlers({ NextResponse }: { NextResponse: Nex
173
275
  if (action === 'initiate') return handleInitiate(request, NextResponse);
174
276
  if (action === 'login') return handleLogin(request, NextResponse);
175
277
  if (action === 'signup') return handleSignup(request, NextResponse);
278
+ if (action === 'send_code') return handleSendCode(request, NextResponse);
279
+ if (action === 'verify_code') return handleVerifyCode(request, NextResponse);
280
+ if (action === 'passwordless_auth') return handlePasswordlessAuth(request, NextResponse);
176
281
  if (action === 'logout') return handleLogout(request, NextResponse);
177
282
  return NextResponse.json({ error: 'Not found' }, { status: 404 });
178
283
  },