keystone-design-bootstrap 1.0.81 → 1.0.83
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/dist/design_system/sections/index.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tracking/index.d.ts +20 -1
- package/dist/tracking/index.js.map +1 -1
- package/package.json +1 -1
- package/src/design_system/portal/LoginForm.tsx +286 -218
- package/src/design_system/portal/LoginModalController.tsx +4 -2
- package/src/next/layouts/root-layout.tsx +5 -4
- package/src/next/routes/consumer-auth.ts +106 -1
- package/src/tracking/captureEvent.ts +27 -0
|
@@ -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
|
-
<
|
|
243
|
+
<body>
|
|
243
244
|
{gtmBootstrapScript ? (
|
|
244
|
-
<
|
|
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
|
},
|
|
@@ -45,6 +45,10 @@ export type KsEventName =
|
|
|
45
45
|
| 'portal_tab_viewed'
|
|
46
46
|
// Member portal — authentication flow
|
|
47
47
|
| 'portal_login_started'
|
|
48
|
+
| 'portal_login_step_viewed'
|
|
49
|
+
| 'portal_login_step_submitted'
|
|
50
|
+
| 'portal_login_step_advanced'
|
|
51
|
+
| 'portal_login_back_clicked'
|
|
48
52
|
| 'portal_login_identified'
|
|
49
53
|
| 'portal_login_completed'
|
|
50
54
|
| 'portal_login_failed';
|
|
@@ -92,6 +96,29 @@ export type KsEventProperties = {
|
|
|
92
96
|
/** Fired when the portal login modal is opened / login flow starts. */
|
|
93
97
|
portal_login_started: Record<string, never>;
|
|
94
98
|
|
|
99
|
+
/** Fired whenever a login step becomes visible to the user. */
|
|
100
|
+
portal_login_step_viewed: {
|
|
101
|
+
step: 'identifier' | 'signin' | 'signup';
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Fired when the user submits a specific login step. */
|
|
105
|
+
portal_login_step_submitted: {
|
|
106
|
+
step: 'identifier' | 'signin' | 'signup';
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Fired when the flow transitions from one step to another state. */
|
|
110
|
+
portal_login_step_advanced: {
|
|
111
|
+
from_step: 'identifier' | 'signin' | 'signup';
|
|
112
|
+
to_step: 'signin' | 'signup' | 'authenticated';
|
|
113
|
+
reason: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Fired when the user explicitly navigates back to the identifier step. */
|
|
117
|
+
portal_login_back_clicked: {
|
|
118
|
+
from_step: 'signin' | 'signup';
|
|
119
|
+
to_step: 'identifier';
|
|
120
|
+
};
|
|
121
|
+
|
|
95
122
|
/**
|
|
96
123
|
* Fired after the identifier step resolves — we know whether the user
|
|
97
124
|
* already has an account.
|