sneekui 0.1.1

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 ADDED
@@ -0,0 +1,152 @@
1
+ # sneekui
2
+
3
+ Drop-in **passwordless OTP login** UI for React, powered by [Sneek](https://sneek.in).
4
+
5
+ One component renders the whole two-step flow — *enter your email/mobile → enter the
6
+ code → signed in* — over SMS, WhatsApp, or email. No passwords.
7
+
8
+ ## The security model (read this first)
9
+
10
+ `sneekui` runs in the browser, so it **never holds a Sneek API key**. Instead it
11
+ calls **your own backend**, and your backend calls Sneek server-side with your secret
12
+ key (using [`sneeksdk`](../../sdk)).
13
+
14
+ ```
15
+ Browser (sneekui)
16
+ │ POST /api/auth/request-otp { identifier }
17
+
18
+ Your backend ──(sneeksdk, secret key)──► Sneek API ──► SMS / WhatsApp / Email
19
+ │ POST /api/auth/verify-otp { requestId, code }
20
+
21
+ Your backend issues your session / token
22
+ ```
23
+
24
+ Putting a Sneek key in front-end code would expose it to every visitor. This package
25
+ is designed so that can't happen.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install sneekui
31
+ ```
32
+
33
+ `react >= 17` is a peer dependency.
34
+
35
+ ## Quick start
36
+
37
+ ```tsx
38
+ import { SneekOtpLogin, createFetchHandlers } from 'sneekui';
39
+
40
+ export function LoginPage() {
41
+ return (
42
+ <SneekOtpLogin
43
+ title="Sign in to ConfHub"
44
+ {...createFetchHandlers({
45
+ requestUrl: '/api/auth/request-otp',
46
+ verifyUrl: '/api/auth/verify-otp',
47
+ })}
48
+ onSuccess={() => {
49
+ window.location.href = '/dashboard';
50
+ }}
51
+ />
52
+ );
53
+ }
54
+ ```
55
+
56
+ That's the whole front end. `createFetchHandlers` POSTs to your endpoints; defaults are
57
+ `/api/auth/request-otp` and `/api/auth/verify-otp`.
58
+
59
+ ### Your backend endpoints
60
+
61
+ ```ts
62
+ // POST /api/auth/request-otp body: { identifier }
63
+ import { Sneek } from 'sneeksdk';
64
+ const sneek = new Sneek({ apiKey: process.env.SNEEK_API_KEY! });
65
+
66
+ const otp = await sneek.sendOTP({ to: identifier, channel: 'auto' });
67
+ return { requestId: otp.id, channels: [otp.channel], expiresInSeconds: 300 };
68
+
69
+ // POST /api/auth/verify-otp body: { requestId, code }
70
+ // verify against your store, then return your own session/token
71
+ ```
72
+
73
+ > The exact verify call depends on how you persist the OTP request. Sneek returns a
74
+ > message id on send; store it keyed by `requestId` and check the code your way, or use
75
+ > Sneek's verify endpoint if enabled for your app.
76
+
77
+ ## Headless usage
78
+
79
+ Want your own markup? Use the hook:
80
+
81
+ ```tsx
82
+ import { useSneekOtp, createFetchHandlers } from 'sneekui';
83
+
84
+ function MyLogin() {
85
+ const otp = useSneekOtp({
86
+ ...createFetchHandlers(),
87
+ onSuccess: (user) => console.log('signed in', user),
88
+ });
89
+
90
+ if (otp.step === 'identify') {
91
+ return (
92
+ <form onSubmit={(e) => { e.preventDefault(); otp.sendOtp(); }}>
93
+ <input value={otp.identifier} onChange={(e) => otp.setIdentifier(e.target.value)} />
94
+ <button disabled={otp.isBusy}>{otp.isSending ? 'Sending…' : 'Send code'}</button>
95
+ {otp.error && <p>{otp.error}</p>}
96
+ </form>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <form onSubmit={(e) => { e.preventDefault(); otp.verify(); }}>
102
+ <input value={otp.code} onChange={(e) => otp.setCode(e.target.value)} />
103
+ <button disabled={otp.isBusy}>{otp.isVerifying ? 'Verifying…' : 'Verify'}</button>
104
+ <button type="button" onClick={otp.back}>Back</button>
105
+ <button type="button" onClick={otp.resend}>Resend</button>
106
+ </form>
107
+ );
108
+ }
109
+ ```
110
+
111
+ ### Custom transport
112
+
113
+ Don't want `fetch`? Provide `requestOtp` / `verifyOtp` directly:
114
+
115
+ ```tsx
116
+ useSneekOtp({
117
+ requestOtp: async (identifier) => {
118
+ const r = await myClient.sendOtp(identifier);
119
+ return { requestId: r.id, channels: r.channels, expiresInSeconds: r.ttl };
120
+ },
121
+ verifyOtp: async ({ requestId, code }) => myClient.verify(requestId, code),
122
+ onSuccess: (result) => {/* … */},
123
+ });
124
+ ```
125
+
126
+ ## API
127
+
128
+ ### `<SneekOtpLogin />`
129
+
130
+ | Prop | Type | Default | Notes |
131
+ | --- | --- | --- | --- |
132
+ | `requestOtp` | `(id) => Promise<RequestOtpResult>` | — | Required |
133
+ | `verifyOtp` | `({requestId, code}) => Promise<T>` | — | Required |
134
+ | `onSuccess` | `(result: T) => void` | — | After verify |
135
+ | `onError` | `(error: unknown) => void` | — | Any failure |
136
+ | `title` | `string` | `'Sign in'` | |
137
+ | `subtitle` | `ReactNode` | Sneek tagline | |
138
+ | `accentColor` | `string` | `'#56d3b5'` | Button colour |
139
+ | `logo` | `ReactNode` | — | Above the title |
140
+ | `style` / `className` | — | — | Container overrides |
141
+
142
+ `createFetchHandlers(options)` returns `{ requestOtp, verifyOtp }`, so spread it into the
143
+ component or hook.
144
+
145
+ ### `useSneekOtp(handlers)`
146
+
147
+ Returns the reducer state plus `setIdentifier`, `setCode`, `sendOtp`, `verify`, `back`,
148
+ `resend`, and the booleans `isSending`, `isVerifying`, `isBusy`.
149
+
150
+ ## License
151
+
152
+ UNLICENSED — © Sneek.
@@ -0,0 +1,160 @@
1
+ import { ReactNode, CSSProperties } from 'react';
2
+
3
+ /**
4
+ * Framework-agnostic state machine for the Sneek passwordless OTP flow.
5
+ *
6
+ * Kept free of React so it can be unit-tested in isolation and reused by other
7
+ * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a
8
+ * thin wrapper around this reducer.
9
+ */
10
+ type SneekChannel = 'sms' | 'whatsapp' | 'email';
11
+ /** Which screen of the two-step flow is showing. */
12
+ type SneekOtpStep = 'identify' | 'verify';
13
+ /** Async lifecycle for the in-flight request. */
14
+ type SneekOtpStatus = 'idle' | 'sending' | 'verifying';
15
+ interface SneekOtpState {
16
+ step: SneekOtpStep;
17
+ status: SneekOtpStatus;
18
+ /** The email / mobile / username the user typed. */
19
+ identifier: string;
20
+ /** The OTP code the user typed on the verify screen. */
21
+ code: string;
22
+ /** Opaque id returned by the partner backend, replayed on verify. */
23
+ requestId: string;
24
+ /** Channels the OTP was actually delivered over. */
25
+ channels: SneekChannel[];
26
+ /** Seconds until the OTP expires (from the request response). */
27
+ expiresInSeconds: number;
28
+ /** User-facing error message, or null. */
29
+ error: string | null;
30
+ }
31
+ declare const initialOtpState: SneekOtpState;
32
+ interface RequestOtpResult {
33
+ requestId: string;
34
+ channels?: SneekChannel[];
35
+ expiresInSeconds?: number;
36
+ }
37
+ type SneekOtpAction = {
38
+ type: 'set_identifier';
39
+ value: string;
40
+ } | {
41
+ type: 'set_code';
42
+ value: string;
43
+ } | {
44
+ type: 'request_start';
45
+ } | {
46
+ type: 'request_success';
47
+ result: RequestOtpResult;
48
+ } | {
49
+ type: 'request_error';
50
+ message: string;
51
+ } | {
52
+ type: 'verify_start';
53
+ } | {
54
+ type: 'verify_error';
55
+ message: string;
56
+ } | {
57
+ type: 'reset';
58
+ } | {
59
+ type: 'back_to_identify';
60
+ };
61
+ declare function otpReducer(state: SneekOtpState, action: SneekOtpAction): SneekOtpState;
62
+ /** "SMS, WhatsApp" — human label for the channels an OTP was sent over. */
63
+ declare function formatChannels(channels: SneekChannel[]): string;
64
+
65
+ /**
66
+ * Partner-supplied transport. These call the *partner's own backend*, which in
67
+ * turn talks to the Sneek API with the secret server-side key. The browser
68
+ * never sees a Sneek API key — that is the whole security model of this package.
69
+ */
70
+ interface SneekOtpHandlers<TResult = unknown> {
71
+ /** Ask the partner backend to send an OTP to `identifier`. */
72
+ requestOtp: (identifier: string) => Promise<RequestOtpResult>;
73
+ /** Verify the code with the partner backend; resolve with the auth result. */
74
+ verifyOtp: (input: {
75
+ requestId: string;
76
+ code: string;
77
+ }) => Promise<TResult>;
78
+ /** Called after a successful verification with the partner's result. */
79
+ onSuccess?: (result: TResult) => void;
80
+ /** Called on any error (request or verify). */
81
+ onError?: (error: unknown) => void;
82
+ }
83
+ interface UseSneekOtp extends SneekOtpState {
84
+ setIdentifier: (value: string) => void;
85
+ setCode: (value: string) => void;
86
+ /** Submit the identify step — triggers `requestOtp`. */
87
+ sendOtp: () => Promise<void>;
88
+ /** Submit the verify step — triggers `verifyOtp`. */
89
+ verify: () => Promise<void>;
90
+ /** Go back to the identify screen (e.g. "use a different email"). */
91
+ back: () => void;
92
+ /** Re-send the OTP to the same identifier. */
93
+ resend: () => Promise<void>;
94
+ /** Convenience booleans. */
95
+ isSending: boolean;
96
+ isVerifying: boolean;
97
+ isBusy: boolean;
98
+ }
99
+ /**
100
+ * Headless hook implementing the passwordless OTP login flow. Bring your own
101
+ * markup, or use the {@link SneekOtpLogin} component for a styled default.
102
+ */
103
+ declare function useSneekOtp<TResult = unknown>(handlers: SneekOtpHandlers<TResult>): UseSneekOtp;
104
+
105
+ interface SneekOtpLoginProps<TResult = unknown> extends SneekOtpHandlers<TResult> {
106
+ /** Heading shown above the form. @default 'Sign in' */
107
+ title?: string;
108
+ /** Sub-heading. @default 'Passwordless login powered by Sneek' */
109
+ subtitle?: ReactNode;
110
+ /** Label for the identifier input. */
111
+ identifierLabel?: string;
112
+ /** Placeholder for the identifier input. */
113
+ identifierPlaceholder?: string;
114
+ /** Brand colour for the primary button. @default '#56d3b5' */
115
+ accentColor?: string;
116
+ /** Optional logo rendered above the title. */
117
+ logo?: ReactNode;
118
+ /** Override the outer container style. */
119
+ style?: CSSProperties;
120
+ /** Extra className on the outer container. */
121
+ className?: string;
122
+ }
123
+ /**
124
+ * Drop-in, dependency-free passwordless OTP login card. The component never
125
+ * receives a Sneek API key; it calls the partner-supplied handlers, which talk
126
+ * to the partner backend. Use {@link createFetchHandlers} for the common case.
127
+ */
128
+ declare function SneekOtpLogin<TResult = unknown>(props: SneekOtpLoginProps<TResult>): ReactNode;
129
+
130
+ interface FetchHandlerOptions {
131
+ /**
132
+ * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,
133
+ * must return `{ requestId, channels?, expiresInSeconds? }`.
134
+ * @default '/api/auth/request-otp'
135
+ */
136
+ requestUrl?: string;
137
+ /**
138
+ * Partner backend endpoint that verifies an OTP. Receives
139
+ * `{ requestId, code }`, returns whatever your app needs (session, token…).
140
+ * @default '/api/auth/verify-otp'
141
+ */
142
+ verifyUrl?: string;
143
+ /** Extra headers (e.g. CSRF token) to send on both requests. */
144
+ headers?: Record<string, string>;
145
+ /** Forwarded to fetch — set to 'include' if you use cookie sessions. */
146
+ credentials?: RequestCredentials;
147
+ }
148
+ /**
149
+ * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.
150
+ * Those endpoints call the Sneek API server-side with your secret key.
151
+ */
152
+ declare function createFetchHandlers<TResult = unknown>(options?: FetchHandlerOptions): {
153
+ requestOtp: (identifier: string) => Promise<RequestOtpResult>;
154
+ verifyOtp: (input: {
155
+ requestId: string;
156
+ code: string;
157
+ }) => Promise<TResult>;
158
+ };
159
+
160
+ export { type FetchHandlerOptions, type RequestOtpResult, type SneekChannel, type SneekOtpAction, type SneekOtpHandlers, SneekOtpLogin, type SneekOtpLoginProps, type SneekOtpState, type SneekOtpStatus, type SneekOtpStep, type UseSneekOtp, createFetchHandlers, formatChannels, initialOtpState, otpReducer, useSneekOtp };
@@ -0,0 +1,160 @@
1
+ import { ReactNode, CSSProperties } from 'react';
2
+
3
+ /**
4
+ * Framework-agnostic state machine for the Sneek passwordless OTP flow.
5
+ *
6
+ * Kept free of React so it can be unit-tested in isolation and reused by other
7
+ * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a
8
+ * thin wrapper around this reducer.
9
+ */
10
+ type SneekChannel = 'sms' | 'whatsapp' | 'email';
11
+ /** Which screen of the two-step flow is showing. */
12
+ type SneekOtpStep = 'identify' | 'verify';
13
+ /** Async lifecycle for the in-flight request. */
14
+ type SneekOtpStatus = 'idle' | 'sending' | 'verifying';
15
+ interface SneekOtpState {
16
+ step: SneekOtpStep;
17
+ status: SneekOtpStatus;
18
+ /** The email / mobile / username the user typed. */
19
+ identifier: string;
20
+ /** The OTP code the user typed on the verify screen. */
21
+ code: string;
22
+ /** Opaque id returned by the partner backend, replayed on verify. */
23
+ requestId: string;
24
+ /** Channels the OTP was actually delivered over. */
25
+ channels: SneekChannel[];
26
+ /** Seconds until the OTP expires (from the request response). */
27
+ expiresInSeconds: number;
28
+ /** User-facing error message, or null. */
29
+ error: string | null;
30
+ }
31
+ declare const initialOtpState: SneekOtpState;
32
+ interface RequestOtpResult {
33
+ requestId: string;
34
+ channels?: SneekChannel[];
35
+ expiresInSeconds?: number;
36
+ }
37
+ type SneekOtpAction = {
38
+ type: 'set_identifier';
39
+ value: string;
40
+ } | {
41
+ type: 'set_code';
42
+ value: string;
43
+ } | {
44
+ type: 'request_start';
45
+ } | {
46
+ type: 'request_success';
47
+ result: RequestOtpResult;
48
+ } | {
49
+ type: 'request_error';
50
+ message: string;
51
+ } | {
52
+ type: 'verify_start';
53
+ } | {
54
+ type: 'verify_error';
55
+ message: string;
56
+ } | {
57
+ type: 'reset';
58
+ } | {
59
+ type: 'back_to_identify';
60
+ };
61
+ declare function otpReducer(state: SneekOtpState, action: SneekOtpAction): SneekOtpState;
62
+ /** "SMS, WhatsApp" — human label for the channels an OTP was sent over. */
63
+ declare function formatChannels(channels: SneekChannel[]): string;
64
+
65
+ /**
66
+ * Partner-supplied transport. These call the *partner's own backend*, which in
67
+ * turn talks to the Sneek API with the secret server-side key. The browser
68
+ * never sees a Sneek API key — that is the whole security model of this package.
69
+ */
70
+ interface SneekOtpHandlers<TResult = unknown> {
71
+ /** Ask the partner backend to send an OTP to `identifier`. */
72
+ requestOtp: (identifier: string) => Promise<RequestOtpResult>;
73
+ /** Verify the code with the partner backend; resolve with the auth result. */
74
+ verifyOtp: (input: {
75
+ requestId: string;
76
+ code: string;
77
+ }) => Promise<TResult>;
78
+ /** Called after a successful verification with the partner's result. */
79
+ onSuccess?: (result: TResult) => void;
80
+ /** Called on any error (request or verify). */
81
+ onError?: (error: unknown) => void;
82
+ }
83
+ interface UseSneekOtp extends SneekOtpState {
84
+ setIdentifier: (value: string) => void;
85
+ setCode: (value: string) => void;
86
+ /** Submit the identify step — triggers `requestOtp`. */
87
+ sendOtp: () => Promise<void>;
88
+ /** Submit the verify step — triggers `verifyOtp`. */
89
+ verify: () => Promise<void>;
90
+ /** Go back to the identify screen (e.g. "use a different email"). */
91
+ back: () => void;
92
+ /** Re-send the OTP to the same identifier. */
93
+ resend: () => Promise<void>;
94
+ /** Convenience booleans. */
95
+ isSending: boolean;
96
+ isVerifying: boolean;
97
+ isBusy: boolean;
98
+ }
99
+ /**
100
+ * Headless hook implementing the passwordless OTP login flow. Bring your own
101
+ * markup, or use the {@link SneekOtpLogin} component for a styled default.
102
+ */
103
+ declare function useSneekOtp<TResult = unknown>(handlers: SneekOtpHandlers<TResult>): UseSneekOtp;
104
+
105
+ interface SneekOtpLoginProps<TResult = unknown> extends SneekOtpHandlers<TResult> {
106
+ /** Heading shown above the form. @default 'Sign in' */
107
+ title?: string;
108
+ /** Sub-heading. @default 'Passwordless login powered by Sneek' */
109
+ subtitle?: ReactNode;
110
+ /** Label for the identifier input. */
111
+ identifierLabel?: string;
112
+ /** Placeholder for the identifier input. */
113
+ identifierPlaceholder?: string;
114
+ /** Brand colour for the primary button. @default '#56d3b5' */
115
+ accentColor?: string;
116
+ /** Optional logo rendered above the title. */
117
+ logo?: ReactNode;
118
+ /** Override the outer container style. */
119
+ style?: CSSProperties;
120
+ /** Extra className on the outer container. */
121
+ className?: string;
122
+ }
123
+ /**
124
+ * Drop-in, dependency-free passwordless OTP login card. The component never
125
+ * receives a Sneek API key; it calls the partner-supplied handlers, which talk
126
+ * to the partner backend. Use {@link createFetchHandlers} for the common case.
127
+ */
128
+ declare function SneekOtpLogin<TResult = unknown>(props: SneekOtpLoginProps<TResult>): ReactNode;
129
+
130
+ interface FetchHandlerOptions {
131
+ /**
132
+ * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,
133
+ * must return `{ requestId, channels?, expiresInSeconds? }`.
134
+ * @default '/api/auth/request-otp'
135
+ */
136
+ requestUrl?: string;
137
+ /**
138
+ * Partner backend endpoint that verifies an OTP. Receives
139
+ * `{ requestId, code }`, returns whatever your app needs (session, token…).
140
+ * @default '/api/auth/verify-otp'
141
+ */
142
+ verifyUrl?: string;
143
+ /** Extra headers (e.g. CSRF token) to send on both requests. */
144
+ headers?: Record<string, string>;
145
+ /** Forwarded to fetch — set to 'include' if you use cookie sessions. */
146
+ credentials?: RequestCredentials;
147
+ }
148
+ /**
149
+ * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.
150
+ * Those endpoints call the Sneek API server-side with your secret key.
151
+ */
152
+ declare function createFetchHandlers<TResult = unknown>(options?: FetchHandlerOptions): {
153
+ requestOtp: (identifier: string) => Promise<RequestOtpResult>;
154
+ verifyOtp: (input: {
155
+ requestId: string;
156
+ code: string;
157
+ }) => Promise<TResult>;
158
+ };
159
+
160
+ export { type FetchHandlerOptions, type RequestOtpResult, type SneekChannel, type SneekOtpAction, type SneekOtpHandlers, SneekOtpLogin, type SneekOtpLoginProps, type SneekOtpState, type SneekOtpStatus, type SneekOtpStep, type UseSneekOtp, createFetchHandlers, formatChannels, initialOtpState, otpReducer, useSneekOtp };
package/dist/index.js ADDED
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SneekOtpLogin: () => SneekOtpLogin,
24
+ createFetchHandlers: () => createFetchHandlers,
25
+ formatChannels: () => formatChannels,
26
+ initialOtpState: () => initialOtpState,
27
+ otpReducer: () => otpReducer,
28
+ useSneekOtp: () => useSneekOtp
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/use-sneek-otp.ts
33
+ var import_react = require("react");
34
+
35
+ // src/state.ts
36
+ var initialOtpState = {
37
+ step: "identify",
38
+ status: "idle",
39
+ identifier: "",
40
+ code: "",
41
+ requestId: "",
42
+ channels: [],
43
+ expiresInSeconds: 0,
44
+ error: null
45
+ };
46
+ var isBusy = (status) => status !== "idle";
47
+ function otpReducer(state, action) {
48
+ switch (action.type) {
49
+ case "set_identifier":
50
+ return { ...state, identifier: action.value, error: null };
51
+ case "set_code":
52
+ return { ...state, code: action.value, error: null };
53
+ case "request_start":
54
+ if (isBusy(state.status)) return state;
55
+ return { ...state, status: "sending", error: null };
56
+ case "request_success":
57
+ return {
58
+ ...state,
59
+ step: "verify",
60
+ status: "idle",
61
+ code: "",
62
+ requestId: action.result.requestId,
63
+ channels: action.result.channels ?? [],
64
+ expiresInSeconds: action.result.expiresInSeconds ?? 0,
65
+ error: null
66
+ };
67
+ case "request_error":
68
+ return { ...state, status: "idle", error: action.message };
69
+ case "verify_start":
70
+ if (isBusy(state.status)) return state;
71
+ return { ...state, status: "verifying", error: null };
72
+ case "verify_error":
73
+ return { ...state, status: "idle", error: action.message };
74
+ case "back_to_identify":
75
+ return {
76
+ ...state,
77
+ step: "identify",
78
+ status: "idle",
79
+ code: "",
80
+ requestId: "",
81
+ channels: [],
82
+ expiresInSeconds: 0,
83
+ error: null
84
+ };
85
+ case "reset":
86
+ return { ...initialOtpState };
87
+ default:
88
+ return state;
89
+ }
90
+ }
91
+ var CHANNEL_LABELS = {
92
+ sms: "SMS",
93
+ whatsapp: "WhatsApp",
94
+ email: "email"
95
+ };
96
+ function formatChannels(channels) {
97
+ return channels.map((c) => CHANNEL_LABELS[c] ?? c).join(", ");
98
+ }
99
+ function toMessage(error, fallback) {
100
+ if (error instanceof Error && error.message) return error.message;
101
+ if (typeof error === "string" && error) return error;
102
+ return fallback;
103
+ }
104
+
105
+ // src/use-sneek-otp.ts
106
+ var DEFAULT_REQUEST_ERROR = "Could not send the code. Please try again.";
107
+ var DEFAULT_VERIFY_ERROR = "That code was not correct. Please try again.";
108
+ function useSneekOtp(handlers) {
109
+ const [state, dispatch] = (0, import_react.useReducer)(otpReducer, initialOtpState);
110
+ const { requestOtp, verifyOtp, onSuccess, onError } = handlers;
111
+ const setIdentifier = (0, import_react.useCallback)((value) => {
112
+ dispatch({ type: "set_identifier", value });
113
+ }, []);
114
+ const setCode = (0, import_react.useCallback)((value) => {
115
+ dispatch({ type: "set_code", value });
116
+ }, []);
117
+ const runRequest = (0, import_react.useCallback)(
118
+ async (identifier) => {
119
+ const trimmed = identifier.trim();
120
+ if (!trimmed) {
121
+ dispatch({ type: "request_error", message: "Enter your email or mobile number." });
122
+ return;
123
+ }
124
+ dispatch({ type: "request_start" });
125
+ try {
126
+ const result = await requestOtp(trimmed);
127
+ dispatch({ type: "request_success", result });
128
+ } catch (error) {
129
+ dispatch({ type: "request_error", message: toMessage(error, DEFAULT_REQUEST_ERROR) });
130
+ onError?.(error);
131
+ }
132
+ },
133
+ [requestOtp, onError]
134
+ );
135
+ const sendOtp = (0, import_react.useCallback)(() => runRequest(state.identifier), [runRequest, state.identifier]);
136
+ const resend = (0, import_react.useCallback)(() => runRequest(state.identifier), [runRequest, state.identifier]);
137
+ const verify = (0, import_react.useCallback)(async () => {
138
+ const code = state.code.trim();
139
+ if (!code) {
140
+ dispatch({ type: "verify_error", message: "Enter the code you received." });
141
+ return;
142
+ }
143
+ dispatch({ type: "verify_start" });
144
+ try {
145
+ const result = await verifyOtp({ requestId: state.requestId, code });
146
+ onSuccess?.(result);
147
+ } catch (error) {
148
+ dispatch({ type: "verify_error", message: toMessage(error, DEFAULT_VERIFY_ERROR) });
149
+ onError?.(error);
150
+ }
151
+ }, [verifyOtp, state.code, state.requestId, onSuccess, onError]);
152
+ const back = (0, import_react.useCallback)(() => {
153
+ dispatch({ type: "back_to_identify" });
154
+ }, []);
155
+ return {
156
+ ...state,
157
+ setIdentifier,
158
+ setCode,
159
+ sendOtp,
160
+ verify,
161
+ back,
162
+ resend,
163
+ isSending: state.status === "sending",
164
+ isVerifying: state.status === "verifying",
165
+ isBusy: state.status !== "idle"
166
+ };
167
+ }
168
+
169
+ // src/component.tsx
170
+ var import_jsx_runtime = require("react/jsx-runtime");
171
+ function SneekOtpLogin(props) {
172
+ const {
173
+ title = "Sign in",
174
+ subtitle = "Passwordless login powered by Sneek",
175
+ identifierLabel = "Email or mobile number",
176
+ identifierPlaceholder = "you@example.com or +9198xxxxxxxx",
177
+ accentColor = "#56d3b5",
178
+ logo,
179
+ style,
180
+ className,
181
+ ...handlers
182
+ } = props;
183
+ const otp = useSneekOtp(handlers);
184
+ const onIdentifySubmit = (event) => {
185
+ event.preventDefault();
186
+ void otp.sendOtp();
187
+ };
188
+ const onVerifySubmit = (event) => {
189
+ event.preventDefault();
190
+ void otp.verify();
191
+ };
192
+ const inputStyle = {
193
+ padding: "12px 14px",
194
+ border: "1px solid rgba(0,0,0,0.15)",
195
+ borderRadius: 10,
196
+ fontSize: "0.95rem",
197
+ width: "100%",
198
+ boxSizing: "border-box",
199
+ outline: "none"
200
+ };
201
+ const buttonStyle = {
202
+ marginTop: 4,
203
+ padding: "12px 14px",
204
+ border: "none",
205
+ borderRadius: 10,
206
+ background: accentColor,
207
+ color: "#04231d",
208
+ fontSize: "1rem",
209
+ fontWeight: 600,
210
+ cursor: otp.isBusy ? "not-allowed" : "pointer",
211
+ opacity: otp.isBusy ? 0.6 : 1,
212
+ width: "100%"
213
+ };
214
+ const linkStyle = {
215
+ border: "none",
216
+ background: "transparent",
217
+ color: accentColor,
218
+ cursor: "pointer",
219
+ font: "inherit",
220
+ fontSize: "0.9rem",
221
+ padding: 0
222
+ };
223
+ const errorStyle = {
224
+ background: "rgba(255,107,94,0.12)",
225
+ color: "#c0392b",
226
+ padding: "10px 12px",
227
+ border: "1px solid rgba(255,107,94,0.25)",
228
+ borderRadius: 8,
229
+ fontSize: "0.85rem"
230
+ };
231
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
232
+ "div",
233
+ {
234
+ className,
235
+ style: {
236
+ maxWidth: 400,
237
+ margin: "0 auto",
238
+ padding: 32,
239
+ border: "1px solid rgba(0,0,0,0.08)",
240
+ borderRadius: 16,
241
+ fontFamily: "system-ui, -apple-system, sans-serif",
242
+ ...style
243
+ },
244
+ children: [
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", marginBottom: 24 }, children: [
246
+ logo,
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: { fontSize: "1.4rem", fontWeight: 700, margin: "8px 0 4px" }, children: title }),
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "0.9rem", opacity: 0.7, margin: 0 }, children: subtitle })
249
+ ] }),
250
+ otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: onIdentifySubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
251
+ otp.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: errorStyle, children: otp.error }),
252
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: "0.85rem" }, children: [
253
+ identifierLabel,
254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
255
+ "input",
256
+ {
257
+ type: "text",
258
+ value: otp.identifier,
259
+ onChange: (e) => otp.setIdentifier(e.target.value),
260
+ placeholder: identifierPlaceholder,
261
+ autoComplete: "username",
262
+ autoFocus: true,
263
+ required: true,
264
+ style: inputStyle
265
+ }
266
+ )
267
+ ] }),
268
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: otp.isBusy, style: buttonStyle, children: otp.isSending ? "Sending code\u2026" : "Send code" })
269
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: onVerifySubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
270
+ otp.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: errorStyle, children: otp.error }),
271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
272
+ "div",
273
+ {
274
+ style: {
275
+ background: "rgba(86,211,181,0.12)",
276
+ padding: "10px 12px",
277
+ borderRadius: 8,
278
+ fontSize: "0.85rem"
279
+ },
280
+ children: [
281
+ otp.channels.length > 0 ? `Code sent via ${formatChannels(otp.channels)}.` : "We sent you a code.",
282
+ otp.expiresInSeconds > 0 && ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`
283
+ ]
284
+ }
285
+ ),
286
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: "0.85rem" }, children: [
287
+ "Verification code",
288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
289
+ "input",
290
+ {
291
+ type: "text",
292
+ inputMode: "numeric",
293
+ autoComplete: "one-time-code",
294
+ value: otp.code,
295
+ onChange: (e) => otp.setCode(e.target.value),
296
+ placeholder: "Enter code",
297
+ autoFocus: true,
298
+ required: true,
299
+ style: inputStyle
300
+ }
301
+ )
302
+ ] }),
303
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: otp.isBusy, style: buttonStyle, children: otp.isVerifying ? "Verifying\u2026" : "Verify and sign in" }),
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: otp.back, disabled: otp.isBusy, style: linkStyle, children: "Use a different contact" }),
306
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => void otp.resend(), disabled: otp.isBusy, style: linkStyle, children: "Resend code" })
307
+ ] })
308
+ ] })
309
+ ]
310
+ }
311
+ );
312
+ }
313
+
314
+ // src/fetch-handlers.ts
315
+ async function postJson(url, body, options) {
316
+ const response = await fetch(url, {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/json", ...options.headers ?? {} },
319
+ credentials: options.credentials,
320
+ body: JSON.stringify(body)
321
+ });
322
+ if (!response.ok) {
323
+ let message = `Request failed (${response.status})`;
324
+ try {
325
+ const data = await response.json();
326
+ message = data.message || data.error || message;
327
+ } catch {
328
+ }
329
+ throw new Error(message);
330
+ }
331
+ return await response.json();
332
+ }
333
+ function createFetchHandlers(options = {}) {
334
+ const requestUrl = options.requestUrl ?? "/api/auth/request-otp";
335
+ const verifyUrl = options.verifyUrl ?? "/api/auth/verify-otp";
336
+ return {
337
+ requestOtp: (identifier) => postJson(requestUrl, { identifier }, options),
338
+ verifyOtp: (input) => postJson(verifyUrl, input, options)
339
+ };
340
+ }
341
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/use-sneek-otp.ts","../src/state.ts","../src/component.tsx","../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} from './component';\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 { type CSSProperties, type FormEvent, type ReactNode } from 'react';\nimport { formatChannels } from './state';\nimport { useSneekOtp, type SneekOtpHandlers } from './use-sneek-otp';\n\nexport interface SneekOtpLoginProps<TResult = unknown>\n extends SneekOtpHandlers<TResult> {\n /** Heading shown above the form. @default 'Sign in' */\n title?: string;\n /** Sub-heading. @default 'Passwordless login powered by Sneek' */\n subtitle?: ReactNode;\n /** Label for the identifier input. */\n identifierLabel?: string;\n /** Placeholder for the identifier input. */\n identifierPlaceholder?: string;\n /** Brand colour for the primary button. @default '#56d3b5' */\n accentColor?: string;\n /** Optional logo rendered above the title. */\n logo?: ReactNode;\n /** Override the outer container style. */\n style?: CSSProperties;\n /** Extra className on the outer container. */\n className?: string;\n}\n\n/**\n * Drop-in, dependency-free passwordless OTP login card. The component never\n * receives a Sneek API key; it calls the partner-supplied handlers, which talk\n * to the partner backend. Use {@link createFetchHandlers} for the common case.\n */\nexport function SneekOtpLogin<TResult = unknown>(\n props: SneekOtpLoginProps<TResult>,\n): ReactNode {\n const {\n title = 'Sign in',\n subtitle = 'Passwordless login powered by Sneek',\n identifierLabel = 'Email or mobile number',\n identifierPlaceholder = 'you@example.com or +9198xxxxxxxx',\n accentColor = '#56d3b5',\n logo,\n style,\n className,\n ...handlers\n } = props;\n\n const otp = useSneekOtp<TResult>(handlers);\n\n const onIdentifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.sendOtp();\n };\n\n const onVerifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.verify();\n };\n\n const inputStyle: CSSProperties = {\n padding: '12px 14px',\n border: '1px solid rgba(0,0,0,0.15)',\n borderRadius: 10,\n fontSize: '0.95rem',\n width: '100%',\n boxSizing: 'border-box',\n outline: 'none',\n };\n\n const buttonStyle: CSSProperties = {\n marginTop: 4,\n padding: '12px 14px',\n border: 'none',\n borderRadius: 10,\n background: accentColor,\n color: '#04231d',\n fontSize: '1rem',\n fontWeight: 600,\n cursor: otp.isBusy ? 'not-allowed' : 'pointer',\n opacity: otp.isBusy ? 0.6 : 1,\n width: '100%',\n };\n\n const linkStyle: CSSProperties = {\n border: 'none',\n background: 'transparent',\n color: accentColor,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: '0.9rem',\n padding: 0,\n };\n\n const errorStyle: CSSProperties = {\n background: 'rgba(255,107,94,0.12)',\n color: '#c0392b',\n padding: '10px 12px',\n border: '1px solid rgba(255,107,94,0.25)',\n borderRadius: 8,\n fontSize: '0.85rem',\n };\n\n return (\n <div\n className={className}\n style={{\n maxWidth: 400,\n margin: '0 auto',\n padding: 32,\n border: '1px solid rgba(0,0,0,0.08)',\n borderRadius: 16,\n fontFamily: 'system-ui, -apple-system, sans-serif',\n ...style,\n }}\n >\n <div style={{ textAlign: 'center', marginBottom: 24 }}>\n {logo}\n <h1 style={{ fontSize: '1.4rem', fontWeight: 700, margin: '8px 0 4px' }}>{title}</h1>\n <p style={{ fontSize: '0.9rem', opacity: 0.7, margin: 0 }}>{subtitle}</p>\n </div>\n\n {otp.step === 'identify' ? (\n <form onSubmit={onIdentifySubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n {otp.error && <div style={errorStyle}>{otp.error}</div>}\n <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.85rem' }}>\n {identifierLabel}\n <input\n type=\"text\"\n value={otp.identifier}\n onChange={(e) => otp.setIdentifier(e.target.value)}\n placeholder={identifierPlaceholder}\n autoComplete=\"username\"\n autoFocus\n required\n style={inputStyle}\n />\n </label>\n <button type=\"submit\" disabled={otp.isBusy} style={buttonStyle}>\n {otp.isSending ? 'Sending code…' : 'Send code'}\n </button>\n </form>\n ) : (\n <form onSubmit={onVerifySubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n {otp.error && <div style={errorStyle}>{otp.error}</div>}\n <div\n style={{\n background: 'rgba(86,211,181,0.12)',\n padding: '10px 12px',\n borderRadius: 8,\n fontSize: '0.85rem',\n }}\n >\n {otp.channels.length > 0\n ? `Code sent via ${formatChannels(otp.channels)}.`\n : 'We sent you a code.'}\n {otp.expiresInSeconds > 0 &&\n ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`}\n </div>\n <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.85rem' }}>\n Verification code\n <input\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n value={otp.code}\n onChange={(e) => otp.setCode(e.target.value)}\n placeholder=\"Enter code\"\n autoFocus\n required\n style={inputStyle}\n />\n </label>\n <button type=\"submit\" disabled={otp.isBusy} style={buttonStyle}>\n {otp.isVerifying ? 'Verifying…' : 'Verify and sign in'}\n </button>\n <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n <button type=\"button\" onClick={otp.back} disabled={otp.isBusy} style={linkStyle}>\n Use a different contact\n </button>\n <button type=\"button\" onClick={() => void otp.resend()} disabled={otp.isBusy} style={linkStyle}>\n Resend code\n </button>\n </div>\n </form>\n )}\n </div>\n );\n}\n","import type { RequestOtpResult } from './state';\n\nexport interface FetchHandlerOptions {\n /**\n * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,\n * must return `{ requestId, channels?, expiresInSeconds? }`.\n * @default '/api/auth/request-otp'\n */\n requestUrl?: string;\n /**\n * Partner backend endpoint that verifies an OTP. Receives\n * `{ requestId, code }`, returns whatever your app needs (session, token…).\n * @default '/api/auth/verify-otp'\n */\n verifyUrl?: string;\n /** Extra headers (e.g. CSRF token) to send on both requests. */\n headers?: Record<string, string>;\n /** Forwarded to fetch — set to 'include' if you use cookie sessions. */\n credentials?: RequestCredentials;\n}\n\nasync function postJson<T>(\n url: string,\n body: unknown,\n options: FetchHandlerOptions,\n): Promise<T> {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },\n credentials: options.credentials,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n let message = `Request failed (${response.status})`;\n try {\n const data = (await response.json()) as { message?: string; error?: string };\n message = data.message || data.error || message;\n } catch {\n // non-JSON error body — keep the status-based message\n }\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.\n * Those endpoints call the Sneek API server-side with your secret key.\n */\nexport function createFetchHandlers<TResult = unknown>(\n options: FetchHandlerOptions = {},\n): {\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n} {\n const requestUrl = options.requestUrl ?? '/api/auth/request-otp';\n const verifyUrl = options.verifyUrl ?? '/api/auth/verify-otp';\n\n return {\n requestOtp: (identifier: string) =>\n postJson<RequestOtpResult>(requestUrl, { identifier }, options),\n verifyOtp: (input: { requestId: string; code: string }) =>\n postJson<TResult>(verifyUrl, input, options),\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,318 @@
1
+ // src/use-sneek-otp.ts
2
+ import { useCallback, useReducer } from "react";
3
+
4
+ // src/state.ts
5
+ var initialOtpState = {
6
+ step: "identify",
7
+ status: "idle",
8
+ identifier: "",
9
+ code: "",
10
+ requestId: "",
11
+ channels: [],
12
+ expiresInSeconds: 0,
13
+ error: null
14
+ };
15
+ var isBusy = (status) => status !== "idle";
16
+ function otpReducer(state, action) {
17
+ switch (action.type) {
18
+ case "set_identifier":
19
+ return { ...state, identifier: action.value, error: null };
20
+ case "set_code":
21
+ return { ...state, code: action.value, error: null };
22
+ case "request_start":
23
+ if (isBusy(state.status)) return state;
24
+ return { ...state, status: "sending", error: null };
25
+ case "request_success":
26
+ return {
27
+ ...state,
28
+ step: "verify",
29
+ status: "idle",
30
+ code: "",
31
+ requestId: action.result.requestId,
32
+ channels: action.result.channels ?? [],
33
+ expiresInSeconds: action.result.expiresInSeconds ?? 0,
34
+ error: null
35
+ };
36
+ case "request_error":
37
+ return { ...state, status: "idle", error: action.message };
38
+ case "verify_start":
39
+ if (isBusy(state.status)) return state;
40
+ return { ...state, status: "verifying", error: null };
41
+ case "verify_error":
42
+ return { ...state, status: "idle", error: action.message };
43
+ case "back_to_identify":
44
+ return {
45
+ ...state,
46
+ step: "identify",
47
+ status: "idle",
48
+ code: "",
49
+ requestId: "",
50
+ channels: [],
51
+ expiresInSeconds: 0,
52
+ error: null
53
+ };
54
+ case "reset":
55
+ return { ...initialOtpState };
56
+ default:
57
+ return state;
58
+ }
59
+ }
60
+ var CHANNEL_LABELS = {
61
+ sms: "SMS",
62
+ whatsapp: "WhatsApp",
63
+ email: "email"
64
+ };
65
+ function formatChannels(channels) {
66
+ return channels.map((c) => CHANNEL_LABELS[c] ?? c).join(", ");
67
+ }
68
+ function toMessage(error, fallback) {
69
+ if (error instanceof Error && error.message) return error.message;
70
+ if (typeof error === "string" && error) return error;
71
+ return fallback;
72
+ }
73
+
74
+ // src/use-sneek-otp.ts
75
+ var DEFAULT_REQUEST_ERROR = "Could not send the code. Please try again.";
76
+ var DEFAULT_VERIFY_ERROR = "That code was not correct. Please try again.";
77
+ function useSneekOtp(handlers) {
78
+ const [state, dispatch] = useReducer(otpReducer, initialOtpState);
79
+ const { requestOtp, verifyOtp, onSuccess, onError } = handlers;
80
+ const setIdentifier = useCallback((value) => {
81
+ dispatch({ type: "set_identifier", value });
82
+ }, []);
83
+ const setCode = useCallback((value) => {
84
+ dispatch({ type: "set_code", value });
85
+ }, []);
86
+ const runRequest = useCallback(
87
+ async (identifier) => {
88
+ const trimmed = identifier.trim();
89
+ if (!trimmed) {
90
+ dispatch({ type: "request_error", message: "Enter your email or mobile number." });
91
+ return;
92
+ }
93
+ dispatch({ type: "request_start" });
94
+ try {
95
+ const result = await requestOtp(trimmed);
96
+ dispatch({ type: "request_success", result });
97
+ } catch (error) {
98
+ dispatch({ type: "request_error", message: toMessage(error, DEFAULT_REQUEST_ERROR) });
99
+ onError?.(error);
100
+ }
101
+ },
102
+ [requestOtp, onError]
103
+ );
104
+ const sendOtp = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);
105
+ const resend = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);
106
+ const verify = useCallback(async () => {
107
+ const code = state.code.trim();
108
+ if (!code) {
109
+ dispatch({ type: "verify_error", message: "Enter the code you received." });
110
+ return;
111
+ }
112
+ dispatch({ type: "verify_start" });
113
+ try {
114
+ const result = await verifyOtp({ requestId: state.requestId, code });
115
+ onSuccess?.(result);
116
+ } catch (error) {
117
+ dispatch({ type: "verify_error", message: toMessage(error, DEFAULT_VERIFY_ERROR) });
118
+ onError?.(error);
119
+ }
120
+ }, [verifyOtp, state.code, state.requestId, onSuccess, onError]);
121
+ const back = useCallback(() => {
122
+ dispatch({ type: "back_to_identify" });
123
+ }, []);
124
+ return {
125
+ ...state,
126
+ setIdentifier,
127
+ setCode,
128
+ sendOtp,
129
+ verify,
130
+ back,
131
+ resend,
132
+ isSending: state.status === "sending",
133
+ isVerifying: state.status === "verifying",
134
+ isBusy: state.status !== "idle"
135
+ };
136
+ }
137
+
138
+ // src/component.tsx
139
+ import { jsx, jsxs } from "react/jsx-runtime";
140
+ function SneekOtpLogin(props) {
141
+ const {
142
+ title = "Sign in",
143
+ subtitle = "Passwordless login powered by Sneek",
144
+ identifierLabel = "Email or mobile number",
145
+ identifierPlaceholder = "you@example.com or +9198xxxxxxxx",
146
+ accentColor = "#56d3b5",
147
+ logo,
148
+ style,
149
+ className,
150
+ ...handlers
151
+ } = props;
152
+ const otp = useSneekOtp(handlers);
153
+ const onIdentifySubmit = (event) => {
154
+ event.preventDefault();
155
+ void otp.sendOtp();
156
+ };
157
+ const onVerifySubmit = (event) => {
158
+ event.preventDefault();
159
+ void otp.verify();
160
+ };
161
+ const inputStyle = {
162
+ padding: "12px 14px",
163
+ border: "1px solid rgba(0,0,0,0.15)",
164
+ borderRadius: 10,
165
+ fontSize: "0.95rem",
166
+ width: "100%",
167
+ boxSizing: "border-box",
168
+ outline: "none"
169
+ };
170
+ const buttonStyle = {
171
+ marginTop: 4,
172
+ padding: "12px 14px",
173
+ border: "none",
174
+ borderRadius: 10,
175
+ background: accentColor,
176
+ color: "#04231d",
177
+ fontSize: "1rem",
178
+ fontWeight: 600,
179
+ cursor: otp.isBusy ? "not-allowed" : "pointer",
180
+ opacity: otp.isBusy ? 0.6 : 1,
181
+ width: "100%"
182
+ };
183
+ const linkStyle = {
184
+ border: "none",
185
+ background: "transparent",
186
+ color: accentColor,
187
+ cursor: "pointer",
188
+ font: "inherit",
189
+ fontSize: "0.9rem",
190
+ padding: 0
191
+ };
192
+ const errorStyle = {
193
+ background: "rgba(255,107,94,0.12)",
194
+ color: "#c0392b",
195
+ padding: "10px 12px",
196
+ border: "1px solid rgba(255,107,94,0.25)",
197
+ borderRadius: 8,
198
+ fontSize: "0.85rem"
199
+ };
200
+ return /* @__PURE__ */ jsxs(
201
+ "div",
202
+ {
203
+ className,
204
+ style: {
205
+ maxWidth: 400,
206
+ margin: "0 auto",
207
+ padding: 32,
208
+ border: "1px solid rgba(0,0,0,0.08)",
209
+ borderRadius: 16,
210
+ fontFamily: "system-ui, -apple-system, sans-serif",
211
+ ...style
212
+ },
213
+ children: [
214
+ /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", marginBottom: 24 }, children: [
215
+ logo,
216
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.4rem", fontWeight: 700, margin: "8px 0 4px" }, children: title }),
217
+ /* @__PURE__ */ jsx("p", { style: { fontSize: "0.9rem", opacity: 0.7, margin: 0 }, children: subtitle })
218
+ ] }),
219
+ otp.step === "identify" ? /* @__PURE__ */ jsxs("form", { onSubmit: onIdentifySubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
220
+ otp.error && /* @__PURE__ */ jsx("div", { style: errorStyle, children: otp.error }),
221
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: "0.85rem" }, children: [
222
+ identifierLabel,
223
+ /* @__PURE__ */ jsx(
224
+ "input",
225
+ {
226
+ type: "text",
227
+ value: otp.identifier,
228
+ onChange: (e) => otp.setIdentifier(e.target.value),
229
+ placeholder: identifierPlaceholder,
230
+ autoComplete: "username",
231
+ autoFocus: true,
232
+ required: true,
233
+ style: inputStyle
234
+ }
235
+ )
236
+ ] }),
237
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: otp.isBusy, style: buttonStyle, children: otp.isSending ? "Sending code\u2026" : "Send code" })
238
+ ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: onVerifySubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
239
+ otp.error && /* @__PURE__ */ jsx("div", { style: errorStyle, children: otp.error }),
240
+ /* @__PURE__ */ jsxs(
241
+ "div",
242
+ {
243
+ style: {
244
+ background: "rgba(86,211,181,0.12)",
245
+ padding: "10px 12px",
246
+ borderRadius: 8,
247
+ fontSize: "0.85rem"
248
+ },
249
+ children: [
250
+ otp.channels.length > 0 ? `Code sent via ${formatChannels(otp.channels)}.` : "We sent you a code.",
251
+ otp.expiresInSeconds > 0 && ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`
252
+ ]
253
+ }
254
+ ),
255
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: "0.85rem" }, children: [
256
+ "Verification code",
257
+ /* @__PURE__ */ jsx(
258
+ "input",
259
+ {
260
+ type: "text",
261
+ inputMode: "numeric",
262
+ autoComplete: "one-time-code",
263
+ value: otp.code,
264
+ onChange: (e) => otp.setCode(e.target.value),
265
+ placeholder: "Enter code",
266
+ autoFocus: true,
267
+ required: true,
268
+ style: inputStyle
269
+ }
270
+ )
271
+ ] }),
272
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: otp.isBusy, style: buttonStyle, children: otp.isVerifying ? "Verifying\u2026" : "Verify and sign in" }),
273
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
274
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: otp.back, disabled: otp.isBusy, style: linkStyle, children: "Use a different contact" }),
275
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void otp.resend(), disabled: otp.isBusy, style: linkStyle, children: "Resend code" })
276
+ ] })
277
+ ] })
278
+ ]
279
+ }
280
+ );
281
+ }
282
+
283
+ // src/fetch-handlers.ts
284
+ async function postJson(url, body, options) {
285
+ const response = await fetch(url, {
286
+ method: "POST",
287
+ headers: { "Content-Type": "application/json", ...options.headers ?? {} },
288
+ credentials: options.credentials,
289
+ body: JSON.stringify(body)
290
+ });
291
+ if (!response.ok) {
292
+ let message = `Request failed (${response.status})`;
293
+ try {
294
+ const data = await response.json();
295
+ message = data.message || data.error || message;
296
+ } catch {
297
+ }
298
+ throw new Error(message);
299
+ }
300
+ return await response.json();
301
+ }
302
+ function createFetchHandlers(options = {}) {
303
+ const requestUrl = options.requestUrl ?? "/api/auth/request-otp";
304
+ const verifyUrl = options.verifyUrl ?? "/api/auth/verify-otp";
305
+ return {
306
+ requestOtp: (identifier) => postJson(requestUrl, { identifier }, options),
307
+ verifyOtp: (input) => postJson(verifyUrl, input, options)
308
+ };
309
+ }
310
+ export {
311
+ SneekOtpLogin,
312
+ createFetchHandlers,
313
+ formatChannels,
314
+ initialOtpState,
315
+ otpReducer,
316
+ useSneekOtp
317
+ };
318
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/use-sneek-otp.ts","../src/state.ts","../src/component.tsx","../src/fetch-handlers.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\nimport {\n initialOtpState,\n otpReducer,\n toMessage,\n type RequestOtpResult,\n type SneekOtpState,\n} from './state';\n\n/**\n * Partner-supplied transport. These call the *partner's own backend*, which in\n * turn talks to the Sneek API with the secret server-side key. The browser\n * never sees a Sneek API key — that is the whole security model of this package.\n */\nexport interface SneekOtpHandlers<TResult = unknown> {\n /** Ask the partner backend to send an OTP to `identifier`. */\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n /** Verify the code with the partner backend; resolve with the auth result. */\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n /** Called after a successful verification with the partner's result. */\n onSuccess?: (result: TResult) => void;\n /** Called on any error (request or verify). */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseSneekOtp extends SneekOtpState {\n setIdentifier: (value: string) => void;\n setCode: (value: string) => void;\n /** Submit the identify step — triggers `requestOtp`. */\n sendOtp: () => Promise<void>;\n /** Submit the verify step — triggers `verifyOtp`. */\n verify: () => Promise<void>;\n /** Go back to the identify screen (e.g. \"use a different email\"). */\n back: () => void;\n /** Re-send the OTP to the same identifier. */\n resend: () => Promise<void>;\n /** Convenience booleans. */\n isSending: boolean;\n isVerifying: boolean;\n isBusy: boolean;\n}\n\nconst DEFAULT_REQUEST_ERROR = 'Could not send the code. Please try again.';\nconst DEFAULT_VERIFY_ERROR = 'That code was not correct. Please try again.';\n\n/**\n * Headless hook implementing the passwordless OTP login flow. Bring your own\n * markup, or use the {@link SneekOtpLogin} component for a styled default.\n */\nexport function useSneekOtp<TResult = unknown>(\n handlers: SneekOtpHandlers<TResult>,\n): UseSneekOtp {\n const [state, dispatch] = useReducer(otpReducer, initialOtpState);\n\n const { requestOtp, verifyOtp, onSuccess, onError } = handlers;\n\n const setIdentifier = useCallback((value: string) => {\n dispatch({ type: 'set_identifier', value });\n }, []);\n\n const setCode = useCallback((value: string) => {\n dispatch({ type: 'set_code', value });\n }, []);\n\n const runRequest = useCallback(\n async (identifier: string) => {\n const trimmed = identifier.trim();\n if (!trimmed) {\n dispatch({ type: 'request_error', message: 'Enter your email or mobile number.' });\n return;\n }\n dispatch({ type: 'request_start' });\n try {\n const result = await requestOtp(trimmed);\n dispatch({ type: 'request_success', result });\n } catch (error) {\n dispatch({ type: 'request_error', message: toMessage(error, DEFAULT_REQUEST_ERROR) });\n onError?.(error);\n }\n },\n [requestOtp, onError],\n );\n\n const sendOtp = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);\n\n const resend = useCallback(() => runRequest(state.identifier), [runRequest, state.identifier]);\n\n const verify = useCallback(async () => {\n const code = state.code.trim();\n if (!code) {\n dispatch({ type: 'verify_error', message: 'Enter the code you received.' });\n return;\n }\n dispatch({ type: 'verify_start' });\n try {\n const result = await verifyOtp({ requestId: state.requestId, code });\n onSuccess?.(result);\n } catch (error) {\n dispatch({ type: 'verify_error', message: toMessage(error, DEFAULT_VERIFY_ERROR) });\n onError?.(error);\n }\n }, [verifyOtp, state.code, state.requestId, onSuccess, onError]);\n\n const back = useCallback(() => {\n dispatch({ type: 'back_to_identify' });\n }, []);\n\n return {\n ...state,\n setIdentifier,\n setCode,\n sendOtp,\n verify,\n back,\n resend,\n isSending: state.status === 'sending',\n isVerifying: state.status === 'verifying',\n isBusy: state.status !== 'idle',\n };\n}\n","/**\n * Framework-agnostic state machine for the Sneek passwordless OTP flow.\n *\n * Kept free of React so it can be unit-tested in isolation and reused by other\n * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a\n * thin wrapper around this reducer.\n */\n\nexport type SneekChannel = 'sms' | 'whatsapp' | 'email';\n\n/** Which screen of the two-step flow is showing. */\nexport type SneekOtpStep = 'identify' | 'verify';\n\n/** Async lifecycle for the in-flight request. */\nexport type SneekOtpStatus = 'idle' | 'sending' | 'verifying';\n\nexport interface SneekOtpState {\n step: SneekOtpStep;\n status: SneekOtpStatus;\n /** The email / mobile / username the user typed. */\n identifier: string;\n /** The OTP code the user typed on the verify screen. */\n code: string;\n /** Opaque id returned by the partner backend, replayed on verify. */\n requestId: string;\n /** Channels the OTP was actually delivered over. */\n channels: SneekChannel[];\n /** Seconds until the OTP expires (from the request response). */\n expiresInSeconds: number;\n /** User-facing error message, or null. */\n error: string | null;\n}\n\nexport const initialOtpState: SneekOtpState = {\n step: 'identify',\n status: 'idle',\n identifier: '',\n code: '',\n requestId: '',\n channels: [],\n expiresInSeconds: 0,\n error: null,\n};\n\nexport interface RequestOtpResult {\n requestId: string;\n channels?: SneekChannel[];\n expiresInSeconds?: number;\n}\n\nexport type SneekOtpAction =\n | { type: 'set_identifier'; value: string }\n | { type: 'set_code'; value: string }\n | { type: 'request_start' }\n | { type: 'request_success'; result: RequestOtpResult }\n | { type: 'request_error'; message: string }\n | { type: 'verify_start' }\n | { type: 'verify_error'; message: string }\n | { type: 'reset' }\n | { type: 'back_to_identify' };\n\nconst isBusy = (status: SneekOtpStatus): boolean => status !== 'idle';\n\nexport function otpReducer(\n state: SneekOtpState,\n action: SneekOtpAction,\n): SneekOtpState {\n switch (action.type) {\n case 'set_identifier':\n return { ...state, identifier: action.value, error: null };\n\n case 'set_code':\n return { ...state, code: action.value, error: null };\n\n case 'request_start':\n // Guard against double submits while a request is already in flight.\n if (isBusy(state.status)) return state;\n return { ...state, status: 'sending', error: null };\n\n case 'request_success':\n return {\n ...state,\n step: 'verify',\n status: 'idle',\n code: '',\n requestId: action.result.requestId,\n channels: action.result.channels ?? [],\n expiresInSeconds: action.result.expiresInSeconds ?? 0,\n error: null,\n };\n\n case 'request_error':\n return { ...state, status: 'idle', error: action.message };\n\n case 'verify_start':\n if (isBusy(state.status)) return state;\n return { ...state, status: 'verifying', error: null };\n\n case 'verify_error':\n return { ...state, status: 'idle', error: action.message };\n\n case 'back_to_identify':\n return {\n ...state,\n step: 'identify',\n status: 'idle',\n code: '',\n requestId: '',\n channels: [],\n expiresInSeconds: 0,\n error: null,\n };\n\n case 'reset':\n return { ...initialOtpState };\n\n default:\n return state;\n }\n}\n\nconst CHANNEL_LABELS: Record<SneekChannel, string> = {\n sms: 'SMS',\n whatsapp: 'WhatsApp',\n email: 'email',\n};\n\n/** \"SMS, WhatsApp\" — human label for the channels an OTP was sent over. */\nexport function formatChannels(channels: SneekChannel[]): string {\n return channels.map((c) => CHANNEL_LABELS[c] ?? c).join(', ');\n}\n\n/** Normalize any thrown value into a user-facing message. */\nexport function toMessage(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n return fallback;\n}\n","import { type CSSProperties, type FormEvent, type ReactNode } from 'react';\nimport { formatChannels } from './state';\nimport { useSneekOtp, type SneekOtpHandlers } from './use-sneek-otp';\n\nexport interface SneekOtpLoginProps<TResult = unknown>\n extends SneekOtpHandlers<TResult> {\n /** Heading shown above the form. @default 'Sign in' */\n title?: string;\n /** Sub-heading. @default 'Passwordless login powered by Sneek' */\n subtitle?: ReactNode;\n /** Label for the identifier input. */\n identifierLabel?: string;\n /** Placeholder for the identifier input. */\n identifierPlaceholder?: string;\n /** Brand colour for the primary button. @default '#56d3b5' */\n accentColor?: string;\n /** Optional logo rendered above the title. */\n logo?: ReactNode;\n /** Override the outer container style. */\n style?: CSSProperties;\n /** Extra className on the outer container. */\n className?: string;\n}\n\n/**\n * Drop-in, dependency-free passwordless OTP login card. The component never\n * receives a Sneek API key; it calls the partner-supplied handlers, which talk\n * to the partner backend. Use {@link createFetchHandlers} for the common case.\n */\nexport function SneekOtpLogin<TResult = unknown>(\n props: SneekOtpLoginProps<TResult>,\n): ReactNode {\n const {\n title = 'Sign in',\n subtitle = 'Passwordless login powered by Sneek',\n identifierLabel = 'Email or mobile number',\n identifierPlaceholder = 'you@example.com or +9198xxxxxxxx',\n accentColor = '#56d3b5',\n logo,\n style,\n className,\n ...handlers\n } = props;\n\n const otp = useSneekOtp<TResult>(handlers);\n\n const onIdentifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.sendOtp();\n };\n\n const onVerifySubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n void otp.verify();\n };\n\n const inputStyle: CSSProperties = {\n padding: '12px 14px',\n border: '1px solid rgba(0,0,0,0.15)',\n borderRadius: 10,\n fontSize: '0.95rem',\n width: '100%',\n boxSizing: 'border-box',\n outline: 'none',\n };\n\n const buttonStyle: CSSProperties = {\n marginTop: 4,\n padding: '12px 14px',\n border: 'none',\n borderRadius: 10,\n background: accentColor,\n color: '#04231d',\n fontSize: '1rem',\n fontWeight: 600,\n cursor: otp.isBusy ? 'not-allowed' : 'pointer',\n opacity: otp.isBusy ? 0.6 : 1,\n width: '100%',\n };\n\n const linkStyle: CSSProperties = {\n border: 'none',\n background: 'transparent',\n color: accentColor,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: '0.9rem',\n padding: 0,\n };\n\n const errorStyle: CSSProperties = {\n background: 'rgba(255,107,94,0.12)',\n color: '#c0392b',\n padding: '10px 12px',\n border: '1px solid rgba(255,107,94,0.25)',\n borderRadius: 8,\n fontSize: '0.85rem',\n };\n\n return (\n <div\n className={className}\n style={{\n maxWidth: 400,\n margin: '0 auto',\n padding: 32,\n border: '1px solid rgba(0,0,0,0.08)',\n borderRadius: 16,\n fontFamily: 'system-ui, -apple-system, sans-serif',\n ...style,\n }}\n >\n <div style={{ textAlign: 'center', marginBottom: 24 }}>\n {logo}\n <h1 style={{ fontSize: '1.4rem', fontWeight: 700, margin: '8px 0 4px' }}>{title}</h1>\n <p style={{ fontSize: '0.9rem', opacity: 0.7, margin: 0 }}>{subtitle}</p>\n </div>\n\n {otp.step === 'identify' ? (\n <form onSubmit={onIdentifySubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n {otp.error && <div style={errorStyle}>{otp.error}</div>}\n <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.85rem' }}>\n {identifierLabel}\n <input\n type=\"text\"\n value={otp.identifier}\n onChange={(e) => otp.setIdentifier(e.target.value)}\n placeholder={identifierPlaceholder}\n autoComplete=\"username\"\n autoFocus\n required\n style={inputStyle}\n />\n </label>\n <button type=\"submit\" disabled={otp.isBusy} style={buttonStyle}>\n {otp.isSending ? 'Sending code…' : 'Send code'}\n </button>\n </form>\n ) : (\n <form onSubmit={onVerifySubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n {otp.error && <div style={errorStyle}>{otp.error}</div>}\n <div\n style={{\n background: 'rgba(86,211,181,0.12)',\n padding: '10px 12px',\n borderRadius: 8,\n fontSize: '0.85rem',\n }}\n >\n {otp.channels.length > 0\n ? `Code sent via ${formatChannels(otp.channels)}.`\n : 'We sent you a code.'}\n {otp.expiresInSeconds > 0 &&\n ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`}\n </div>\n <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.85rem' }}>\n Verification code\n <input\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n value={otp.code}\n onChange={(e) => otp.setCode(e.target.value)}\n placeholder=\"Enter code\"\n autoFocus\n required\n style={inputStyle}\n />\n </label>\n <button type=\"submit\" disabled={otp.isBusy} style={buttonStyle}>\n {otp.isVerifying ? 'Verifying…' : 'Verify and sign in'}\n </button>\n <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n <button type=\"button\" onClick={otp.back} disabled={otp.isBusy} style={linkStyle}>\n Use a different contact\n </button>\n <button type=\"button\" onClick={() => void otp.resend()} disabled={otp.isBusy} style={linkStyle}>\n Resend code\n </button>\n </div>\n </form>\n )}\n </div>\n );\n}\n","import type { RequestOtpResult } from './state';\n\nexport interface FetchHandlerOptions {\n /**\n * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,\n * must return `{ requestId, channels?, expiresInSeconds? }`.\n * @default '/api/auth/request-otp'\n */\n requestUrl?: string;\n /**\n * Partner backend endpoint that verifies an OTP. Receives\n * `{ requestId, code }`, returns whatever your app needs (session, token…).\n * @default '/api/auth/verify-otp'\n */\n verifyUrl?: string;\n /** Extra headers (e.g. CSRF token) to send on both requests. */\n headers?: Record<string, string>;\n /** Forwarded to fetch — set to 'include' if you use cookie sessions. */\n credentials?: RequestCredentials;\n}\n\nasync function postJson<T>(\n url: string,\n body: unknown,\n options: FetchHandlerOptions,\n): Promise<T> {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },\n credentials: options.credentials,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n let message = `Request failed (${response.status})`;\n try {\n const data = (await response.json()) as { message?: string; error?: string };\n message = data.message || data.error || message;\n } catch {\n // non-JSON error body — keep the status-based message\n }\n throw new Error(message);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.\n * Those endpoints call the Sneek API server-side with your secret key.\n */\nexport function createFetchHandlers<TResult = unknown>(\n options: FetchHandlerOptions = {},\n): {\n requestOtp: (identifier: string) => Promise<RequestOtpResult>;\n verifyOtp: (input: { requestId: string; code: string }) => Promise<TResult>;\n} {\n const requestUrl = options.requestUrl ?? '/api/auth/request-otp';\n const verifyUrl = options.verifyUrl ?? '/api/auth/verify-otp';\n\n return {\n requestOtp: (identifier: string) =>\n postJson<RequestOtpResult>(requestUrl, { identifier }, options),\n verifyOtp: (input: { requestId: string; code: string }) =>\n postJson<TResult>(verifyUrl, input, options),\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "sneekui",
3
+ "version": "0.1.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Drop-in passwordless OTP login UI for React, powered by Sneek",
8
+ "homepage": "https://sneek.in/docs",
9
+ "bugs": {
10
+ "url": "https://sneek.in/docs"
11
+ },
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.mjs",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.mjs",
19
+ "require": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "type-check": "tsc --noEmit",
29
+ "test": "node --test --import tsx test/*.test.ts"
30
+ },
31
+ "keywords": [
32
+ "sneek",
33
+ "otp",
34
+ "passwordless",
35
+ "react",
36
+ "authentication",
37
+ "login",
38
+ "2fa"
39
+ ],
40
+ "author": "Abblor Tech Pvt Ltd <abblorltd@gmail.com> (https://sneek.in)",
41
+ "license": "UNLICENSED",
42
+ "peerDependencies": {
43
+ "react": ">=17.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.11.0",
47
+ "@types/react": "^18.2.0",
48
+ "react": "^18.2.0",
49
+ "tsup": "^8.0.0",
50
+ "tsx": "^4.7.0",
51
+ "typescript": "^5.3.3"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ }