@sneekin/ui 0.1.2 → 0.2.0
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 +59 -108
- package/dist/component.d.ts +47 -0
- package/dist/fetch-handlers.d.ts +30 -0
- package/dist/index.d.ts +5 -160
- package/dist/index.js +304 -97
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -97
- package/dist/index.mjs.map +1 -1
- package/dist/state.d.ts +63 -0
- package/dist/theme.d.ts +19 -0
- package/dist/use-sneek-otp.d.ts +40 -0
- package/package.json +9 -9
- package/dist/index.d.mts +0 -160
package/dist/index.mjs
CHANGED
|
@@ -135,21 +135,124 @@ function useSneekOtp(handlers) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// src/component.tsx
|
|
139
|
+
import {
|
|
140
|
+
useEffect,
|
|
141
|
+
useId
|
|
142
|
+
} from "react";
|
|
143
|
+
|
|
144
|
+
// src/theme.ts
|
|
145
|
+
var SNEEK_STYLE_ID = "sneek-otp-styles";
|
|
146
|
+
var SNEEK_ACCENT_DARK = "#f86513";
|
|
147
|
+
var SNEEK_ACCENT_LIGHT = "#b83f06";
|
|
148
|
+
function buildStyles(accent) {
|
|
149
|
+
const darkAccent = accent ?? SNEEK_ACCENT_DARK;
|
|
150
|
+
const lightAccent = accent ?? SNEEK_ACCENT_LIGHT;
|
|
151
|
+
const light = `
|
|
152
|
+
--sneek-surface: #ffffff;
|
|
153
|
+
--sneek-surface-2: #f4f1ed;
|
|
154
|
+
--sneek-border: #e3ddd5;
|
|
155
|
+
--sneek-text: #1c1917;
|
|
156
|
+
--sneek-text-muted: #57534e;
|
|
157
|
+
--sneek-accent: ${lightAccent};
|
|
158
|
+
--sneek-accent-soft: color-mix(in srgb, ${lightAccent} 12%, transparent);
|
|
159
|
+
--sneek-accent-contrast: #ffffff;
|
|
160
|
+
--sneek-danger: #b0242a;
|
|
161
|
+
--sneek-shadow: 0 3px 3px -2px rgb(0 0 0 / 0.10), 0 3px 4px rgb(0 0 0 / 0.07),
|
|
162
|
+
0 1px 8px rgb(0 0 0 / 0.06);`;
|
|
163
|
+
const dark = `
|
|
164
|
+
--sneek-surface: #17150f;
|
|
165
|
+
--sneek-surface-2: #201d16;
|
|
166
|
+
--sneek-border: rgba(255, 255, 255, 0.08);
|
|
167
|
+
--sneek-text: #faf8f6;
|
|
168
|
+
--sneek-text-muted: #a8a29b;
|
|
169
|
+
--sneek-accent: ${darkAccent};
|
|
170
|
+
--sneek-accent-soft: color-mix(in srgb, ${darkAccent} 16%, transparent);
|
|
171
|
+
--sneek-accent-contrast: #1a0d04;
|
|
172
|
+
--sneek-danger: #d43a3c;
|
|
173
|
+
--sneek-shadow: none;`;
|
|
174
|
+
return `
|
|
175
|
+
.sneek-otp {${light}
|
|
176
|
+
--sneek-ease: cubic-bezier(0, 0, 0.2, 1);
|
|
177
|
+
}
|
|
178
|
+
@media (prefers-color-scheme: dark) {
|
|
179
|
+
.sneek-otp:not([data-theme="light"]) {${dark}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
.sneek-otp[data-theme="dark"] {${dark}
|
|
183
|
+
}
|
|
184
|
+
.sneek-otp[data-theme="light"] {${light}
|
|
185
|
+
}
|
|
186
|
+
.sneek-otp *, .sneek-otp *::before, .sneek-otp *::after { box-sizing: border-box; }
|
|
187
|
+
.sneek-otp-input:focus-visible,
|
|
188
|
+
.sneek-otp-button:focus-visible,
|
|
189
|
+
.sneek-otp-link:focus-visible {
|
|
190
|
+
outline: 2px solid var(--sneek-accent);
|
|
191
|
+
outline-offset: 2px;
|
|
192
|
+
}
|
|
193
|
+
.sneek-otp-input:focus {
|
|
194
|
+
border-color: var(--sneek-accent);
|
|
195
|
+
}
|
|
196
|
+
.sneek-otp-button:not(:disabled):hover {
|
|
197
|
+
filter: brightness(1.08);
|
|
198
|
+
}
|
|
199
|
+
.sneek-otp-link:not(:disabled):hover {
|
|
200
|
+
text-decoration: underline;
|
|
201
|
+
}
|
|
202
|
+
@media (prefers-reduced-motion: reduce) {
|
|
203
|
+
.sneek-otp * { transition-duration: 0.01ms !important; }
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
138
208
|
// src/component.tsx
|
|
139
209
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
210
|
+
function SneekMark() {
|
|
211
|
+
return /* @__PURE__ */ jsxs(
|
|
212
|
+
"svg",
|
|
213
|
+
{
|
|
214
|
+
width: "36",
|
|
215
|
+
height: "36",
|
|
216
|
+
viewBox: "0 0 32 32",
|
|
217
|
+
fill: "none",
|
|
218
|
+
role: "img",
|
|
219
|
+
"aria-label": "Sneek",
|
|
220
|
+
children: [
|
|
221
|
+
/* @__PURE__ */ jsx("path", { d: "M21.3 1 29.3 16H13.3L21.3 1Z", fill: "var(--sneek-accent)" }),
|
|
222
|
+
/* @__PURE__ */ jsx("path", { d: "M2.7 17.5H18.7L10.7 31 2.7 17.5Z", fill: "var(--sneek-accent)" })
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
}
|
|
140
227
|
function SneekOtpLogin(props) {
|
|
141
228
|
const {
|
|
142
229
|
title = "Sign in",
|
|
143
|
-
subtitle = "
|
|
144
|
-
identifierLabel = "Email or mobile
|
|
145
|
-
identifierPlaceholder = "you@
|
|
146
|
-
|
|
230
|
+
subtitle = "We will send you a one-time code.",
|
|
231
|
+
identifierLabel = "Email or mobile",
|
|
232
|
+
identifierPlaceholder = "you@company.com or +91\u2026",
|
|
233
|
+
theme = "auto",
|
|
234
|
+
accentColor,
|
|
147
235
|
logo,
|
|
236
|
+
showBranding = true,
|
|
148
237
|
style,
|
|
149
238
|
className,
|
|
150
239
|
...handlers
|
|
151
240
|
} = props;
|
|
152
241
|
const otp = useSneekOtp(handlers);
|
|
242
|
+
const uid = useId();
|
|
243
|
+
const identifierId = `${uid}-identifier`;
|
|
244
|
+
const codeId = `${uid}-code`;
|
|
245
|
+
const statusId = `${uid}-status`;
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (typeof document === "undefined") return;
|
|
248
|
+
let el = document.getElementById(SNEEK_STYLE_ID);
|
|
249
|
+
if (!el) {
|
|
250
|
+
el = document.createElement("style");
|
|
251
|
+
el.id = SNEEK_STYLE_ID;
|
|
252
|
+
document.head.appendChild(el);
|
|
253
|
+
}
|
|
254
|
+
el.textContent = buildStyles(accentColor);
|
|
255
|
+
}, [accentColor]);
|
|
153
256
|
const onIdentifySubmit = (event) => {
|
|
154
257
|
event.preventDefault();
|
|
155
258
|
void otp.sendOtp();
|
|
@@ -158,123 +261,228 @@ function SneekOtpLogin(props) {
|
|
|
158
261
|
event.preventDefault();
|
|
159
262
|
void otp.verify();
|
|
160
263
|
};
|
|
161
|
-
const
|
|
264
|
+
const label = {
|
|
265
|
+
display: "flex",
|
|
266
|
+
flexDirection: "column",
|
|
267
|
+
gap: 8,
|
|
268
|
+
fontSize: 14,
|
|
269
|
+
lineHeight: "20px",
|
|
270
|
+
color: "var(--sneek-text-muted)"
|
|
271
|
+
};
|
|
272
|
+
const input = {
|
|
162
273
|
padding: "12px 14px",
|
|
163
|
-
border: "1px solid
|
|
164
|
-
borderRadius:
|
|
165
|
-
|
|
274
|
+
border: "1px solid var(--sneek-border)",
|
|
275
|
+
borderRadius: 12,
|
|
276
|
+
background: "var(--sneek-surface-2)",
|
|
277
|
+
color: "var(--sneek-text)",
|
|
278
|
+
fontSize: 15,
|
|
166
279
|
width: "100%",
|
|
167
|
-
|
|
168
|
-
|
|
280
|
+
outline: "none",
|
|
281
|
+
transition: "border-color 200ms var(--sneek-ease)"
|
|
169
282
|
};
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
padding: "12px 14px",
|
|
283
|
+
const button = {
|
|
284
|
+
padding: "12px 18px",
|
|
173
285
|
border: "none",
|
|
174
|
-
borderRadius:
|
|
175
|
-
background:
|
|
176
|
-
color: "
|
|
177
|
-
fontSize:
|
|
178
|
-
fontWeight:
|
|
286
|
+
borderRadius: 9999,
|
|
287
|
+
background: "var(--sneek-accent)",
|
|
288
|
+
color: "var(--sneek-accent-contrast)",
|
|
289
|
+
fontSize: 15,
|
|
290
|
+
fontWeight: 500,
|
|
179
291
|
cursor: otp.isBusy ? "not-allowed" : "pointer",
|
|
180
292
|
opacity: otp.isBusy ? 0.6 : 1,
|
|
181
|
-
width: "100%"
|
|
293
|
+
width: "100%",
|
|
294
|
+
transition: "filter 200ms var(--sneek-ease), opacity 200ms var(--sneek-ease)"
|
|
182
295
|
};
|
|
183
|
-
const
|
|
296
|
+
const link = {
|
|
184
297
|
border: "none",
|
|
185
298
|
background: "transparent",
|
|
186
|
-
color:
|
|
187
|
-
cursor: "pointer",
|
|
299
|
+
color: "var(--sneek-accent)",
|
|
300
|
+
cursor: otp.isBusy ? "not-allowed" : "pointer",
|
|
188
301
|
font: "inherit",
|
|
189
|
-
fontSize:
|
|
302
|
+
fontSize: 14,
|
|
190
303
|
padding: 0
|
|
191
304
|
};
|
|
192
|
-
const
|
|
193
|
-
background: "rgba(255,107,94,0.12)",
|
|
194
|
-
color: "#c0392b",
|
|
305
|
+
const notice = {
|
|
195
306
|
padding: "10px 12px",
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
307
|
+
borderRadius: 12,
|
|
308
|
+
background: "var(--sneek-surface-2)",
|
|
309
|
+
border: "1px solid var(--sneek-border)",
|
|
310
|
+
fontSize: 14,
|
|
311
|
+
color: "var(--sneek-text-muted)"
|
|
199
312
|
};
|
|
200
313
|
return /* @__PURE__ */ jsxs(
|
|
201
314
|
"div",
|
|
202
315
|
{
|
|
203
|
-
className,
|
|
316
|
+
className: className ? `sneek-otp ${className}` : "sneek-otp",
|
|
317
|
+
"data-theme": theme === "auto" ? void 0 : theme,
|
|
204
318
|
style: {
|
|
205
319
|
maxWidth: 400,
|
|
206
320
|
margin: "0 auto",
|
|
207
|
-
padding:
|
|
208
|
-
border: "1px solid
|
|
321
|
+
padding: 24,
|
|
322
|
+
border: "1px solid var(--sneek-border)",
|
|
209
323
|
borderRadius: 16,
|
|
210
|
-
|
|
324
|
+
background: "var(--sneek-surface)",
|
|
325
|
+
boxShadow: "var(--sneek-shadow)",
|
|
326
|
+
color: "var(--sneek-text)",
|
|
327
|
+
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
328
|
+
lineHeight: 1.5,
|
|
211
329
|
...style
|
|
212
330
|
},
|
|
213
331
|
children: [
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
332
|
+
logo === null ? null : logo ?? /* @__PURE__ */ jsx(SneekMark, {}),
|
|
333
|
+
/* @__PURE__ */ jsx(
|
|
334
|
+
"h1",
|
|
335
|
+
{
|
|
336
|
+
style: {
|
|
337
|
+
fontSize: 21,
|
|
338
|
+
lineHeight: "28px",
|
|
339
|
+
fontWeight: 500,
|
|
340
|
+
letterSpacing: "-0.01em",
|
|
341
|
+
margin: "16px 0 0"
|
|
342
|
+
},
|
|
343
|
+
children: title
|
|
344
|
+
}
|
|
345
|
+
),
|
|
346
|
+
subtitle ? /* @__PURE__ */ jsx(
|
|
347
|
+
"p",
|
|
348
|
+
{
|
|
349
|
+
style: {
|
|
350
|
+
fontSize: 14,
|
|
351
|
+
lineHeight: "20px",
|
|
352
|
+
color: "var(--sneek-text-muted)",
|
|
353
|
+
margin: "8px 0 0"
|
|
354
|
+
},
|
|
355
|
+
children: subtitle
|
|
356
|
+
}
|
|
357
|
+
) : null,
|
|
358
|
+
/* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", id: statusId, style: { marginTop: 32 }, children: otp.error ? /* @__PURE__ */ jsx(
|
|
359
|
+
"div",
|
|
360
|
+
{
|
|
361
|
+
role: "alert",
|
|
362
|
+
style: {
|
|
363
|
+
...notice,
|
|
364
|
+
color: "var(--sneek-danger)",
|
|
365
|
+
borderColor: "var(--sneek-danger)"
|
|
366
|
+
},
|
|
367
|
+
children: otp.error
|
|
368
|
+
}
|
|
369
|
+
) : otp.step === "verify" ? /* @__PURE__ */ jsxs("div", { style: notice, children: [
|
|
370
|
+
otp.channels.length > 0 ? `Code sent by ${formatChannels(otp.channels)}.` : "We sent you a code.",
|
|
371
|
+
otp.expiresInSeconds > 0 ? ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.` : ""
|
|
372
|
+
] }) : null }),
|
|
373
|
+
otp.step === "identify" ? /* @__PURE__ */ jsxs(
|
|
374
|
+
"form",
|
|
375
|
+
{
|
|
376
|
+
onSubmit: onIdentifySubmit,
|
|
377
|
+
style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
|
|
378
|
+
children: [
|
|
379
|
+
/* @__PURE__ */ jsxs("label", { style: label, htmlFor: identifierId, children: [
|
|
380
|
+
identifierLabel,
|
|
381
|
+
/* @__PURE__ */ jsx(
|
|
382
|
+
"input",
|
|
383
|
+
{
|
|
384
|
+
id: identifierId,
|
|
385
|
+
className: "sneek-otp-input",
|
|
386
|
+
type: "text",
|
|
387
|
+
value: otp.identifier,
|
|
388
|
+
onChange: (e) => otp.setIdentifier(e.target.value),
|
|
389
|
+
placeholder: identifierPlaceholder,
|
|
390
|
+
autoComplete: "username",
|
|
391
|
+
autoFocus: true,
|
|
392
|
+
required: true,
|
|
393
|
+
"aria-describedby": statusId,
|
|
394
|
+
style: input
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
] }),
|
|
398
|
+
/* @__PURE__ */ jsx(
|
|
399
|
+
"button",
|
|
400
|
+
{
|
|
401
|
+
type: "submit",
|
|
402
|
+
className: "sneek-otp-button",
|
|
403
|
+
disabled: otp.isBusy,
|
|
404
|
+
style: button,
|
|
405
|
+
children: otp.isSending ? "Sending code\u2026" : "Send code"
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
) : /* @__PURE__ */ jsxs(
|
|
411
|
+
"form",
|
|
412
|
+
{
|
|
413
|
+
onSubmit: onVerifySubmit,
|
|
414
|
+
style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
|
|
415
|
+
children: [
|
|
416
|
+
/* @__PURE__ */ jsxs("label", { style: label, htmlFor: codeId, children: [
|
|
417
|
+
"Verification code",
|
|
418
|
+
/* @__PURE__ */ jsx(
|
|
419
|
+
"input",
|
|
420
|
+
{
|
|
421
|
+
id: codeId,
|
|
422
|
+
className: "sneek-otp-input",
|
|
423
|
+
type: "text",
|
|
424
|
+
inputMode: "numeric",
|
|
425
|
+
autoComplete: "one-time-code",
|
|
426
|
+
value: otp.code,
|
|
427
|
+
onChange: (e) => otp.setCode(e.target.value),
|
|
428
|
+
placeholder: "6-digit code",
|
|
429
|
+
autoFocus: true,
|
|
430
|
+
required: true,
|
|
431
|
+
"aria-describedby": statusId,
|
|
432
|
+
style: { ...input, letterSpacing: "0.3em" }
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
] }),
|
|
436
|
+
/* @__PURE__ */ jsx(
|
|
437
|
+
"button",
|
|
438
|
+
{
|
|
439
|
+
type: "submit",
|
|
440
|
+
className: "sneek-otp-button",
|
|
441
|
+
disabled: otp.isBusy,
|
|
442
|
+
style: button,
|
|
443
|
+
children: otp.isVerifying ? "Verifying\u2026" : "Verify and sign in"
|
|
444
|
+
}
|
|
445
|
+
),
|
|
446
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 }, children: [
|
|
447
|
+
/* @__PURE__ */ jsx(
|
|
448
|
+
"button",
|
|
449
|
+
{
|
|
450
|
+
type: "button",
|
|
451
|
+
className: "sneek-otp-link",
|
|
452
|
+
onClick: otp.back,
|
|
453
|
+
disabled: otp.isBusy,
|
|
454
|
+
style: link,
|
|
455
|
+
children: "Use a different contact"
|
|
456
|
+
}
|
|
457
|
+
),
|
|
458
|
+
/* @__PURE__ */ jsx(
|
|
459
|
+
"button",
|
|
460
|
+
{
|
|
461
|
+
type: "button",
|
|
462
|
+
className: "sneek-otp-link",
|
|
463
|
+
onClick: () => void otp.resend(),
|
|
464
|
+
disabled: otp.isBusy,
|
|
465
|
+
style: link,
|
|
466
|
+
children: "Resend code"
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
] })
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
showBranding ? /* @__PURE__ */ jsx(
|
|
474
|
+
"p",
|
|
475
|
+
{
|
|
476
|
+
style: {
|
|
477
|
+
margin: "32px 0 0",
|
|
478
|
+
textAlign: "center",
|
|
479
|
+
fontSize: 12,
|
|
480
|
+
lineHeight: "16px",
|
|
481
|
+
color: "var(--sneek-text-muted)"
|
|
482
|
+
},
|
|
483
|
+
children: "Secured by Sneek"
|
|
484
|
+
}
|
|
485
|
+
) : null
|
|
278
486
|
]
|
|
279
487
|
}
|
|
280
488
|
);
|
|
@@ -308,6 +516,8 @@ function createFetchHandlers(options = {}) {
|
|
|
308
516
|
};
|
|
309
517
|
}
|
|
310
518
|
export {
|
|
519
|
+
SNEEK_ACCENT_DARK,
|
|
520
|
+
SNEEK_ACCENT_LIGHT,
|
|
311
521
|
SneekOtpLogin,
|
|
312
522
|
createFetchHandlers,
|
|
313
523
|
formatChannels,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/use-sneek-otp.ts","../src/state.ts","../src/component.tsx","../src/fetch-handlers.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\nimport {\n initialOtpState,\n otpReducer,\n toMessage,\n type RequestOtpResult,\n type SneekOtpState,\n} from './state';\n\n/**\n * Partner-supplied transport. These call the *partner's own backend*, which in\n * turn talks to the Sneek API with the secret server-side key. The browser\n * never sees a Sneek API key — that is the whole security model of this package.\n */\nexport interface SneekOtpHandlers<TResult = unknown> {\n /** Ask the partner backend to send an OTP to `identifier`. */\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n /** Verify the code with the partner backend; resolve with the auth result. */\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n /** Called after a successful verification with the partner's result. */\n onSuccess?: (result: TResult) => void;\n /** Called on any error (request or verify). */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseSneekOtp extends SneekOtpState {\n setIdentifier: (value: string) => void;\n setCode: (value: string) => void;\n /** Submit the identify step — triggers `requestOtp`. */\n sendOtp: () => Promise<void>;\n /** Submit the verify step — triggers `verifyOtp`. */\n verify: () => Promise<void>;\n /** Go back to the identify screen (e.g. \"use a different email\"). */\n back: () => void;\n /** Re-send the OTP to the same identifier. */\n resend: () => Promise<void>;\n /** Convenience booleans. */\n isSending: boolean;\n isVerifying: boolean;\n isBusy: boolean;\n}\n\nconst DEFAULT_REQUEST_ERROR = 'Could not send the code. Please try again.';\nconst DEFAULT_VERIFY_ERROR = 'That code was not correct. Please try again.';\n\n/**\n * Headless hook implementing the passwordless OTP login flow. Bring your own\n * markup, or use the {@link SneekOtpLogin} component for a styled default.\n */\nexport function useSneekOtp<TResult = unknown>(\n handlers: SneekOtpHandlers<TResult>,\n): UseSneekOtp {\n const [state, dispatch] = useReducer(otpReducer, initialOtpState);\n\n const { requestOtp, verifyOtp, onSuccess, onError } = handlers;\n\n const setIdentifier = useCallback((value: string) => {\n dispatch({ type: 'set_identifier', value });\n }, []);\n\n const setCode = useCallback((value: string) => {\n dispatch({ type: 'set_code', value });\n }, []);\n\n const runRequest = useCallback(\n async (identifier: string) => {\n const trimmed = identifier.trim();\n if (!trimmed) {\n dispatch({ type: 'request_error', message: 'Enter your email or mobile number.' });\n return;\n }\n dispatch({ type: 'request_start' });\n try {\n const result = await requestOtp(trimmed);\n dispatch({ type: 'request_success', result });\n } catch (error) {\n dispatch({ type: 'request_error', message: toMessage(error, DEFAULT_REQUEST_ERROR) });\n onError?.(error);\n }\n },\n [requestOtp, onError],\n );\n\n const sendOtp = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);\n\n const resend = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);\n\n const verify = useCallback(async () => {\n const code = state.code.trim();\n if (!code) {\n dispatch({ type: 'verify_error', message: 'Enter the code you received.' });\n return;\n }\n dispatch({ type: 'verify_start' });\n try {\n const result = await verifyOtp({ requestId: state.requestId, code });\n onSuccess?.(result);\n } catch (error) {\n dispatch({ type: 'verify_error', message: toMessage(error, DEFAULT_VERIFY_ERROR) });\n onError?.(error);\n }\n }, [verifyOtp, state.code, state.requestId, onSuccess, onError]);\n\n const back = useCallback(() => {\n dispatch({ type: 'back_to_identify' });\n }, []);\n\n return {\n ...state,\n setIdentifier,\n setCode,\n sendOtp,\n verify,\n back,\n resend,\n isSending: state.status === 'sending',\n isVerifying: state.status === 'verifying',\n isBusy: state.status !== 'idle',\n };\n}\n","/**\n * Framework-agnostic state machine for the Sneek passwordless OTP flow.\n *\n * Kept free of React so it can be unit-tested in isolation and reused by other\n * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a\n * thin wrapper around this reducer.\n */\n\nexport type SneekChannel = 'sms' | 'whatsapp' | 'email';\n\n/** Which screen of the two-step flow is showing. */\nexport type SneekOtpStep = 'identify' | 'verify';\n\n/** Async lifecycle for the in-flight request. */\nexport type SneekOtpStatus = 'idle' | 'sending' | 'verifying';\n\nexport interface SneekOtpState {\n step: SneekOtpStep;\n status: SneekOtpStatus;\n /** The email / mobile / username the user typed. */\n identifier: string;\n /** The OTP code the user typed on the verify screen. */\n code: string;\n /** Opaque id returned by the partner backend, replayed on verify. */\n requestId: string;\n /** Channels the OTP was actually delivered over. */\n channels: SneekChannel[];\n /** Seconds until the OTP expires (from the request response). */\n expiresInSeconds: number;\n /** User-facing error message, or null. */\n error: string | null;\n}\n\nexport const initialOtpState: SneekOtpState = {\n step: 'identify',\n status: 'idle',\n identifier: '',\n code: '',\n requestId: '',\n channels: [],\n expiresInSeconds: 0,\n error: null,\n};\n\nexport interface RequestOtpResult {\n requestId: string;\n channels?: SneekChannel[];\n expiresInSeconds?: number;\n}\n\nexport type SneekOtpAction =\n | { type: 'set_identifier'; value: string }\n | { type: 'set_code'; value: string }\n | { type: 'request_start' }\n | { type: 'request_success'; result: RequestOtpResult }\n | { type: 'request_error'; message: string }\n | { type: 'verify_start' }\n | { type: 'verify_error'; message: string }\n | { type: 'reset' }\n | { type: 'back_to_identify' };\n\nconst isBusy = (status: SneekOtpStatus): boolean => status !== 'idle';\n\nexport function otpReducer(\n state: SneekOtpState,\n action: SneekOtpAction,\n): SneekOtpState {\n switch (action.type) {\n case 'set_identifier':\n return { ...state, identifier: action.value, error: null };\n\n case 'set_code':\n return { ...state, code: action.value, error: null };\n\n case 'request_start':\n // Guard against double submits while a request is already in flight.\n if (isBusy(state.status)) return state;\n return { ...state, status: 'sending', error: null };\n\n case 'request_success':\n return {\n ...state,\n step: 'verify',\n status: 'idle',\n code: '',\n requestId: action.result.requestId,\n channels: action.result.channels ?? [],\n expiresInSeconds: action.result.expiresInSeconds ?? 0,\n error: null,\n };\n\n case 'request_error':\n return { ...state, status: 'idle', error: action.message };\n\n case 'verify_start':\n if (isBusy(state.status)) return state;\n return { ...state, status: 'verifying', error: null };\n\n case 'verify_error':\n return { ...state, status: 'idle', error: action.message };\n\n case 'back_to_identify':\n return {\n ...state,\n step: 'identify',\n status: 'idle',\n code: '',\n requestId: '',\n channels: [],\n expiresInSeconds: 0,\n error: null,\n };\n\n case 'reset':\n return { ...initialOtpState };\n\n default:\n return state;\n }\n}\n\nconst CHANNEL_LABELS: Record<SneekChannel, string> = {\n sms: 'SMS',\n whatsapp: 'WhatsApp',\n email: 'email',\n};\n\n/** \"SMS, WhatsApp\" — human label for the channels an OTP was sent over. */\nexport function formatChannels(channels: SneekChannel[]): string {\n return channels.map((c) => CHANNEL_LABELS[c] ?? c).join(', ');\n}\n\n/** Normalize any thrown value into a user-facing message. */\nexport function toMessage(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n return fallback;\n}\n","import { type CSSProperties, type FormEvent, type ReactNode } from 'react';\nimport { formatChannels } from './state';\nimport { useSneekOtp, type SneekOtpHandlers } from './use-sneek-otp';\n\nexport interface SneekOtpLoginProps<TResult = unknown>\n extends SneekOtpHandlers<TResult> {\n /** Heading shown above the form. @default 'Sign in' */\n title?: string;\n /** Sub-heading. @default 'Passwordless login powered by Sneek' */\n subtitle?: ReactNode;\n /** Label for the identifier input. */\n identifierLabel?: string;\n /** Placeholder for the identifier input. */\n identifierPlaceholder?: string;\n /** Brand colour for the primary button. @default '#56d3b5' */\n accentColor?: string;\n /** Optional logo rendered above the title. */\n logo?: ReactNode;\n /** Override the outer container style. */\n style?: CSSProperties;\n /** Extra className on the outer container. */\n className?: string;\n}\n\n/**\n * Drop-in, dependency-free passwordless OTP login card. The component never\n * receives a Sneek API key; it calls the partner-supplied handlers, which talk\n * to the partner backend. Use {@link createFetchHandlers} for the common case.\n */\nexport function SneekOtpLogin<TResult = unknown>(\n props: SneekOtpLoginProps<TResult>,\n): ReactNode {\n const {\n title = 'Sign in',\n subtitle = 'Passwordless login powered by Sneek',\n identifierLabel = 'Email or mobile number',\n identifierPlaceholder = 'you@example.com or +9198xxxxxxxx',\n accentColor = '#56d3b5',\n logo,\n style,\n className,\n ...handlers\n } = props;\n\n const otp = useSneekOtp<TResult>(handlers);\n\n const onIdentifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.sendOtp();\n };\n\n const onVerifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.verify();\n };\n\n const inputStyle: CSSProperties = {\n padding: '12px 14px',\n border: '1px solid rgba(0,0,0,0.15)',\n borderRadius: 10,\n fontSize: '0.95rem',\n width: '100%',\n boxSizing: 'border-box',\n outline: 'none',\n };\n\n const buttonStyle: CSSProperties = {\n marginTop: 4,\n padding: '12px 14px',\n border: 'none',\n borderRadius: 10,\n background: accentColor,\n color: '#04231d',\n fontSize: '1rem',\n fontWeight: 600,\n cursor: otp.isBusy ? 'not-allowed' : 'pointer',\n opacity: otp.isBusy ? 0.6 : 1,\n width: '100%',\n };\n\n const linkStyle: CSSProperties = {\n border: 'none',\n background: 'transparent',\n color: accentColor,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: '0.9rem',\n padding: 0,\n };\n\n const errorStyle: CSSProperties = {\n background: 'rgba(255,107,94,0.12)',\n color: '#c0392b',\n padding: '10px 12px',\n border: '1px solid rgba(255,107,94,0.25)',\n borderRadius: 8,\n fontSize: '0.85rem',\n };\n\n return (\n <div\n className={className}\n style={{\n maxWidth: 400,\n margin: '0 auto',\n padding: 32,\n border: '1px solid rgba(0,0,0,0.08)',\n borderRadius: 16,\n fontFamily: 'system-ui, -apple-system, sans-serif',\n ...style,\n }}\n >\n <div style={{ textAlign: 'center', marginBottom: 24 }}>\n {logo}\n <h1 style={{ fontSize: '1.4rem', fontWeight: 700, margin: '8px 0 4px' }}>{title}</h1>\n <p style={{ fontSize: '0.9rem', opacity: 0.7, margin: 0 }}>{subtitle}</p>\n </div>\n\n {otp.step === 'identify' ? (\n <form onSubmit={onIdentifySubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n {otp.error && <div style={errorStyle}>{otp.error}</div>}\n <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.85rem' }}>\n {identifierLabel}\n <input\n type=\"text\"\n value={otp.identifier}\n onChange={(e) => otp.setIdentifier(e.target.value)}\n placeholder={identifierPlaceholder}\n autoComplete=\"username\"\n autoFocus\n required\n style={inputStyle}\n />\n </label>\n <button type=\"submit\" disabled={otp.isBusy} style={buttonStyle}>\n {otp.isSending ? 'Sending code…' : 'Send code'}\n </button>\n </form>\n ) : (\n <form onSubmit={onVerifySubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n {otp.error && <div style={errorStyle}>{otp.error}</div>}\n <div\n style={{\n background: 'rgba(86,211,181,0.12)',\n padding: '10px 12px',\n borderRadius: 8,\n fontSize: '0.85rem',\n }}\n >\n {otp.channels.length > 0\n ? `Code sent via ${formatChannels(otp.channels)}.`\n : 'We sent you a code.'}\n {otp.expiresInSeconds > 0 &&\n ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`}\n </div>\n <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.85rem' }}>\n Verification code\n <input\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n value={otp.code}\n onChange={(e) => otp.setCode(e.target.value)}\n placeholder=\"Enter code\"\n autoFocus\n required\n style={inputStyle}\n />\n </label>\n <button type=\"submit\" disabled={otp.isBusy} style={buttonStyle}>\n {otp.isVerifying ? 'Verifying…' : 'Verify and sign in'}\n </button>\n <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n <button type=\"button\" onClick={otp.back} disabled={otp.isBusy} style={linkStyle}>\n Use a different contact\n </button>\n <button type=\"button\" onClick={() => void otp.resend()} disabled={otp.isBusy} style={linkStyle}>\n Resend code\n </button>\n </div>\n </form>\n )}\n </div>\n );\n}\n","import type { RequestOtpResult } from './state';\n\nexport interface FetchHandlerOptions {\n /**\n * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,\n * must return `{ requestId, channels?, expiresInSeconds? }`.\n * @default '/api/auth/request-otp'\n */\n requestUrl?: string;\n /**\n * Partner backend endpoint that verifies an OTP. Receives\n * `{ requestId, code }`, returns whatever your app needs (session, token…).\n * @default '/api/auth/verify-otp'\n */\n verifyUrl?: string;\n /** Extra headers (e.g. CSRF token) to send on both requests. */\n headers?: Record<string, string>;\n /** Forwarded to fetch — set to 'include' if you use cookie sessions. */\n credentials?: RequestCredentials;\n}\n\nasync function postJson<T>(\n url: string,\n body: unknown,\n options: FetchHandlerOptions,\n): Promise<T> {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },\n credentials: options.credentials,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n let message = `Request failed (${response.status})`;\n try {\n const data = (await response.json()) as { message?: string; error?: string };\n message = data.message || data.error || message;\n } catch {\n // non-JSON error body — keep the status-based message\n }\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.\n * Those endpoints call the Sneek API server-side with your secret key.\n */\nexport function createFetchHandlers<TResult = unknown>(\n options: FetchHandlerOptions = {},\n): {\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n} {\n const requestUrl = options.requestUrl ?? '/api/auth/request-otp';\n const verifyUrl = options.verifyUrl ?? '/api/auth/verify-otp';\n\n return {\n requestOtp: (identifier: string) =>\n postJson<RequestOtpResult>(requestUrl, { identifier }, options),\n verifyOtp: (input: { requestId: string; code: string }) =>\n postJson<TResult>(verifyUrl, input, options),\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/use-sneek-otp.ts","../src/state.ts","../src/component.tsx","../src/theme.ts","../src/fetch-handlers.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\nimport {\n initialOtpState,\n otpReducer,\n toMessage,\n type RequestOtpResult,\n type SneekOtpState,\n} from './state';\n\n/**\n * Partner-supplied transport. These call the *partner's own backend*, which in\n * turn talks to the Sneek API with the secret server-side key. The browser\n * never sees a Sneek API key — that is the whole security model of this package.\n */\nexport interface SneekOtpHandlers<TResult = unknown> {\n /** Ask the partner backend to send an OTP to `identifier`. */\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n /** Verify the code with the partner backend; resolve with the auth result. */\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n /** Called after a successful verification with the partner's result. */\n onSuccess?: (result: TResult) => void;\n /** Called on any error (request or verify). */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseSneekOtp extends SneekOtpState {\n setIdentifier: (value: string) => void;\n setCode: (value: string) => void;\n /** Submit the identify step — triggers `requestOtp`. */\n sendOtp: () => Promise<void>;\n /** Submit the verify step — triggers `verifyOtp`. */\n verify: () => Promise<void>;\n /** Go back to the identify screen (e.g. \"use a different email\"). */\n back: () => void;\n /** Re-send the OTP to the same identifier. */\n resend: () => Promise<void>;\n /** Convenience booleans. */\n isSending: boolean;\n isVerifying: boolean;\n isBusy: boolean;\n}\n\nconst DEFAULT_REQUEST_ERROR = 'Could not send the code. Please try again.';\nconst DEFAULT_VERIFY_ERROR = 'That code was not correct. Please try again.';\n\n/**\n * Headless hook implementing the passwordless OTP login flow. Bring your own\n * markup, or use the {@link SneekOtpLogin} component for a styled default.\n */\nexport function useSneekOtp<TResult = unknown>(\n handlers: SneekOtpHandlers<TResult>,\n): UseSneekOtp {\n const [state, dispatch] = useReducer(otpReducer, initialOtpState);\n\n const { requestOtp, verifyOtp, onSuccess, onError } = handlers;\n\n const setIdentifier = useCallback((value: string) => {\n dispatch({ type: 'set_identifier', value });\n }, []);\n\n const setCode = useCallback((value: string) => {\n dispatch({ type: 'set_code', value });\n }, []);\n\n const runRequest = useCallback(\n async (identifier: string) => {\n const trimmed = identifier.trim();\n if (!trimmed) {\n dispatch({ type: 'request_error', message: 'Enter your email or mobile number.' });\n return;\n }\n dispatch({ type: 'request_start' });\n try {\n const result = await requestOtp(trimmed);\n dispatch({ type: 'request_success', result });\n } catch (error) {\n dispatch({ type: 'request_error', message: toMessage(error, DEFAULT_REQUEST_ERROR) });\n onError?.(error);\n }\n },\n [requestOtp, onError],\n );\n\n const sendOtp = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);\n\n const resend = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);\n\n const verify = useCallback(async () => {\n const code = state.code.trim();\n if (!code) {\n dispatch({ type: 'verify_error', message: 'Enter the code you received.' });\n return;\n }\n dispatch({ type: 'verify_start' });\n try {\n const result = await verifyOtp({ requestId: state.requestId, code });\n onSuccess?.(result);\n } catch (error) {\n dispatch({ type: 'verify_error', message: toMessage(error, DEFAULT_VERIFY_ERROR) });\n onError?.(error);\n }\n }, [verifyOtp, state.code, state.requestId, onSuccess, onError]);\n\n const back = useCallback(() => {\n dispatch({ type: 'back_to_identify' });\n }, []);\n\n return {\n ...state,\n setIdentifier,\n setCode,\n sendOtp,\n verify,\n back,\n resend,\n isSending: state.status === 'sending',\n isVerifying: state.status === 'verifying',\n isBusy: state.status !== 'idle',\n };\n}\n","/**\n * Framework-agnostic state machine for the Sneek passwordless OTP flow.\n *\n * Kept free of React so it can be unit-tested in isolation and reused by other\n * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a\n * thin wrapper around this reducer.\n */\n\nexport type SneekChannel = 'sms' | 'whatsapp' | 'email';\n\n/** Which screen of the two-step flow is showing. */\nexport type SneekOtpStep = 'identify' | 'verify';\n\n/** Async lifecycle for the in-flight request. */\nexport type SneekOtpStatus = 'idle' | 'sending' | 'verifying';\n\nexport interface SneekOtpState {\n step: SneekOtpStep;\n status: SneekOtpStatus;\n /** The email / mobile / username the user typed. */\n identifier: string;\n /** The OTP code the user typed on the verify screen. */\n code: string;\n /** Opaque id returned by the partner backend, replayed on verify. */\n requestId: string;\n /** Channels the OTP was actually delivered over. */\n channels: SneekChannel[];\n /** Seconds until the OTP expires (from the request response). */\n expiresInSeconds: number;\n /** User-facing error message, or null. */\n error: string | null;\n}\n\nexport const initialOtpState: SneekOtpState = {\n step: 'identify',\n status: 'idle',\n identifier: '',\n code: '',\n requestId: '',\n channels: [],\n expiresInSeconds: 0,\n error: null,\n};\n\nexport interface RequestOtpResult {\n requestId: string;\n channels?: SneekChannel[];\n expiresInSeconds?: number;\n}\n\nexport type SneekOtpAction =\n | { type: 'set_identifier'; value: string }\n | { type: 'set_code'; value: string }\n | { type: 'request_start' }\n | { type: 'request_success'; result: RequestOtpResult }\n | { type: 'request_error'; message: string }\n | { type: 'verify_start' }\n | { type: 'verify_error'; message: string }\n | { type: 'reset' }\n | { type: 'back_to_identify' };\n\nconst isBusy = (status: SneekOtpStatus): boolean => status !== 'idle';\n\nexport function otpReducer(\n state: SneekOtpState,\n action: SneekOtpAction,\n): SneekOtpState {\n switch (action.type) {\n case 'set_identifier':\n return { ...state, identifier: action.value, error: null };\n\n case 'set_code':\n return { ...state, code: action.value, error: null };\n\n case 'request_start':\n // Guard against double submits while a request is already in flight.\n if (isBusy(state.status)) return state;\n return { ...state, status: 'sending', error: null };\n\n case 'request_success':\n return {\n ...state,\n step: 'verify',\n status: 'idle',\n code: '',\n requestId: action.result.requestId,\n channels: action.result.channels ?? [],\n expiresInSeconds: action.result.expiresInSeconds ?? 0,\n error: null,\n };\n\n case 'request_error':\n return { ...state, status: 'idle', error: action.message };\n\n case 'verify_start':\n if (isBusy(state.status)) return state;\n return { ...state, status: 'verifying', error: null };\n\n case 'verify_error':\n return { ...state, status: 'idle', error: action.message };\n\n case 'back_to_identify':\n return {\n ...state,\n step: 'identify',\n status: 'idle',\n code: '',\n requestId: '',\n channels: [],\n expiresInSeconds: 0,\n error: null,\n };\n\n case 'reset':\n return { ...initialOtpState };\n\n default:\n return state;\n }\n}\n\nconst CHANNEL_LABELS: Record<SneekChannel, string> = {\n sms: 'SMS',\n whatsapp: 'WhatsApp',\n email: 'email',\n};\n\n/** \"SMS, WhatsApp\" — human label for the channels an OTP was sent over. */\nexport function formatChannels(channels: SneekChannel[]): string {\n return channels.map((c) => CHANNEL_LABELS[c] ?? c).join(', ');\n}\n\n/** Normalize any thrown value into a user-facing message. */\nexport function toMessage(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n return fallback;\n}\n","import {\n useEffect,\n useId,\n type CSSProperties,\n type FormEvent,\n type ReactNode,\n} from 'react';\nimport { formatChannels } from './state';\nimport { buildStyles, SNEEK_STYLE_ID } from './theme';\nimport { useSneekOtp, type SneekOtpHandlers } from './use-sneek-otp';\n\nexport type SneekTheme = 'auto' | 'light' | 'dark';\n\nexport interface SneekOtpLoginProps<TResult = unknown>\n extends SneekOtpHandlers<TResult> {\n /** Heading above the form. @default 'Sign in' */\n title?: string;\n /** Sub-heading. Pass `null` to remove it. */\n subtitle?: ReactNode;\n /** Label for the identifier input. */\n identifierLabel?: string;\n /** Placeholder for the identifier input. */\n identifierPlaceholder?: string;\n /**\n * Colour scheme. `auto` follows the visitor's system setting.\n * @default 'auto'\n */\n theme?: SneekTheme;\n /**\n * Override the single accent colour. Leave unset to use Sneek orange,\n * which is tuned per mode for contrast.\n */\n accentColor?: string;\n /** Replaces the Sneek mark above the title. Pass `null` to remove it. */\n logo?: ReactNode;\n /** Show the \"Secured by Sneek\" line under the form. @default true */\n showBranding?: boolean;\n /** Style overrides for the outer card. */\n style?: CSSProperties;\n /** Extra class on the outer card. */\n className?: string;\n}\n\n/**\n * Sneek mark: two triangles offset on a diagonal — one pointing up on the\n * right, one pointing down on the left. Traced from the brand asset so the\n * package needs no image file and no CDN.\n */\nfunction SneekMark() {\n return (\n <svg\n width=\"36\"\n height=\"36\"\n viewBox=\"0 0 32 32\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Sneek\"\n >\n <path d=\"M21.3 1 29.3 16H13.3L21.3 1Z\" fill=\"var(--sneek-accent)\" />\n <path d=\"M2.7 17.5H18.7L10.7 31 2.7 17.5Z\" fill=\"var(--sneek-accent)\" />\n </svg>\n );\n}\n\n/**\n * Drop-in passwordless login card.\n *\n * The component never receives a Sneek API key — it calls the handlers you\n * supply, which talk to your own backend. See `createFetchHandlers` for the\n * common case.\n *\n * @example\n * ```tsx\n * <SneekOtpLogin\n * {...createFetchHandlers({ baseUrl: '/api/auth' })}\n * onSuccess={(session) => router.push('/app')}\n * />\n * ```\n */\nexport function SneekOtpLogin<TResult = unknown>(\n props: SneekOtpLoginProps<TResult>,\n): ReactNode {\n const {\n title = 'Sign in',\n subtitle = 'We will send you a one-time code.',\n identifierLabel = 'Email or mobile',\n identifierPlaceholder = 'you@company.com or +91…',\n theme = 'auto',\n accentColor,\n logo,\n showBranding = true,\n style,\n className,\n ...handlers\n } = props;\n\n const otp = useSneekOtp<TResult>(handlers);\n const uid = useId();\n const identifierId = `${uid}-identifier`;\n const codeId = `${uid}-code`;\n const statusId = `${uid}-status`;\n\n // One <style> element for the whole page, regardless of how many cards\n // render. Injected on mount so the package stays SSR-safe.\n useEffect(() => {\n if (typeof document === 'undefined') return;\n let el = document.getElementById(SNEEK_STYLE_ID);\n if (!el) {\n el = document.createElement('style');\n el.id = SNEEK_STYLE_ID;\n document.head.appendChild(el);\n }\n el.textContent = buildStyles(accentColor);\n }, [accentColor]);\n\n const onIdentifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.sendOtp();\n };\n\n const onVerifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.verify();\n };\n\n const label: CSSProperties = {\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n fontSize: 14,\n lineHeight: '20px',\n color: 'var(--sneek-text-muted)',\n };\n\n const input: CSSProperties = {\n padding: '12px 14px',\n border: '1px solid var(--sneek-border)',\n borderRadius: 12,\n background: 'var(--sneek-surface-2)',\n color: 'var(--sneek-text)',\n fontSize: 15,\n width: '100%',\n outline: 'none',\n transition: 'border-color 200ms var(--sneek-ease)',\n };\n\n const button: CSSProperties = {\n padding: '12px 18px',\n border: 'none',\n borderRadius: 9999,\n background: 'var(--sneek-accent)',\n color: 'var(--sneek-accent-contrast)',\n fontSize: 15,\n fontWeight: 500,\n cursor: otp.isBusy ? 'not-allowed' : 'pointer',\n opacity: otp.isBusy ? 0.6 : 1,\n width: '100%',\n transition: 'filter 200ms var(--sneek-ease), opacity 200ms var(--sneek-ease)',\n };\n\n const link: CSSProperties = {\n border: 'none',\n background: 'transparent',\n color: 'var(--sneek-accent)',\n cursor: otp.isBusy ? 'not-allowed' : 'pointer',\n font: 'inherit',\n fontSize: 14,\n padding: 0,\n };\n\n const notice: CSSProperties = {\n padding: '10px 12px',\n borderRadius: 12,\n background: 'var(--sneek-surface-2)',\n border: '1px solid var(--sneek-border)',\n fontSize: 14,\n color: 'var(--sneek-text-muted)',\n };\n\n return (\n <div\n className={className ? `sneek-otp ${className}` : 'sneek-otp'}\n data-theme={theme === 'auto' ? undefined : theme}\n style={{\n maxWidth: 400,\n margin: '0 auto',\n padding: 24,\n border: '1px solid var(--sneek-border)',\n borderRadius: 16,\n background: 'var(--sneek-surface)',\n boxShadow: 'var(--sneek-shadow)',\n color: 'var(--sneek-text)',\n fontFamily:\n 'Inter, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif',\n lineHeight: 1.5,\n ...style,\n }}\n >\n {logo === null ? null : (logo ?? <SneekMark />)}\n <h1\n style={{\n fontSize: 21,\n lineHeight: '28px',\n fontWeight: 500,\n letterSpacing: '-0.01em',\n margin: '16px 0 0',\n }}\n >\n {title}\n </h1>\n {subtitle ? (\n <p\n style={{\n fontSize: 14,\n lineHeight: '20px',\n color: 'var(--sneek-text-muted)',\n margin: '8px 0 0',\n }}\n >\n {subtitle}\n </p>\n ) : null}\n\n {/* Errors and channel notices are announced, not just shown. */}\n <div role=\"status\" aria-live=\"polite\" id={statusId} style={{ marginTop: 32 }}>\n {otp.error ? (\n <div\n role=\"alert\"\n style={{\n ...notice,\n color: 'var(--sneek-danger)',\n borderColor: 'var(--sneek-danger)',\n }}\n >\n {otp.error}\n </div>\n ) : otp.step === 'verify' ? (\n <div style={notice}>\n {otp.channels.length > 0\n ? `Code sent by ${formatChannels(otp.channels)}.`\n : 'We sent you a code.'}\n {otp.expiresInSeconds > 0\n ? ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`\n : ''}\n </div>\n ) : null}\n </div>\n\n {otp.step === 'identify' ? (\n <form\n onSubmit={onIdentifySubmit}\n style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 16 }}\n >\n <label style={label} htmlFor={identifierId}>\n {identifierLabel}\n <input\n id={identifierId}\n className=\"sneek-otp-input\"\n type=\"text\"\n value={otp.identifier}\n onChange={(e) => otp.setIdentifier(e.target.value)}\n placeholder={identifierPlaceholder}\n autoComplete=\"username\"\n autoFocus\n required\n aria-describedby={statusId}\n style={input}\n />\n </label>\n <button\n type=\"submit\"\n className=\"sneek-otp-button\"\n disabled={otp.isBusy}\n style={button}\n >\n {otp.isSending ? 'Sending code…' : 'Send code'}\n </button>\n </form>\n ) : (\n <form\n onSubmit={onVerifySubmit}\n style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 16 }}\n >\n <label style={label} htmlFor={codeId}>\n Verification code\n <input\n id={codeId}\n className=\"sneek-otp-input\"\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n value={otp.code}\n onChange={(e) => otp.setCode(e.target.value)}\n placeholder=\"6-digit code\"\n autoFocus\n required\n aria-describedby={statusId}\n style={{ ...input, letterSpacing: '0.3em' }}\n />\n </label>\n <button\n type=\"submit\"\n className=\"sneek-otp-button\"\n disabled={otp.isBusy}\n style={button}\n >\n {otp.isVerifying ? 'Verifying…' : 'Verify and sign in'}\n </button>\n <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>\n <button\n type=\"button\"\n className=\"sneek-otp-link\"\n onClick={otp.back}\n disabled={otp.isBusy}\n style={link}\n >\n Use a different contact\n </button>\n <button\n type=\"button\"\n className=\"sneek-otp-link\"\n onClick={() => void otp.resend()}\n disabled={otp.isBusy}\n style={link}\n >\n Resend code\n </button>\n </div>\n </form>\n )}\n\n {showBranding ? (\n <p\n style={{\n margin: '32px 0 0',\n textAlign: 'center',\n fontSize: 12,\n lineHeight: '16px',\n color: 'var(--sneek-text-muted)',\n }}\n >\n Secured by Sneek\n </p>\n ) : null}\n </div>\n );\n}\n","/**\n * Theme for the drop-in login card.\n *\n * The package ships zero dependencies and no stylesheet, so the tokens are\n * injected once as CSS custom properties scoped to `.sneek-otp`. That buys two\n * things inline styles cannot: a `prefers-color-scheme` media query, and real\n * `:focus-visible` / `:hover` states.\n *\n * Light and dark are authored independently rather than one being an inversion\n * of the other. In dark, the page-to-card lightness delta carries depth and the\n * card needs no shadow; in light both are near-white, so the card takes a\n * visible border and a real shadow.\n */\nexport const SNEEK_STYLE_ID = 'sneek-otp-styles';\n\n/** Brand orange. The single accent — nothing else on the card is coloured. */\nexport const SNEEK_ACCENT_DARK = '#f86513';\n/** Darkened for light mode so it clears WCAG AA as text and as a button fill. */\nexport const SNEEK_ACCENT_LIGHT = '#b83f06';\n\nexport function buildStyles(accent?: string): string {\n const darkAccent = accent ?? SNEEK_ACCENT_DARK;\n const lightAccent = accent ?? SNEEK_ACCENT_LIGHT;\n\n const light = `\n --sneek-surface: #ffffff;\n --sneek-surface-2: #f4f1ed;\n --sneek-border: #e3ddd5;\n --sneek-text: #1c1917;\n --sneek-text-muted: #57534e;\n --sneek-accent: ${lightAccent};\n --sneek-accent-soft: color-mix(in srgb, ${lightAccent} 12%, transparent);\n --sneek-accent-contrast: #ffffff;\n --sneek-danger: #b0242a;\n --sneek-shadow: 0 3px 3px -2px rgb(0 0 0 / 0.10), 0 3px 4px rgb(0 0 0 / 0.07),\n 0 1px 8px rgb(0 0 0 / 0.06);`;\n\n const dark = `\n --sneek-surface: #17150f;\n --sneek-surface-2: #201d16;\n --sneek-border: rgba(255, 255, 255, 0.08);\n --sneek-text: #faf8f6;\n --sneek-text-muted: #a8a29b;\n --sneek-accent: ${darkAccent};\n --sneek-accent-soft: color-mix(in srgb, ${darkAccent} 16%, transparent);\n --sneek-accent-contrast: #1a0d04;\n --sneek-danger: #d43a3c;\n --sneek-shadow: none;`;\n\n return `\n.sneek-otp {${light}\n --sneek-ease: cubic-bezier(0, 0, 0.2, 1);\n}\n@media (prefers-color-scheme: dark) {\n .sneek-otp:not([data-theme=\"light\"]) {${dark}\n }\n}\n.sneek-otp[data-theme=\"dark\"] {${dark}\n}\n.sneek-otp[data-theme=\"light\"] {${light}\n}\n.sneek-otp *, .sneek-otp *::before, .sneek-otp *::after { box-sizing: border-box; }\n.sneek-otp-input:focus-visible,\n.sneek-otp-button:focus-visible,\n.sneek-otp-link:focus-visible {\n outline: 2px solid var(--sneek-accent);\n outline-offset: 2px;\n}\n.sneek-otp-input:focus {\n border-color: var(--sneek-accent);\n}\n.sneek-otp-button:not(:disabled):hover {\n filter: brightness(1.08);\n}\n.sneek-otp-link:not(:disabled):hover {\n text-decoration: underline;\n}\n@media (prefers-reduced-motion: reduce) {\n .sneek-otp * { transition-duration: 0.01ms !important; }\n}\n`;\n}\n","import type { RequestOtpResult } from './state';\n\nexport interface FetchHandlerOptions {\n /**\n * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,\n * must return `{ requestId, channels?, expiresInSeconds? }`.\n * @default '/api/auth/request-otp'\n */\n requestUrl?: string;\n /**\n * Partner backend endpoint that verifies an OTP. Receives\n * `{ requestId, code }`, returns whatever your app needs (session, token…).\n * @default '/api/auth/verify-otp'\n */\n verifyUrl?: string;\n /** Extra headers (e.g. CSRF token) to send on both requests. */\n headers?: Record<string, string>;\n /** Forwarded to fetch — set to 'include' if you use cookie sessions. */\n credentials?: RequestCredentials;\n}\n\nasync function postJson<T>(\n url: string,\n body: unknown,\n options: FetchHandlerOptions,\n): Promise<T> {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },\n credentials: options.credentials,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n let message = `Request failed (${response.status})`;\n try {\n const data = (await response.json()) as { message?: string; error?: string };\n message = data.message || data.error || message;\n } catch {\n // non-JSON error body — keep the status-based message\n }\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.\n * Those endpoints call the Sneek API server-side with your secret key.\n */\nexport function createFetchHandlers<TResult = unknown>(\n options: FetchHandlerOptions = {},\n): {\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n} {\n const requestUrl = options.requestUrl ?? '/api/auth/request-otp';\n const verifyUrl = options.verifyUrl ?? '/api/auth/verify-otp';\n\n return {\n requestOtp: (identifier: string) =>\n postJson<RequestOtpResult>(requestUrl, { identifier }, options),\n verifyOtp: (input: { requestId: string; code: string }) =>\n postJson<TResult>(verifyUrl, input, options),\n };\n}\n"],"mappings":"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","names":[]}
|