@horus-wallet/sdk-react 0.1.0-beta.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 ADDED
@@ -0,0 +1,398 @@
1
+ # @horus-wallet/sdk-react
2
+
3
+ React provider + hooks + drop-in components for the Horus embedded-wallet platform. Sign-in, wallets, signing, and on-chain transfers in **15 minutes**.
4
+
5
+ ```tsx
6
+ import { HorusProvider, useHorusAuth, useWallets, HorusLoginButton } from '@horus-wallet/sdk-react';
7
+
8
+ function App() {
9
+ return (
10
+ <HorusProvider appId="app_acme123">
11
+ <Page />
12
+ </HorusProvider>
13
+ );
14
+ }
15
+
16
+ function Page() {
17
+ const { authenticated, user, logout } = useHorusAuth();
18
+ const { wallets } = useWallets();
19
+
20
+ if (!authenticated) {
21
+ return <HorusLoginButton flow="google">Continue with Google</HorusLoginButton>;
22
+ }
23
+ return (
24
+ <>
25
+ <p>Hi {user?.email}</p>
26
+ <p>EVM wallet: {wallets.find(w => w.network === 'EVM')?.publicKey}</p>
27
+ <button onClick={logout}>Sign out</button>
28
+ </>
29
+ );
30
+ }
31
+ ```
32
+
33
+ That's it. Sign-in, token persistence, auto-refresh, and wallet fetching all happen inside the provider.
34
+
35
+ ## What you get
36
+
37
+ - **Drop-in React DX** — one provider, a handful of hooks, no wallet-management code in your app
38
+ - **Built-in auth** — email + password, magic-link email, Google / Apple / phone via popup
39
+ - **Multi-chain wallets** — EVM, Bitcoin, ICP, Casper, Aeternity — same hook, pick `network`
40
+ - **Signing + transfers** — `signMessage`, native + ERC-20 transfers, all behind a hook
41
+ - **Token persistence** — sessions survive page reloads; cross-tab sync; auto-refresh near expiry
42
+ - **Drop-in components** — `<HorusLoginButton>` for one-click signin if you don't want to build the UI yourself
43
+ - **White-label friendly** — partner branding (logo, colors, terms links) flows through to the sign-in popup
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ npm install @horus-wallet/sdk-react
49
+ ```
50
+
51
+ Peer dependency: React 17+. Works in React 18, Next.js (App Router and Pages), Vite, Remix.
52
+
53
+ ## Quick setup (3 steps)
54
+
55
+ ### 1. Wrap your app in `<HorusProvider>`
56
+
57
+ ```tsx
58
+ import { HorusProvider } from '@horus-wallet/sdk-react';
59
+
60
+ export function App({ children }) {
61
+ return (
62
+ <HorusProvider appId="app_acme123">
63
+ {children}
64
+ </HorusProvider>
65
+ );
66
+ }
67
+ ```
68
+
69
+ You get `appId` from Horus during onboarding.
70
+
71
+ ### 2. Mount a sign-in button
72
+
73
+ ```tsx
74
+ import { HorusLoginButton } from '@horus-wallet/sdk-react';
75
+
76
+ <HorusLoginButton flow="google">Continue with Google</HorusLoginButton>
77
+ <HorusLoginButton flow="email_link">Email me a link</HorusLoginButton>
78
+ <HorusLoginButton flow="phone">Continue with phone</HorusLoginButton>
79
+ ```
80
+
81
+ Or build your own form and call `useHorusAuth()` methods directly (see hooks below).
82
+
83
+ ### 3. Add a server-side proxy
84
+
85
+ The React SDK calls **your backend** so your secret API key never reaches the browser:
86
+
87
+ ```
88
+ React app ──► Your backend (~50 lines) ──► api.horuswallet.com
89
+ ```
90
+
91
+ The proxy is a thin pass-through. Reference implementations for Express and Next.js are below.
92
+
93
+ ## Provider config
94
+
95
+ ```tsx
96
+ <HorusProvider
97
+ appId="app_acme123" // required — server-assigned during onboarding
98
+ apiBase="/api/horus" // where YOUR backend exposes the proxy. Default: /api/horus
99
+
100
+ branding={{ // optional — passed to the sign-in popup
101
+ logoUrl: 'https://acme.com/logo.png',
102
+ brandName: 'Acme',
103
+ primaryColor: '#FF5733',
104
+ showPoweredByHorus: false, // requires the white-label tier
105
+ }}
106
+
107
+ tokenStorage="localStorage" // 'localStorage' | 'sessionStorage' | 'memory'
108
+ autoRefresh={true} // refresh tokens before expiry; default true
109
+ >
110
+ <App />
111
+ </HorusProvider>
112
+ ```
113
+
114
+ ## Hooks
115
+
116
+ ### `useHorusAuth()` — sign-in, sign-up, sign-out
117
+
118
+ ```tsx
119
+ const {
120
+ ready, // true once the provider has hydrated from storage
121
+ authenticated, // true if user is signed in
122
+ user, // { uid, email, displayName, ... } or undefined
123
+
124
+ // Headless flows — your form, your UI
125
+ loginWithEmail, // ({ email, password }) => Promise<User>
126
+ signUpWithEmail, // ({ email, password }) => Promise<User>
127
+ sendEmailLink, // ({ email, continueUrl }) => Promise<void>
128
+ verifyEmailLink, // ({ email, oobCode }) => Promise<User>
129
+
130
+ // Popup flows — Horus-hosted UI for OAuth providers
131
+ loginWithGoogle, // () => Promise<User>
132
+ loginWithEmailLink, // popup variant of email-link
133
+ loginWithPhone, // ({ phone? }) => Promise<User>
134
+
135
+ logout, // clears tokens
136
+ refresh, // manual token refresh
137
+ } = useHorusAuth();
138
+ ```
139
+
140
+ ### `useUser()` — read the current user
141
+
142
+ ```tsx
143
+ const user = useUser();
144
+ // User | undefined — pure read, no side-effects
145
+ ```
146
+
147
+ ### `useWallets()` — the user's wallets
148
+
149
+ ```tsx
150
+ const { wallets, ready, refresh, error } = useWallets();
151
+ // wallets: [{ network: 'EVM', publicKey: '0x…' }, { network: 'BITCOIN', publicKey: 'bc1…' }, …]
152
+ ```
153
+
154
+ ### `useSignMessage()` — sign arbitrary messages
155
+
156
+ ```tsx
157
+ const { signMessage, pending, error } = useSignMessage();
158
+
159
+ const sig = await signMessage({
160
+ network: 'BASE',
161
+ networkType: 'MAINNET',
162
+ message: 'Sign in to Acme — nonce abc123',
163
+ });
164
+ ```
165
+
166
+ ### `useTransfer()` — native + token transfers
167
+
168
+ ```tsx
169
+ const { native, token, pending, error } = useTransfer();
170
+
171
+ // Native (ETH / MATIC / BNB / …)
172
+ const { txHash } = await native({
173
+ network: 'BASE',
174
+ networkType: 'MAINNET',
175
+ recipients: ['0x742d35Cc…'],
176
+ amount: '1000000000000000', // wei (string for values > 2^53)
177
+ });
178
+
179
+ // ERC-20 / chain-equivalent token
180
+ const { txHash } = await token({
181
+ network: 'BASE',
182
+ networkType: 'MAINNET',
183
+ tokenAddress: '0xA0b86…', // USDC
184
+ recipient: '0x742d35Cc…',
185
+ amount: '5000000', // smallest unit (USDC has 6 decimals)
186
+ });
187
+ ```
188
+
189
+ ## Drop-in components
190
+
191
+ ### `<HorusLoginButton>`
192
+
193
+ ```tsx
194
+ <HorusLoginButton flow="google">Continue with Google</HorusLoginButton>
195
+ <HorusLoginButton flow="email_link">Email me a link</HorusLoginButton>
196
+ <HorusLoginButton flow="phone">Continue with phone</HorusLoginButton>
197
+ ```
198
+
199
+ For total UI control, skip the component and call `useHorusAuth()` methods from your own form.
200
+
201
+ ## Backend setup (the proxy)
202
+
203
+ A thin pass-through that forwards SDK calls to Horus, holding your secret key server-side. Roughly **50 lines**.
204
+
205
+ ### Express / Connect
206
+
207
+ ```ts
208
+ // server/horus-proxy.ts
209
+ import express from 'express';
210
+ import { HorusClient } from '@horus-wallet/sdk';
211
+
212
+ export function horusProxy() {
213
+ const router = express.Router();
214
+
215
+ // Per-request client — picks up the user's auth token from the SDK header.
216
+ const clientFor = (req: express.Request) =>
217
+ new HorusClient({
218
+ baseUrl: process.env.HORUS_BASE_URL!,
219
+ apiKey: process.env.HORUS_API_KEY!, // hk_sk_*
220
+ getIdToken: async () => req.header('x-horus-id-token') ?? '',
221
+ });
222
+
223
+ // Auth — unauthenticated; mints tokens on success
224
+ router.post('/auth/email/signup', async (req, res, next) => {
225
+ try { res.json(await clientFor(req).auth.signUpWithEmail(req.body)); }
226
+ catch (err) { next(err); }
227
+ });
228
+ router.post('/auth/email/signin', async (req, res, next) => {
229
+ try { res.json(await clientFor(req).auth.signInWithEmail(req.body)); }
230
+ catch (err) { next(err); }
231
+ });
232
+ router.post('/auth/email-link/send', async (req, res, next) => {
233
+ try { res.json(await clientFor(req).auth.sendEmailLink(req.body)); }
234
+ catch (err) { next(err); }
235
+ });
236
+ router.post('/auth/email-link/verify', async (req, res, next) => {
237
+ try { res.json(await clientFor(req).auth.verifyEmailLink(req.body)); }
238
+ catch (err) { next(err); }
239
+ });
240
+ router.post('/auth/oauth', async (req, res, next) => {
241
+ try { res.json(await clientFor(req).auth.signInWithOAuth(req.body)); }
242
+ catch (err) { next(err); }
243
+ });
244
+ router.post('/auth/refresh', async (req, res, next) => {
245
+ try { res.json(await clientFor(req).auth.refresh(req.body.refreshToken)); }
246
+ catch (err) { next(err); }
247
+ });
248
+
249
+ // Wallet ops — authenticated; SDK forwards the user's token
250
+ router.get('/wallets', async (req, res, next) => {
251
+ try { res.json(await clientFor(req).wallets.get()); }
252
+ catch (err) { next(err); }
253
+ });
254
+ router.post('/sign-message', async (req, res, next) => {
255
+ try { res.json(await clientFor(req).wallets.signMessage(req.body)); }
256
+ catch (err) { next(err); }
257
+ });
258
+ router.post('/transfer/native', async (req, res, next) => {
259
+ try { res.json(await clientFor(req).transfers.native(req.body)); }
260
+ catch (err) { next(err); }
261
+ });
262
+ router.post('/transfer/token', async (req, res, next) => {
263
+ try { res.json(await clientFor(req).transfers.token(req.body)); }
264
+ catch (err) { next(err); }
265
+ });
266
+
267
+ return router;
268
+ }
269
+ ```
270
+
271
+ Mount it:
272
+
273
+ ```ts
274
+ import express from 'express';
275
+ import { horusProxy } from './horus-proxy';
276
+
277
+ const app = express();
278
+ app.use(express.json());
279
+ app.use('/api/horus', horusProxy());
280
+ app.listen(3001);
281
+ ```
282
+
283
+ That's the whole backend integration.
284
+
285
+ ### Next.js (App Router)
286
+
287
+ ```tsx
288
+ // app/providers.tsx
289
+ 'use client';
290
+ import { HorusProvider } from '@horus-wallet/sdk-react';
291
+
292
+ export function Providers({ children }: { children: React.ReactNode }) {
293
+ return (
294
+ <HorusProvider appId={process.env.NEXT_PUBLIC_HORUS_APP_ID!}>
295
+ {children}
296
+ </HorusProvider>
297
+ );
298
+ }
299
+
300
+ // app/layout.tsx
301
+ import { Providers } from './providers';
302
+
303
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
304
+ return <html><body><Providers>{children}</Providers></body></html>;
305
+ }
306
+ ```
307
+
308
+ ```ts
309
+ // app/api/horus/[...path]/route.ts
310
+ import { HorusClient } from '@horus-wallet/sdk';
311
+
312
+ const client = (req: Request) =>
313
+ new HorusClient({
314
+ baseUrl: process.env.HORUS_BASE_URL!,
315
+ apiKey: process.env.HORUS_API_KEY!,
316
+ getIdToken: async () => req.headers.get('x-horus-id-token') ?? '',
317
+ });
318
+
319
+ const handlers: Record<string, (req: Request, body: any) => Promise<unknown>> = {
320
+ 'auth/email/signin': (req, body) => client(req).auth.signInWithEmail(body),
321
+ 'auth/email/signup': (req, body) => client(req).auth.signUpWithEmail(body),
322
+ 'auth/email-link/send': (req, body) => client(req).auth.sendEmailLink(body),
323
+ 'auth/email-link/verify': (req, body) => client(req).auth.verifyEmailLink(body),
324
+ 'auth/oauth': (req, body) => client(req).auth.signInWithOAuth(body),
325
+ 'auth/refresh': (req, body) => client(req).auth.refresh(body.refreshToken),
326
+ 'wallets': (req) => client(req).wallets.get(),
327
+ 'sign-message': (req, body) => client(req).wallets.signMessage(body),
328
+ 'transfer/native': (req, body) => client(req).transfers.native(body),
329
+ 'transfer/token': (req, body) => client(req).transfers.token(body),
330
+ };
331
+
332
+ async function dispatch(req: Request, ctx: { params: { path: string[] } }) {
333
+ const fn = handlers[ctx.params.path.join('/')];
334
+ if (!fn) return Response.json({ message: 'Not found' }, { status: 404 });
335
+ try {
336
+ const body = req.headers.get('content-type')?.includes('json') ? await req.json() : undefined;
337
+ return Response.json(await fn(req, body));
338
+ } catch (err: any) {
339
+ return Response.json({ message: err.message }, { status: err.status ?? 500 });
340
+ }
341
+ }
342
+
343
+ export const GET = dispatch;
344
+ export const POST = dispatch;
345
+ ```
346
+
347
+ ## Errors
348
+
349
+ ```tsx
350
+ import { HorusHttpError } from '@horus-wallet/sdk-react';
351
+
352
+ try {
353
+ await loginWithEmail({ email, password });
354
+ } catch (err) {
355
+ if (err instanceof HorusHttpError) {
356
+ if (err.status === 401) {
357
+ // Wrong creds, expired token, or token rejected by server
358
+ }
359
+ if (err.status === 429) {
360
+ // Rate limited — back off
361
+ }
362
+ }
363
+ }
364
+ ```
365
+
366
+ The hooks (`useWallets`, `useSignMessage`, `useTransfer`) also expose `error` as part of their return shape so you can render error states declaratively.
367
+
368
+ ## Security
369
+
370
+ - The `hk_sk_*` secret stays on your backend — it never reaches the browser
371
+ - Tokens persist in `localStorage` by default. For stricter privacy, use `sessionStorage` (cleared on tab close) or `memory` (RAM only) via the `tokenStorage` prop
372
+ - The popup flows pin their `postMessage` target origin to the partner's window — tokens cannot be exfiltrated to other origins
373
+ - White-label settings (hiding the Horus footer in the popup) are server-enforced — partners on the standard tier always see the footer regardless of client config
374
+
375
+ ## Coming from Privy / Magic / Web3Auth?
376
+
377
+ | Privy | Horus |
378
+ |---|---|
379
+ | `<PrivyProvider>` | `<HorusProvider>` |
380
+ | `usePrivy()` | `useHorusAuth()` |
381
+ | `usePrivy().login()` | `useHorusAuth().loginWithGoogle()` (or any flow) |
382
+ | `useWallets()` | `useWallets()` |
383
+ | `usePrivy().user` | `useHorusAuth().user` or `useUser()` |
384
+ | Hosted modal | `<HorusLoginButton>` (popup) |
385
+ | Backend SDK | `@horus-wallet/sdk` |
386
+
387
+ Conceptual differences:
388
+ - Horus uses a **partner-backend proxy** so your secret never leaves your server. Privy talks to its API directly from the browser.
389
+ - **More chains supported** — EVM + Bitcoin + ICP + Casper + Aeternity all in one SDK.
390
+
391
+ ## What's not yet here
392
+
393
+ - External wallet connect (MetaMask / WalletConnect) — coming
394
+ - Mobile SDKs (iOS / Android / React Native) — coming
395
+ - Drop-in `<HorusAuthModal>` (inline UI without popup) — coming
396
+ - Pre-generated wallets for unsigned-in users — coming
397
+
398
+ For partner integration questions: `info@horuswallet.com`.