@sneekin/ui
v0.1.2
Published
Drop-in passwordless OTP login UI for React, powered by Sneek
Readme
@sneekin/ui
Drop-in passwordless OTP login UI for React, powered by Sneek.
One component renders the whole two-step flow — enter your email/mobile → enter the code → signed in — over SMS, WhatsApp, or email. No passwords.
The security model (read this first)
@sneekin/ui runs in the browser, so it never holds a Sneek API key. Instead it
calls your own backend, and your backend calls Sneek server-side with your secret
key (using @sneekin/sdk).
Browser (@sneekin/ui)
│ POST /api/auth/request-otp { identifier }
▼
Your backend ──(@sneekin/sdk, secret key)──► Sneek API ──► SMS / WhatsApp / Email
│ POST /api/auth/verify-otp { requestId, code }
▼
Your backend issues your session / tokenPutting a Sneek key in front-end code would expose it to every visitor. This package is designed so that can't happen.
Install
npm install @sneekin/uireact >= 17 is a peer dependency.
Quick start
import { SneekOtpLogin, createFetchHandlers } from '@sneekin/ui';
export function LoginPage() {
return (
<SneekOtpLogin
title="Sign in to ConfHub"
{...createFetchHandlers({
requestUrl: '/api/auth/request-otp',
verifyUrl: '/api/auth/verify-otp',
})}
onSuccess={() => {
window.location.href = '/dashboard';
}}
/>
);
}That's the whole front end. createFetchHandlers POSTs to your endpoints; defaults are
/api/auth/request-otp and /api/auth/verify-otp.
Your backend endpoints
// POST /api/auth/request-otp body: { identifier }
import { Sneek } from '@sneekin/sdk';
const sneek = new Sneek({ apiKey: process.env.SNEEK_API_KEY! });
const otp = await sneek.sendOTP({ to: identifier, channel: 'auto' });
return { requestId: otp.id, channels: [otp.channel], expiresInSeconds: 300 };
// POST /api/auth/verify-otp body: { requestId, code }
// verify against your store, then return your own session/tokenThe exact verify call depends on how you persist the OTP request. Sneek returns a message id on send; store it keyed by
requestIdand check the code your way, or use Sneek's verify endpoint if enabled for your app.
Headless usage
Want your own markup? Use the hook:
import { useSneekOtp, createFetchHandlers } from '@sneekin/ui';
function MyLogin() {
const otp = useSneekOtp({
...createFetchHandlers(),
onSuccess: (user) => console.log('signed in', user),
});
if (otp.step === 'identify') {
return (
<form onSubmit={(e) => { e.preventDefault(); otp.sendOtp(); }}>
<input value={otp.identifier} onChange={(e) => otp.setIdentifier(e.target.value)} />
<button disabled={otp.isBusy}>{otp.isSending ? 'Sending…' : 'Send code'}</button>
{otp.error && <p>{otp.error}</p>}
</form>
);
}
return (
<form onSubmit={(e) => { e.preventDefault(); otp.verify(); }}>
<input value={otp.code} onChange={(e) => otp.setCode(e.target.value)} />
<button disabled={otp.isBusy}>{otp.isVerifying ? 'Verifying…' : 'Verify'}</button>
<button type="button" onClick={otp.back}>Back</button>
<button type="button" onClick={otp.resend}>Resend</button>
</form>
);
}Custom transport
Don't want fetch? Provide requestOtp / verifyOtp directly:
useSneekOtp({
requestOtp: async (identifier) => {
const r = await myClient.sendOtp(identifier);
return { requestId: r.id, channels: r.channels, expiresInSeconds: r.ttl };
},
verifyOtp: async ({ requestId, code }) => myClient.verify(requestId, code),
onSuccess: (result) => {/* … */},
});API
<SneekOtpLogin />
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| requestOtp | (id) => Promise<RequestOtpResult> | — | Required |
| verifyOtp | ({requestId, code}) => Promise<T> | — | Required |
| onSuccess | (result: T) => void | — | After verify |
| onError | (error: unknown) => void | — | Any failure |
| title | string | 'Sign in' | |
| subtitle | ReactNode | Sneek tagline | |
| accentColor | string | '#56d3b5' | Button colour |
| logo | ReactNode | — | Above the title |
| style / className | — | — | Container overrides |
createFetchHandlers(options) returns { requestOtp, verifyOtp }, so spread it into the
component or hook.
useSneekOtp(handlers)
Returns the reducer state plus setIdentifier, setCode, sendOtp, verify, back,
resend, and the booleans isSending, isVerifying, isBusy.
License
UNLICENSED — © Sneek.
