@sneekin/ui 0.2.0 → 0.2.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 +4 -0
- package/dist/component.d.ts +2 -0
- package/dist/index.js +149 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +151 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,10 @@ Override the single accent colour — everything else derives from it:
|
|
|
53
53
|
<SneekOtpLogin accentColor="#7c3aed" {...handlers} />
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
The code entry step shows the destination with an inline way to change it,
|
|
57
|
+
and submits itself on the sixth digit — no separate verify tap needed on
|
|
58
|
+
mobile.
|
|
59
|
+
|
|
56
60
|
Swap the mark, drop the sub-heading, or remove the Sneek line:
|
|
57
61
|
|
|
58
62
|
```tsx
|
package/dist/component.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type SneekTheme = 'auto' | 'light' | 'dark';
|
|
|
4
4
|
export interface SneekOtpLoginProps<TResult = unknown> extends SneekOtpHandlers<TResult> {
|
|
5
5
|
/** Heading above the form. @default 'Sign in' */
|
|
6
6
|
title?: string;
|
|
7
|
+
/** Heading once the code has been sent. @default 'Enter the code' */
|
|
8
|
+
verifyTitle?: string;
|
|
7
9
|
/** Sub-heading. Pass `null` to remove it. */
|
|
8
10
|
subtitle?: ReactNode;
|
|
9
11
|
/** Label for the identifier input. */
|
package/dist/index.js
CHANGED
|
@@ -232,11 +232,20 @@ function buildStyles(accent) {
|
|
|
232
232
|
@media (prefers-reduced-motion: reduce) {
|
|
233
233
|
.sneek-otp * { transition-duration: 0.01ms !important; }
|
|
234
234
|
}
|
|
235
|
+
@keyframes sneek-otp-caret {
|
|
236
|
+
0%, 45% { opacity: 1; }
|
|
237
|
+
50%, 100% { opacity: 0; }
|
|
238
|
+
}
|
|
239
|
+
.sneek-otp-caret {
|
|
240
|
+
animation: sneek-otp-caret 1s step-end infinite;
|
|
241
|
+
}
|
|
235
242
|
`;
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
// src/component.tsx
|
|
239
246
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
247
|
+
var CODE_LENGTH = 6;
|
|
248
|
+
var RESEND_SECONDS = 30;
|
|
240
249
|
function SneekMark() {
|
|
241
250
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
242
251
|
"svg",
|
|
@@ -257,7 +266,8 @@ function SneekMark() {
|
|
|
257
266
|
function SneekOtpLogin(props) {
|
|
258
267
|
const {
|
|
259
268
|
title = "Sign in",
|
|
260
|
-
|
|
269
|
+
verifyTitle = "Enter the code",
|
|
270
|
+
subtitle = "Enter your email or phone. We will send you a code.",
|
|
261
271
|
identifierLabel = "Email or mobile",
|
|
262
272
|
identifierPlaceholder = "you@company.com or +91\u2026",
|
|
263
273
|
theme = "auto",
|
|
@@ -270,6 +280,8 @@ function SneekOtpLogin(props) {
|
|
|
270
280
|
} = props;
|
|
271
281
|
const otp = useSneekOtp(handlers);
|
|
272
282
|
const uid = (0, import_react2.useId)();
|
|
283
|
+
const [resendIn, setResendIn] = (0, import_react2.useState)(0);
|
|
284
|
+
const [codeFocused, setCodeFocused] = (0, import_react2.useState)(false);
|
|
273
285
|
const identifierId = `${uid}-identifier`;
|
|
274
286
|
const codeId = `${uid}-code`;
|
|
275
287
|
const statusId = `${uid}-status`;
|
|
@@ -291,6 +303,19 @@ function SneekOtpLogin(props) {
|
|
|
291
303
|
event.preventDefault();
|
|
292
304
|
void otp.verify();
|
|
293
305
|
};
|
|
306
|
+
(0, import_react2.useEffect)(() => {
|
|
307
|
+
if (otp.step === "verify") setResendIn(RESEND_SECONDS);
|
|
308
|
+
}, [otp.step]);
|
|
309
|
+
(0, import_react2.useEffect)(() => {
|
|
310
|
+
if (resendIn <= 0) return;
|
|
311
|
+
const timer = setTimeout(() => setResendIn((n) => n - 1), 1e3);
|
|
312
|
+
return () => clearTimeout(timer);
|
|
313
|
+
}, [resendIn]);
|
|
314
|
+
(0, import_react2.useEffect)(() => {
|
|
315
|
+
if (otp.step === "verify" && otp.code.length === CODE_LENGTH && !otp.isBusy) {
|
|
316
|
+
void otp.verify();
|
|
317
|
+
}
|
|
318
|
+
}, [otp.code, otp.step]);
|
|
294
319
|
const label = {
|
|
295
320
|
display: "flex",
|
|
296
321
|
flexDirection: "column",
|
|
@@ -370,10 +395,37 @@ function SneekOtpLogin(props) {
|
|
|
370
395
|
letterSpacing: "-0.01em",
|
|
371
396
|
margin: "16px 0 0"
|
|
372
397
|
},
|
|
373
|
-
children: title
|
|
398
|
+
children: otp.step === "verify" ? verifyTitle : title
|
|
374
399
|
}
|
|
375
400
|
),
|
|
376
|
-
|
|
401
|
+
otp.step === "verify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
402
|
+
"p",
|
|
403
|
+
{
|
|
404
|
+
style: {
|
|
405
|
+
fontSize: 14,
|
|
406
|
+
lineHeight: "20px",
|
|
407
|
+
color: "var(--sneek-text-muted)",
|
|
408
|
+
margin: "8px 0 0"
|
|
409
|
+
},
|
|
410
|
+
children: [
|
|
411
|
+
"Sent to",
|
|
412
|
+
" ",
|
|
413
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--sneek-text)" }, children: otp.identifier }),
|
|
414
|
+
" \xB7 ",
|
|
415
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
416
|
+
"button",
|
|
417
|
+
{
|
|
418
|
+
type: "button",
|
|
419
|
+
className: "sneek-otp-link",
|
|
420
|
+
onClick: otp.back,
|
|
421
|
+
disabled: otp.isBusy,
|
|
422
|
+
style: { ...link, fontSize: 14 },
|
|
423
|
+
children: "Change"
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
) : subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
377
429
|
"p",
|
|
378
430
|
{
|
|
379
431
|
style: {
|
|
@@ -396,10 +448,7 @@ function SneekOtpLogin(props) {
|
|
|
396
448
|
},
|
|
397
449
|
children: otp.error
|
|
398
450
|
}
|
|
399
|
-
) :
|
|
400
|
-
otp.channels.length > 0 ? `Code sent by ${formatChannels(otp.channels)}.` : "We sent you a code.",
|
|
401
|
-
otp.expiresInSeconds > 0 ? ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.` : ""
|
|
402
|
-
] }) : null }),
|
|
451
|
+
) : null }),
|
|
403
452
|
otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
404
453
|
"form",
|
|
405
454
|
{
|
|
@@ -443,23 +492,80 @@ function SneekOtpLogin(props) {
|
|
|
443
492
|
onSubmit: onVerifySubmit,
|
|
444
493
|
style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
|
|
445
494
|
children: [
|
|
446
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: label, htmlFor: codeId, children: [
|
|
447
|
-
|
|
495
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { ...label, marginBottom: -8 }, htmlFor: codeId, children: [
|
|
496
|
+
CODE_LENGTH,
|
|
497
|
+
"-digit code"
|
|
498
|
+
] }),
|
|
499
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative" }, children: [
|
|
448
500
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
449
501
|
"input",
|
|
450
502
|
{
|
|
451
503
|
id: codeId,
|
|
452
|
-
className: "sneek-otp-input",
|
|
504
|
+
className: "sneek-otp-code-input",
|
|
453
505
|
type: "text",
|
|
454
506
|
inputMode: "numeric",
|
|
507
|
+
pattern: "[0-9]*",
|
|
455
508
|
autoComplete: "one-time-code",
|
|
509
|
+
maxLength: CODE_LENGTH,
|
|
456
510
|
value: otp.code,
|
|
457
|
-
onChange: (e) => otp.setCode(
|
|
458
|
-
|
|
511
|
+
onChange: (e) => otp.setCode(
|
|
512
|
+
e.target.value.replace(/\D/g, "").slice(0, CODE_LENGTH)
|
|
513
|
+
),
|
|
514
|
+
onFocus: () => setCodeFocused(true),
|
|
515
|
+
onBlur: () => setCodeFocused(false),
|
|
459
516
|
autoFocus: true,
|
|
460
517
|
required: true,
|
|
461
518
|
"aria-describedby": statusId,
|
|
462
|
-
style: {
|
|
519
|
+
style: {
|
|
520
|
+
position: "absolute",
|
|
521
|
+
inset: 0,
|
|
522
|
+
width: "100%",
|
|
523
|
+
height: "100%",
|
|
524
|
+
opacity: 0,
|
|
525
|
+
caretColor: "transparent",
|
|
526
|
+
cursor: "text"
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
),
|
|
530
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
531
|
+
"div",
|
|
532
|
+
{
|
|
533
|
+
className: "sneek-otp-code-cells",
|
|
534
|
+
"aria-hidden": "true",
|
|
535
|
+
style: { display: "flex", gap: 10 },
|
|
536
|
+
children: Array.from({ length: CODE_LENGTH }).map((_, i) => {
|
|
537
|
+
const digit = otp.code[i];
|
|
538
|
+
const isActive = codeFocused && i === otp.code.length;
|
|
539
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
540
|
+
"div",
|
|
541
|
+
{
|
|
542
|
+
"data-active": isActive,
|
|
543
|
+
style: {
|
|
544
|
+
flex: 1,
|
|
545
|
+
height: 44,
|
|
546
|
+
display: "flex",
|
|
547
|
+
alignItems: "center",
|
|
548
|
+
justifyContent: "center",
|
|
549
|
+
fontSize: 22,
|
|
550
|
+
color: "var(--sneek-text)",
|
|
551
|
+
borderBottom: `2px solid ${isActive ? "var(--sneek-accent)" : "var(--sneek-border)"}`,
|
|
552
|
+
transition: "border-color 200ms var(--sneek-ease)"
|
|
553
|
+
},
|
|
554
|
+
children: digit ?? (isActive ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
555
|
+
"span",
|
|
556
|
+
{
|
|
557
|
+
className: "sneek-otp-caret",
|
|
558
|
+
style: {
|
|
559
|
+
width: 2,
|
|
560
|
+
height: 22,
|
|
561
|
+
background: "var(--sneek-accent)"
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
) : null)
|
|
565
|
+
},
|
|
566
|
+
i
|
|
567
|
+
);
|
|
568
|
+
})
|
|
463
569
|
}
|
|
464
570
|
)
|
|
465
571
|
] }),
|
|
@@ -468,35 +574,38 @@ function SneekOtpLogin(props) {
|
|
|
468
574
|
{
|
|
469
575
|
type: "submit",
|
|
470
576
|
className: "sneek-otp-button",
|
|
471
|
-
disabled: otp.isBusy,
|
|
472
|
-
style:
|
|
473
|
-
|
|
577
|
+
disabled: otp.isBusy || otp.code.length < CODE_LENGTH,
|
|
578
|
+
style: {
|
|
579
|
+
...button,
|
|
580
|
+
opacity: otp.isBusy || otp.code.length < CODE_LENGTH ? 0.6 : 1
|
|
581
|
+
},
|
|
582
|
+
children: otp.isVerifying ? "Verifying\u2026" : "Verify"
|
|
474
583
|
}
|
|
475
584
|
),
|
|
476
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
585
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { textAlign: "center" }, children: resendIn > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
586
|
+
"span",
|
|
587
|
+
{
|
|
588
|
+
style: {
|
|
589
|
+
fontSize: 14,
|
|
590
|
+
color: "var(--sneek-text-muted)"
|
|
591
|
+
},
|
|
592
|
+
children: [
|
|
593
|
+
"Resend code in ",
|
|
594
|
+
resendIn,
|
|
595
|
+
"s"
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
599
|
+
"button",
|
|
600
|
+
{
|
|
601
|
+
type: "button",
|
|
602
|
+
className: "sneek-otp-link",
|
|
603
|
+
onClick: () => void otp.resend(),
|
|
604
|
+
disabled: otp.isBusy,
|
|
605
|
+
style: link,
|
|
606
|
+
children: "Resend code"
|
|
607
|
+
}
|
|
608
|
+
) })
|
|
500
609
|
]
|
|
501
610
|
}
|
|
502
611
|
),
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/use-sneek-otp.ts","../src/state.ts","../src/component.tsx","../src/theme.ts","../src/fetch-handlers.ts"],"sourcesContent":["export {\n useSneekOtp,\n type SneekOtpHandlers,\n type UseSneekOtp,\n} from './use-sneek-otp';\n\nexport {\n SneekOtpLogin,\n type SneekOtpLoginProps,\n type SneekTheme,\n} from './component';\n\nexport {\n SNEEK_ACCENT_DARK,\n SNEEK_ACCENT_LIGHT,\n} from './theme';\n\nexport {\n createFetchHandlers,\n type FetchHandlerOptions,\n} from './fetch-handlers';\n\nexport {\n otpReducer,\n initialOtpState,\n formatChannels,\n type SneekChannel,\n type SneekOtpStep,\n type SneekOtpStatus,\n type SneekOtpState,\n type SneekOtpAction,\n type RequestOtpResult,\n} from './state';\n","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":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/use-sneek-otp.ts","../src/state.ts","../src/component.tsx","../src/theme.ts","../src/fetch-handlers.ts"],"sourcesContent":["export {\n useSneekOtp,\n type SneekOtpHandlers,\n type UseSneekOtp,\n} from './use-sneek-otp';\n\nexport {\n SneekOtpLogin,\n type SneekOtpLoginProps,\n type SneekTheme,\n} from './component';\n\nexport {\n SNEEK_ACCENT_DARK,\n SNEEK_ACCENT_LIGHT,\n} from './theme';\n\nexport {\n createFetchHandlers,\n type FetchHandlerOptions,\n} from './fetch-handlers';\n\nexport {\n otpReducer,\n initialOtpState,\n formatChannels,\n type SneekChannel,\n type SneekOtpStep,\n type SneekOtpStatus,\n type SneekOtpState,\n type SneekOtpAction,\n type RequestOtpResult,\n} from './state';\n","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 useState,\n type CSSProperties,\n type FormEvent,\n type ReactNode,\n} from 'react';\n\nimport { buildStyles, SNEEK_STYLE_ID } from './theme';\nimport { useSneekOtp, type SneekOtpHandlers } from './use-sneek-otp';\n\nexport type SneekTheme = 'auto' | 'light' | 'dark';\n\n/** Digits in a Sneek code. */\nconst CODE_LENGTH = 6;\n/** Seconds before \"Resend code\" becomes tappable again. */\nconst RESEND_SECONDS = 30;\n\nexport interface SneekOtpLoginProps<TResult = unknown>\n extends SneekOtpHandlers<TResult> {\n /** Heading above the form. @default 'Sign in' */\n title?: string;\n /** Heading once the code has been sent. @default 'Enter the code' */\n verifyTitle?: 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 verifyTitle = 'Enter the code',\n subtitle = 'Enter your email or phone. We will send you a 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 // Countdown before resend re-enables. Instant resend is both poor feedback\n // and an easy way to burn someone's SMS quota.\n const [resendIn, setResendIn] = useState(0);\n const [codeFocused, setCodeFocused] = useState(false);\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 // Start the cooldown whenever a code goes out.\n useEffect(() => {\n if (otp.step === 'verify') setResendIn(RESEND_SECONDS);\n }, [otp.step]);\n\n useEffect(() => {\n if (resendIn <= 0) return;\n const timer = setTimeout(() => setResendIn((n) => n - 1), 1000);\n return () => clearTimeout(timer);\n }, [resendIn]);\n\n // Submit as soon as the code looks complete — nobody should have to reach\n // for a button after typing the last digit.\n useEffect(() => {\n if (otp.step === 'verify' && otp.code.length === CODE_LENGTH && !otp.isBusy) {\n void otp.verify();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [otp.code, otp.step]);\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 {otp.step === 'verify' ? verifyTitle : title}\n </h1>\n {otp.step === 'verify' ? (\n <p\n style={{\n fontSize: 14,\n lineHeight: '20px',\n color: 'var(--sneek-text-muted)',\n margin: '8px 0 0',\n }}\n >\n Sent to{' '}\n <span style={{ color: 'var(--sneek-text)' }}>{otp.identifier}</span>\n {' · '}\n <button\n type=\"button\"\n className=\"sneek-otp-link\"\n onClick={otp.back}\n disabled={otp.isBusy}\n style={{ ...link, fontSize: 14 }}\n >\n Change\n </button>\n </p>\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 ) : 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, marginBottom: -8 }} htmlFor={codeId}>\n {CODE_LENGTH}-digit code\n </label>\n {/*\n * One real input captures keystrokes — paste, backspace and\n * SMS/email autofill all keep working, which six separate\n * `<input>`s make surprisingly easy to break. It sits over\n * the visible cells, transparent; the cells below just\n * mirror its value.\n */}\n <div style={{ position: 'relative' }}>\n <input\n id={codeId}\n className=\"sneek-otp-code-input\"\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n autoComplete=\"one-time-code\"\n maxLength={CODE_LENGTH}\n value={otp.code}\n onChange={(e) =>\n otp.setCode(\n e.target.value.replace(/\\D/g, '').slice(0, CODE_LENGTH),\n )\n }\n onFocus={() => setCodeFocused(true)}\n onBlur={() => setCodeFocused(false)}\n autoFocus\n required\n aria-describedby={statusId}\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n opacity: 0,\n caretColor: 'transparent',\n cursor: 'text',\n }}\n />\n <div\n className=\"sneek-otp-code-cells\"\n aria-hidden=\"true\"\n style={{ display: 'flex', gap: 10 }}\n >\n {Array.from({ length: CODE_LENGTH }).map((_, i) => {\n const digit = otp.code[i];\n const isActive = codeFocused && i === otp.code.length;\n return (\n <div\n key={i}\n data-active={isActive}\n style={{\n flex: 1,\n height: 44,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: 22,\n color: 'var(--sneek-text)',\n borderBottom: `2px solid ${\n isActive ? 'var(--sneek-accent)' : 'var(--sneek-border)'\n }`,\n transition: 'border-color 200ms var(--sneek-ease)',\n }}\n >\n {digit ?? (isActive ? (\n <span\n className=\"sneek-otp-caret\"\n style={{\n width: 2,\n height: 22,\n background: 'var(--sneek-accent)',\n }}\n />\n ) : null)}\n </div>\n );\n })}\n </div>\n </div>\n <button\n type=\"submit\"\n className=\"sneek-otp-button\"\n disabled={otp.isBusy || otp.code.length < CODE_LENGTH}\n style={{\n ...button,\n opacity:\n otp.isBusy || otp.code.length < CODE_LENGTH ? 0.6 : 1,\n }}\n >\n {otp.isVerifying ? 'Verifying…' : 'Verify'}\n </button>\n <div style={{ textAlign: 'center' }}>\n {resendIn > 0 ? (\n <span\n style={{\n fontSize: 14,\n color: 'var(--sneek-text-muted)',\n }}\n >\n Resend code in {resendIn}s\n </span>\n ) : (\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 )}\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@keyframes sneek-otp-caret {\n 0%, 45% { opacity: 1; }\n 50%, 100% { opacity: 0; }\n}\n.sneek-otp-caret {\n animation: sneek-otp-caret 1s step-end infinite;\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":["import_react"]}
|
package/dist/index.mjs
CHANGED
|
@@ -138,7 +138,8 @@ function useSneekOtp(handlers) {
|
|
|
138
138
|
// src/component.tsx
|
|
139
139
|
import {
|
|
140
140
|
useEffect,
|
|
141
|
-
useId
|
|
141
|
+
useId,
|
|
142
|
+
useState
|
|
142
143
|
} from "react";
|
|
143
144
|
|
|
144
145
|
// src/theme.ts
|
|
@@ -202,11 +203,20 @@ function buildStyles(accent) {
|
|
|
202
203
|
@media (prefers-reduced-motion: reduce) {
|
|
203
204
|
.sneek-otp * { transition-duration: 0.01ms !important; }
|
|
204
205
|
}
|
|
206
|
+
@keyframes sneek-otp-caret {
|
|
207
|
+
0%, 45% { opacity: 1; }
|
|
208
|
+
50%, 100% { opacity: 0; }
|
|
209
|
+
}
|
|
210
|
+
.sneek-otp-caret {
|
|
211
|
+
animation: sneek-otp-caret 1s step-end infinite;
|
|
212
|
+
}
|
|
205
213
|
`;
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
// src/component.tsx
|
|
209
217
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
218
|
+
var CODE_LENGTH = 6;
|
|
219
|
+
var RESEND_SECONDS = 30;
|
|
210
220
|
function SneekMark() {
|
|
211
221
|
return /* @__PURE__ */ jsxs(
|
|
212
222
|
"svg",
|
|
@@ -227,7 +237,8 @@ function SneekMark() {
|
|
|
227
237
|
function SneekOtpLogin(props) {
|
|
228
238
|
const {
|
|
229
239
|
title = "Sign in",
|
|
230
|
-
|
|
240
|
+
verifyTitle = "Enter the code",
|
|
241
|
+
subtitle = "Enter your email or phone. We will send you a code.",
|
|
231
242
|
identifierLabel = "Email or mobile",
|
|
232
243
|
identifierPlaceholder = "you@company.com or +91\u2026",
|
|
233
244
|
theme = "auto",
|
|
@@ -240,6 +251,8 @@ function SneekOtpLogin(props) {
|
|
|
240
251
|
} = props;
|
|
241
252
|
const otp = useSneekOtp(handlers);
|
|
242
253
|
const uid = useId();
|
|
254
|
+
const [resendIn, setResendIn] = useState(0);
|
|
255
|
+
const [codeFocused, setCodeFocused] = useState(false);
|
|
243
256
|
const identifierId = `${uid}-identifier`;
|
|
244
257
|
const codeId = `${uid}-code`;
|
|
245
258
|
const statusId = `${uid}-status`;
|
|
@@ -261,6 +274,19 @@ function SneekOtpLogin(props) {
|
|
|
261
274
|
event.preventDefault();
|
|
262
275
|
void otp.verify();
|
|
263
276
|
};
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (otp.step === "verify") setResendIn(RESEND_SECONDS);
|
|
279
|
+
}, [otp.step]);
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (resendIn <= 0) return;
|
|
282
|
+
const timer = setTimeout(() => setResendIn((n) => n - 1), 1e3);
|
|
283
|
+
return () => clearTimeout(timer);
|
|
284
|
+
}, [resendIn]);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (otp.step === "verify" && otp.code.length === CODE_LENGTH && !otp.isBusy) {
|
|
287
|
+
void otp.verify();
|
|
288
|
+
}
|
|
289
|
+
}, [otp.code, otp.step]);
|
|
264
290
|
const label = {
|
|
265
291
|
display: "flex",
|
|
266
292
|
flexDirection: "column",
|
|
@@ -340,10 +366,37 @@ function SneekOtpLogin(props) {
|
|
|
340
366
|
letterSpacing: "-0.01em",
|
|
341
367
|
margin: "16px 0 0"
|
|
342
368
|
},
|
|
343
|
-
children: title
|
|
369
|
+
children: otp.step === "verify" ? verifyTitle : title
|
|
344
370
|
}
|
|
345
371
|
),
|
|
346
|
-
|
|
372
|
+
otp.step === "verify" ? /* @__PURE__ */ jsxs(
|
|
373
|
+
"p",
|
|
374
|
+
{
|
|
375
|
+
style: {
|
|
376
|
+
fontSize: 14,
|
|
377
|
+
lineHeight: "20px",
|
|
378
|
+
color: "var(--sneek-text-muted)",
|
|
379
|
+
margin: "8px 0 0"
|
|
380
|
+
},
|
|
381
|
+
children: [
|
|
382
|
+
"Sent to",
|
|
383
|
+
" ",
|
|
384
|
+
/* @__PURE__ */ jsx("span", { style: { color: "var(--sneek-text)" }, children: otp.identifier }),
|
|
385
|
+
" \xB7 ",
|
|
386
|
+
/* @__PURE__ */ jsx(
|
|
387
|
+
"button",
|
|
388
|
+
{
|
|
389
|
+
type: "button",
|
|
390
|
+
className: "sneek-otp-link",
|
|
391
|
+
onClick: otp.back,
|
|
392
|
+
disabled: otp.isBusy,
|
|
393
|
+
style: { ...link, fontSize: 14 },
|
|
394
|
+
children: "Change"
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
) : subtitle ? /* @__PURE__ */ jsx(
|
|
347
400
|
"p",
|
|
348
401
|
{
|
|
349
402
|
style: {
|
|
@@ -366,10 +419,7 @@ function SneekOtpLogin(props) {
|
|
|
366
419
|
},
|
|
367
420
|
children: otp.error
|
|
368
421
|
}
|
|
369
|
-
) :
|
|
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 }),
|
|
422
|
+
) : null }),
|
|
373
423
|
otp.step === "identify" ? /* @__PURE__ */ jsxs(
|
|
374
424
|
"form",
|
|
375
425
|
{
|
|
@@ -413,23 +463,80 @@ function SneekOtpLogin(props) {
|
|
|
413
463
|
onSubmit: onVerifySubmit,
|
|
414
464
|
style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
|
|
415
465
|
children: [
|
|
416
|
-
/* @__PURE__ */ jsxs("label", { style: label, htmlFor: codeId, children: [
|
|
417
|
-
|
|
466
|
+
/* @__PURE__ */ jsxs("label", { style: { ...label, marginBottom: -8 }, htmlFor: codeId, children: [
|
|
467
|
+
CODE_LENGTH,
|
|
468
|
+
"-digit code"
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
|
|
418
471
|
/* @__PURE__ */ jsx(
|
|
419
472
|
"input",
|
|
420
473
|
{
|
|
421
474
|
id: codeId,
|
|
422
|
-
className: "sneek-otp-input",
|
|
475
|
+
className: "sneek-otp-code-input",
|
|
423
476
|
type: "text",
|
|
424
477
|
inputMode: "numeric",
|
|
478
|
+
pattern: "[0-9]*",
|
|
425
479
|
autoComplete: "one-time-code",
|
|
480
|
+
maxLength: CODE_LENGTH,
|
|
426
481
|
value: otp.code,
|
|
427
|
-
onChange: (e) => otp.setCode(
|
|
428
|
-
|
|
482
|
+
onChange: (e) => otp.setCode(
|
|
483
|
+
e.target.value.replace(/\D/g, "").slice(0, CODE_LENGTH)
|
|
484
|
+
),
|
|
485
|
+
onFocus: () => setCodeFocused(true),
|
|
486
|
+
onBlur: () => setCodeFocused(false),
|
|
429
487
|
autoFocus: true,
|
|
430
488
|
required: true,
|
|
431
489
|
"aria-describedby": statusId,
|
|
432
|
-
style: {
|
|
490
|
+
style: {
|
|
491
|
+
position: "absolute",
|
|
492
|
+
inset: 0,
|
|
493
|
+
width: "100%",
|
|
494
|
+
height: "100%",
|
|
495
|
+
opacity: 0,
|
|
496
|
+
caretColor: "transparent",
|
|
497
|
+
cursor: "text"
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
),
|
|
501
|
+
/* @__PURE__ */ jsx(
|
|
502
|
+
"div",
|
|
503
|
+
{
|
|
504
|
+
className: "sneek-otp-code-cells",
|
|
505
|
+
"aria-hidden": "true",
|
|
506
|
+
style: { display: "flex", gap: 10 },
|
|
507
|
+
children: Array.from({ length: CODE_LENGTH }).map((_, i) => {
|
|
508
|
+
const digit = otp.code[i];
|
|
509
|
+
const isActive = codeFocused && i === otp.code.length;
|
|
510
|
+
return /* @__PURE__ */ jsx(
|
|
511
|
+
"div",
|
|
512
|
+
{
|
|
513
|
+
"data-active": isActive,
|
|
514
|
+
style: {
|
|
515
|
+
flex: 1,
|
|
516
|
+
height: 44,
|
|
517
|
+
display: "flex",
|
|
518
|
+
alignItems: "center",
|
|
519
|
+
justifyContent: "center",
|
|
520
|
+
fontSize: 22,
|
|
521
|
+
color: "var(--sneek-text)",
|
|
522
|
+
borderBottom: `2px solid ${isActive ? "var(--sneek-accent)" : "var(--sneek-border)"}`,
|
|
523
|
+
transition: "border-color 200ms var(--sneek-ease)"
|
|
524
|
+
},
|
|
525
|
+
children: digit ?? (isActive ? /* @__PURE__ */ jsx(
|
|
526
|
+
"span",
|
|
527
|
+
{
|
|
528
|
+
className: "sneek-otp-caret",
|
|
529
|
+
style: {
|
|
530
|
+
width: 2,
|
|
531
|
+
height: 22,
|
|
532
|
+
background: "var(--sneek-accent)"
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
) : null)
|
|
536
|
+
},
|
|
537
|
+
i
|
|
538
|
+
);
|
|
539
|
+
})
|
|
433
540
|
}
|
|
434
541
|
)
|
|
435
542
|
] }),
|
|
@@ -438,35 +545,38 @@ function SneekOtpLogin(props) {
|
|
|
438
545
|
{
|
|
439
546
|
type: "submit",
|
|
440
547
|
className: "sneek-otp-button",
|
|
441
|
-
disabled: otp.isBusy,
|
|
442
|
-
style:
|
|
443
|
-
|
|
548
|
+
disabled: otp.isBusy || otp.code.length < CODE_LENGTH,
|
|
549
|
+
style: {
|
|
550
|
+
...button,
|
|
551
|
+
opacity: otp.isBusy || otp.code.length < CODE_LENGTH ? 0.6 : 1
|
|
552
|
+
},
|
|
553
|
+
children: otp.isVerifying ? "Verifying\u2026" : "Verify"
|
|
444
554
|
}
|
|
445
555
|
),
|
|
446
|
-
/* @__PURE__ */
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
{
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
556
|
+
/* @__PURE__ */ jsx("div", { style: { textAlign: "center" }, children: resendIn > 0 ? /* @__PURE__ */ jsxs(
|
|
557
|
+
"span",
|
|
558
|
+
{
|
|
559
|
+
style: {
|
|
560
|
+
fontSize: 14,
|
|
561
|
+
color: "var(--sneek-text-muted)"
|
|
562
|
+
},
|
|
563
|
+
children: [
|
|
564
|
+
"Resend code in ",
|
|
565
|
+
resendIn,
|
|
566
|
+
"s"
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
) : /* @__PURE__ */ jsx(
|
|
570
|
+
"button",
|
|
571
|
+
{
|
|
572
|
+
type: "button",
|
|
573
|
+
className: "sneek-otp-link",
|
|
574
|
+
onClick: () => void otp.resend(),
|
|
575
|
+
disabled: otp.isBusy,
|
|
576
|
+
style: link,
|
|
577
|
+
children: "Resend code"
|
|
578
|
+
}
|
|
579
|
+
) })
|
|
470
580
|
]
|
|
471
581
|
}
|
|
472
582
|
),
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
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 useState,\n type CSSProperties,\n type FormEvent,\n type ReactNode,\n} from 'react';\n\nimport { buildStyles, SNEEK_STYLE_ID } from './theme';\nimport { useSneekOtp, type SneekOtpHandlers } from './use-sneek-otp';\n\nexport type SneekTheme = 'auto' | 'light' | 'dark';\n\n/** Digits in a Sneek code. */\nconst CODE_LENGTH = 6;\n/** Seconds before \"Resend code\" becomes tappable again. */\nconst RESEND_SECONDS = 30;\n\nexport interface SneekOtpLoginProps<TResult = unknown>\n extends SneekOtpHandlers<TResult> {\n /** Heading above the form. @default 'Sign in' */\n title?: string;\n /** Heading once the code has been sent. @default 'Enter the code' */\n verifyTitle?: 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 verifyTitle = 'Enter the code',\n subtitle = 'Enter your email or phone. We will send you a 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 // Countdown before resend re-enables. Instant resend is both poor feedback\n // and an easy way to burn someone's SMS quota.\n const [resendIn, setResendIn] = useState(0);\n const [codeFocused, setCodeFocused] = useState(false);\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 // Start the cooldown whenever a code goes out.\n useEffect(() => {\n if (otp.step === 'verify') setResendIn(RESEND_SECONDS);\n }, [otp.step]);\n\n useEffect(() => {\n if (resendIn <= 0) return;\n const timer = setTimeout(() => setResendIn((n) => n - 1), 1000);\n return () => clearTimeout(timer);\n }, [resendIn]);\n\n // Submit as soon as the code looks complete — nobody should have to reach\n // for a button after typing the last digit.\n useEffect(() => {\n if (otp.step === 'verify' && otp.code.length === CODE_LENGTH && !otp.isBusy) {\n void otp.verify();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [otp.code, otp.step]);\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 {otp.step === 'verify' ? verifyTitle : title}\n </h1>\n {otp.step === 'verify' ? (\n <p\n style={{\n fontSize: 14,\n lineHeight: '20px',\n color: 'var(--sneek-text-muted)',\n margin: '8px 0 0',\n }}\n >\n Sent to{' '}\n <span style={{ color: 'var(--sneek-text)' }}>{otp.identifier}</span>\n {' · '}\n <button\n type=\"button\"\n className=\"sneek-otp-link\"\n onClick={otp.back}\n disabled={otp.isBusy}\n style={{ ...link, fontSize: 14 }}\n >\n Change\n </button>\n </p>\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 ) : 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, marginBottom: -8 }} htmlFor={codeId}>\n {CODE_LENGTH}-digit code\n </label>\n {/*\n * One real input captures keystrokes — paste, backspace and\n * SMS/email autofill all keep working, which six separate\n * `<input>`s make surprisingly easy to break. It sits over\n * the visible cells, transparent; the cells below just\n * mirror its value.\n */}\n <div style={{ position: 'relative' }}>\n <input\n id={codeId}\n className=\"sneek-otp-code-input\"\n type=\"text\"\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n autoComplete=\"one-time-code\"\n maxLength={CODE_LENGTH}\n value={otp.code}\n onChange={(e) =>\n otp.setCode(\n e.target.value.replace(/\\D/g, '').slice(0, CODE_LENGTH),\n )\n }\n onFocus={() => setCodeFocused(true)}\n onBlur={() => setCodeFocused(false)}\n autoFocus\n required\n aria-describedby={statusId}\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n opacity: 0,\n caretColor: 'transparent',\n cursor: 'text',\n }}\n />\n <div\n className=\"sneek-otp-code-cells\"\n aria-hidden=\"true\"\n style={{ display: 'flex', gap: 10 }}\n >\n {Array.from({ length: CODE_LENGTH }).map((_, i) => {\n const digit = otp.code[i];\n const isActive = codeFocused && i === otp.code.length;\n return (\n <div\n key={i}\n data-active={isActive}\n style={{\n flex: 1,\n height: 44,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: 22,\n color: 'var(--sneek-text)',\n borderBottom: `2px solid ${\n isActive ? 'var(--sneek-accent)' : 'var(--sneek-border)'\n }`,\n transition: 'border-color 200ms var(--sneek-ease)',\n }}\n >\n {digit ?? (isActive ? (\n <span\n className=\"sneek-otp-caret\"\n style={{\n width: 2,\n height: 22,\n background: 'var(--sneek-accent)',\n }}\n />\n ) : null)}\n </div>\n );\n })}\n </div>\n </div>\n <button\n type=\"submit\"\n className=\"sneek-otp-button\"\n disabled={otp.isBusy || otp.code.length < CODE_LENGTH}\n style={{\n ...button,\n opacity:\n otp.isBusy || otp.code.length < CODE_LENGTH ? 0.6 : 1,\n }}\n >\n {otp.isVerifying ? 'Verifying…' : 'Verify'}\n </button>\n <div style={{ textAlign: 'center' }}>\n {resendIn > 0 ? (\n <span\n style={{\n fontSize: 14,\n color: 'var(--sneek-text-muted)',\n }}\n >\n Resend code in {resendIn}s\n </span>\n ) : (\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 )}\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@keyframes sneek-otp-caret {\n 0%, 45% { opacity: 1; }\n 50%, 100% { opacity: 0; }\n}\n.sneek-otp-caret {\n animation: sneek-otp-caret 1s step-end infinite;\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":[]}
|