@smarthivelabs-devs/auth-expo 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +505 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +50 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# @smarthivelabs-devs/auth-expo
|
|
2
|
+
|
|
3
|
+
SmartHive Auth for React Native and Expo. Provides a provider, hooks, and components with SecureStore-backed token storage.
|
|
4
|
+
|
|
5
|
+
Supports two sign-in modes — both in the same package, zero config difference:
|
|
6
|
+
|
|
7
|
+
| Mode | How it works | Good for |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Headless** | Call `signIn.*` directly — no browser, custom UI | Native mobile apps with branded login screens |
|
|
10
|
+
| **OAuth redirect** | `login()` opens system browser, deep-links back | Social login, SSO, or when you want the hosted UI |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx expo install @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# npm / pnpm
|
|
22
|
+
npm install @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
|
|
23
|
+
pnpm add @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Peer dependencies:** `expo-auth-session>=5`, `expo-secure-store>=12`, `react>=18`, `react-native>=0.73`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
Wrap your root component with `SmartHiveProvider`. The `redirectUri` is only needed for the OAuth redirect flow — you can still set it even if you only use headless.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// app/_layout.tsx
|
|
36
|
+
import { SmartHiveProvider, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
|
|
37
|
+
|
|
38
|
+
export default function Layout() {
|
|
39
|
+
return (
|
|
40
|
+
<SmartHiveProvider
|
|
41
|
+
projectId={process.env.EXPO_PUBLIC_AUTH_PROJECT_ID!}
|
|
42
|
+
publishableKey={process.env.EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY!}
|
|
43
|
+
baseUrl={process.env.EXPO_PUBLIC_AUTH_BASE_URL!}
|
|
44
|
+
redirectUri={buildRedirectUri("myapp")}
|
|
45
|
+
>
|
|
46
|
+
<Stack />
|
|
47
|
+
</SmartHiveProvider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# .env
|
|
54
|
+
EXPO_PUBLIC_AUTH_PROJECT_ID=proj_abc123
|
|
55
|
+
EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY=pk_prod_abc123
|
|
56
|
+
EXPO_PUBLIC_AUTH_BASE_URL=https://auth.myapp.com
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Headless Sign-in (Custom Login Screen)
|
|
62
|
+
|
|
63
|
+
No browser, no redirect. Call the method, get tokens. Full control of your UI.
|
|
64
|
+
|
|
65
|
+
### Email + Password
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { useAuth } from "@smarthivelabs-devs/auth-expo";
|
|
69
|
+
import { useState } from "react";
|
|
70
|
+
import { Button, TextInput, View, Text } from "react-native";
|
|
71
|
+
|
|
72
|
+
export default function LoginScreen() {
|
|
73
|
+
const { signIn } = useAuth();
|
|
74
|
+
const [email, setEmail] = useState("");
|
|
75
|
+
const [password, setPassword] = useState("");
|
|
76
|
+
const [error, setError] = useState("");
|
|
77
|
+
|
|
78
|
+
async function handleSignIn() {
|
|
79
|
+
try {
|
|
80
|
+
await signIn.email({ email, password });
|
|
81
|
+
// Session is saved automatically — user is now signed in
|
|
82
|
+
} catch (e: any) {
|
|
83
|
+
setError(e.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View>
|
|
89
|
+
<TextInput value={email} onChangeText={setEmail} placeholder="Email" />
|
|
90
|
+
<TextInput value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry />
|
|
91
|
+
{error ? <Text>{error}</Text> : null}
|
|
92
|
+
<Button title="Sign in" onPress={handleSignIn} />
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Phone OTP
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { useAuth } from "@smarthivelabs-devs/auth-expo";
|
|
102
|
+
import { useState } from "react";
|
|
103
|
+
import { Button, TextInput, View } from "react-native";
|
|
104
|
+
|
|
105
|
+
export default function PhoneLoginScreen() {
|
|
106
|
+
const { signIn } = useAuth();
|
|
107
|
+
const [phone, setPhone] = useState("");
|
|
108
|
+
const [code, setCode] = useState("");
|
|
109
|
+
const [step, setStep] = useState<"phone" | "code">("phone");
|
|
110
|
+
|
|
111
|
+
async function sendOtp() {
|
|
112
|
+
await signIn.phone.sendOtp({ phoneNumber: phone });
|
|
113
|
+
setStep("code");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function verifyOtp() {
|
|
117
|
+
await signIn.phone.verify({ phoneNumber: phone, code });
|
|
118
|
+
// Signed in — session saved automatically
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (step === "phone") {
|
|
122
|
+
return (
|
|
123
|
+
<View>
|
|
124
|
+
<TextInput value={phone} onChangeText={setPhone} placeholder="+1234567890" keyboardType="phone-pad" />
|
|
125
|
+
<Button title="Send code" onPress={sendOtp} />
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<View>
|
|
132
|
+
<TextInput value={code} onChangeText={setCode} placeholder="Enter code" keyboardType="number-pad" />
|
|
133
|
+
<Button title="Verify" onPress={verifyOtp} />
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Email OTP
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const { signIn } = useAuth();
|
|
143
|
+
|
|
144
|
+
// Step 1 — send the code
|
|
145
|
+
await signIn.emailOtp.send({ email: "user@example.com" });
|
|
146
|
+
|
|
147
|
+
// Step 2 — verify (returns session, user is now signed in)
|
|
148
|
+
await signIn.emailOtp.verify({ email: "user@example.com", code: "123456" });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Magic Link
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
const { signIn } = useAuth();
|
|
155
|
+
|
|
156
|
+
// Sends an email — user clicks the link to sign in (no token returned here)
|
|
157
|
+
await signIn.magicLink.send({ email: "user@example.com" });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Headless Sign-up
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { useAuth } from "@smarthivelabs-devs/auth-expo";
|
|
166
|
+
|
|
167
|
+
const { signUp } = useAuth();
|
|
168
|
+
|
|
169
|
+
const result = await signUp.email({
|
|
170
|
+
email: "user@example.com",
|
|
171
|
+
password: "secret123",
|
|
172
|
+
name: "Jane Doe", // optional
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (result.requiresVerification) {
|
|
176
|
+
// Email verification required — show "check your inbox" screen
|
|
177
|
+
// No session yet, tokens are empty
|
|
178
|
+
} else {
|
|
179
|
+
// Account created and signed in immediately
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## OAuth Redirect Sign-in (Browser)
|
|
186
|
+
|
|
187
|
+
The original flow is completely unchanged. Use it for social login or when you want the hosted login page.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { useAuth } from "@smarthivelabs-devs/auth-expo";
|
|
191
|
+
import { Button } from "react-native";
|
|
192
|
+
|
|
193
|
+
export default function LoginScreen() {
|
|
194
|
+
const { login } = useAuth();
|
|
195
|
+
return <Button title="Sign in with SmartHive" onPress={() => login()} />;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Deep Link Setup (required for OAuth redirect only)
|
|
200
|
+
|
|
201
|
+
If you use `login()`, register a custom scheme in `app.json` so the browser can redirect back:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"expo": {
|
|
206
|
+
"scheme": "myapp",
|
|
207
|
+
"android": {
|
|
208
|
+
"intentFilters": [
|
|
209
|
+
{
|
|
210
|
+
"action": "VIEW",
|
|
211
|
+
"autoVerify": true,
|
|
212
|
+
"data": [{ "scheme": "myapp" }],
|
|
213
|
+
"category": ["BROWSABLE", "DEFAULT"]
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Rebuild after changing `app.json`:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
npx expo prebuild
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
No extra setup needed for headless sign-in — it works without a deep link.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Sign-out
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
const { logout } = useAuth();
|
|
235
|
+
|
|
236
|
+
// Clears SecureStore + invalidates session on the server
|
|
237
|
+
await logout();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Hooks
|
|
243
|
+
|
|
244
|
+
### `useAuth()`
|
|
245
|
+
|
|
246
|
+
Returns the full auth context.
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
const {
|
|
250
|
+
session, // AuthSession | null
|
|
251
|
+
isLoaded, // true once initial SecureStore read is done
|
|
252
|
+
isSignedIn, // boolean
|
|
253
|
+
login, // OAuth redirect sign-in
|
|
254
|
+
logout, // sign out
|
|
255
|
+
signIn, // headless sign-in methods
|
|
256
|
+
signUp, // headless sign-up methods
|
|
257
|
+
refreshSession, // force a token refresh
|
|
258
|
+
authFetch, // authenticated fetch wrapper
|
|
259
|
+
getAuthorizationHeader, // () => Promise<{ authorization: string }>
|
|
260
|
+
} = useAuth();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### `useSession()`
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
const session = useSession(); // AuthSession | null
|
|
267
|
+
// session.accessToken, session.refreshToken, session.expiresAt, session.user
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### `useUser()`
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
const user = useUser(); // unknown | null
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `useIsLoaded()`
|
|
277
|
+
|
|
278
|
+
`true` once the initial SecureStore check is complete. Use this to avoid a flash of unauthenticated state on startup.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
const isLoaded = useIsLoaded();
|
|
282
|
+
if (!isLoaded) return <SplashScreen />;
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### `useIsSignedIn()`
|
|
286
|
+
|
|
287
|
+
Returns `true` (signed in), `false` (signed out), or `null` (still loading).
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
const isSignedIn = useIsSignedIn();
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (isSignedIn === false) router.replace("/login");
|
|
294
|
+
}, [isSignedIn]);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### `useAuthFetch()`
|
|
298
|
+
|
|
299
|
+
Authenticated `fetch` wrapper. Bearer token is injected automatically and refreshed transparently when near expiry.
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
const authFetch = useAuthFetch();
|
|
303
|
+
const res = await authFetch("https://api.myapp.com/protected");
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `useAuthorizationHeader()`
|
|
307
|
+
|
|
308
|
+
Resolves to `{ authorization: "Bearer <token>" }`. Useful for GraphQL clients or custom SDK setup.
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
const getAuthorizationHeader = useAuthorizationHeader();
|
|
312
|
+
const headers = await getAuthorizationHeader();
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Render Helpers
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
import { SignedIn, SignedOut, AuthLoading } from "@smarthivelabs-devs/auth-expo";
|
|
321
|
+
|
|
322
|
+
// Shown only when loaded + authenticated
|
|
323
|
+
<SignedIn><Dashboard /></SignedIn>
|
|
324
|
+
|
|
325
|
+
// Shown only when loaded + not authenticated
|
|
326
|
+
<SignedOut><LoginScreen /></SignedOut>
|
|
327
|
+
|
|
328
|
+
// Shown while the initial SecureStore check is running
|
|
329
|
+
<AuthLoading><ActivityIndicator /></AuthLoading>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Expo Router Integration
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
app/
|
|
338
|
+
├── _layout.tsx ← SmartHiveProvider here
|
|
339
|
+
├── index.tsx ← SignedIn / SignedOut routing
|
|
340
|
+
├── login.tsx ← your custom login screen using signIn.*
|
|
341
|
+
└── (protected)/
|
|
342
|
+
└── dashboard.tsx
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
// app/_layout.tsx
|
|
347
|
+
import { Stack } from "expo-router";
|
|
348
|
+
import { SmartHiveProvider, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
|
|
349
|
+
|
|
350
|
+
export default function Layout() {
|
|
351
|
+
return (
|
|
352
|
+
<SmartHiveProvider
|
|
353
|
+
projectId={process.env.EXPO_PUBLIC_AUTH_PROJECT_ID!}
|
|
354
|
+
publishableKey={process.env.EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY!}
|
|
355
|
+
baseUrl={process.env.EXPO_PUBLIC_AUTH_BASE_URL!}
|
|
356
|
+
redirectUri={buildRedirectUri("myapp")}
|
|
357
|
+
>
|
|
358
|
+
<Stack />
|
|
359
|
+
</SmartHiveProvider>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
// app/index.tsx
|
|
366
|
+
import { SignedIn, SignedOut, AuthLoading } from "@smarthivelabs-devs/auth-expo";
|
|
367
|
+
import { Redirect } from "expo-router";
|
|
368
|
+
import { ActivityIndicator } from "react-native";
|
|
369
|
+
|
|
370
|
+
export default function Index() {
|
|
371
|
+
return (
|
|
372
|
+
<>
|
|
373
|
+
<AuthLoading><ActivityIndicator /></AuthLoading>
|
|
374
|
+
<SignedIn><Redirect href="/dashboard" /></SignedIn>
|
|
375
|
+
<SignedOut><Redirect href="/login" /></SignedOut>
|
|
376
|
+
</>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
// app/login.tsx — custom screen, no browser redirect
|
|
383
|
+
import { useAuth } from "@smarthivelabs-devs/auth-expo";
|
|
384
|
+
import { useState } from "react";
|
|
385
|
+
import { Button, TextInput, View, Text, StyleSheet } from "react-native";
|
|
386
|
+
|
|
387
|
+
export default function LoginScreen() {
|
|
388
|
+
const { signIn } = useAuth();
|
|
389
|
+
const [email, setEmail] = useState("");
|
|
390
|
+
const [password, setPassword] = useState("");
|
|
391
|
+
const [loading, setLoading] = useState(false);
|
|
392
|
+
const [error, setError] = useState("");
|
|
393
|
+
|
|
394
|
+
async function handleSignIn() {
|
|
395
|
+
setLoading(true);
|
|
396
|
+
setError("");
|
|
397
|
+
try {
|
|
398
|
+
await signIn.email({ email, password });
|
|
399
|
+
} catch (e: any) {
|
|
400
|
+
setError(e.message ?? "Sign in failed.");
|
|
401
|
+
} finally {
|
|
402
|
+
setLoading(false);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<View style={styles.container}>
|
|
408
|
+
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="Email" autoCapitalize="none" />
|
|
409
|
+
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry />
|
|
410
|
+
{error ? <Text style={styles.error}>{error}</Text> : null}
|
|
411
|
+
<Button title={loading ? "Signing in…" : "Sign in"} onPress={handleSignIn} disabled={loading} />
|
|
412
|
+
</View>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const styles = StyleSheet.create({
|
|
417
|
+
container: { flex: 1, justifyContent: "center", padding: 24 },
|
|
418
|
+
input: { borderWidth: 1, borderColor: "#ccc", borderRadius: 8, padding: 12, marginBottom: 12 },
|
|
419
|
+
error: { color: "red", marginBottom: 12 },
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Token Storage
|
|
426
|
+
|
|
427
|
+
All tokens are stored in `expo-secure-store`:
|
|
428
|
+
|
|
429
|
+
- **iOS**: Keychain Services
|
|
430
|
+
- **Android**: Android Keystore (AES encryption)
|
|
431
|
+
|
|
432
|
+
PKCE verifier and state (used during OAuth redirect flow) are also stored in SecureStore and deleted after the code exchange completes.
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Provider Props
|
|
437
|
+
|
|
438
|
+
| Prop | Type | Required | Description |
|
|
439
|
+
|---|---|---|---|
|
|
440
|
+
| `projectId` | `string` | Yes | Your SmartHive project ID |
|
|
441
|
+
| `publishableKey` | `string` | Yes | Your publishable key (`pk_prod_*`) |
|
|
442
|
+
| `baseUrl` | `string` | Yes | URL of your SmartHive Auth service |
|
|
443
|
+
| `redirectUri` | `string` | Yes | Deep link callback URI — used only for OAuth redirect flow |
|
|
444
|
+
| `authDomain` | `string` | No | Custom branded auth domain |
|
|
445
|
+
| `children` | `ReactNode` | Yes | Your app tree |
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## TypeScript Types
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import type {
|
|
453
|
+
SmartHiveExpoConfig,
|
|
454
|
+
SmartHiveProviderProps,
|
|
455
|
+
} from "@smarthivelabs-devs/auth-expo";
|
|
456
|
+
|
|
457
|
+
import type {
|
|
458
|
+
AuthSession,
|
|
459
|
+
HeadlessClient,
|
|
460
|
+
HeadlessSignInResult,
|
|
461
|
+
HeadlessSignUpResult,
|
|
462
|
+
SmartHiveAuthClient,
|
|
463
|
+
} from "@smarthivelabs-devs/auth-sdk";
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Low-level: `initExpoAuth`
|
|
469
|
+
|
|
470
|
+
Direct client without the provider (advanced use):
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { initExpoAuth, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
|
|
474
|
+
|
|
475
|
+
const client = initExpoAuth({
|
|
476
|
+
projectId: "proj_abc123",
|
|
477
|
+
publishableKey: "pk_prod_abc123",
|
|
478
|
+
baseUrl: "https://auth.myapp.com",
|
|
479
|
+
redirectUri: buildRedirectUri("myapp"),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await client.initialize();
|
|
483
|
+
|
|
484
|
+
// Headless sign-in
|
|
485
|
+
const session = await client.headless.signIn.email({ email, password });
|
|
486
|
+
|
|
487
|
+
// OAuth redirect
|
|
488
|
+
await client.login();
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Related Packages
|
|
494
|
+
|
|
495
|
+
| Package | Use case |
|
|
496
|
+
|---|---|
|
|
497
|
+
| [`@smarthivelabs-devs/auth-sdk`](https://www.npmjs.com/package/@smarthivelabs-devs/auth-sdk) | Core SDK — framework-agnostic |
|
|
498
|
+
| [`@smarthivelabs-devs/auth-react`](https://www.npmjs.com/package/@smarthivelabs-devs/auth-react) | React web apps |
|
|
499
|
+
| [`@smarthivelabs-devs/auth-server`](https://www.npmjs.com/package/@smarthivelabs-devs/auth-server) | Express / Next.js server-side JWT verification |
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## License
|
|
504
|
+
|
|
505
|
+
MIT © SmartHive Labs
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { SmartHiveAuthConfig, SmartHiveAuthClient, AuthSession } from '@smarthivelabs-devs/auth-sdk';
|
|
2
|
+
import { SmartHiveAuthConfig, SmartHiveAuthClient, AuthSession, HeadlessSignInResult, HeadlessSignUpResult } from '@smarthivelabs-devs/auth-sdk';
|
|
3
3
|
|
|
4
4
|
interface SmartHiveExpoConfig extends Omit<SmartHiveAuthConfig, "storage"> {
|
|
5
5
|
/** Deep link redirect URI, e.g. "myapp://auth/callback" */
|
|
@@ -20,6 +20,7 @@ interface AuthContextValue {
|
|
|
20
20
|
session: AuthSession | null;
|
|
21
21
|
isLoaded: boolean;
|
|
22
22
|
isSignedIn: boolean;
|
|
23
|
+
/** OAuth 2.0 + PKCE browser redirect sign-in (unchanged). */
|
|
23
24
|
login(options?: {
|
|
24
25
|
redirectUri?: string;
|
|
25
26
|
}): Promise<void>;
|
|
@@ -27,23 +28,56 @@ interface AuthContextValue {
|
|
|
27
28
|
refreshSession(): Promise<void>;
|
|
28
29
|
getAuthorizationHeader(): Promise<Record<string, string>>;
|
|
29
30
|
authFetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
31
|
+
/**
|
|
32
|
+
* Headless (no browser) sign-in methods for custom login screens.
|
|
33
|
+
* Tokens are stored in SecureStore automatically on success.
|
|
34
|
+
*/
|
|
35
|
+
signIn: {
|
|
36
|
+
email(params: {
|
|
37
|
+
email: string;
|
|
38
|
+
password: string;
|
|
39
|
+
}): Promise<HeadlessSignInResult>;
|
|
40
|
+
phone: {
|
|
41
|
+
sendOtp(params: {
|
|
42
|
+
phoneNumber: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
verify(params: {
|
|
45
|
+
phoneNumber: string;
|
|
46
|
+
code: string;
|
|
47
|
+
}): Promise<HeadlessSignInResult>;
|
|
48
|
+
};
|
|
49
|
+
emailOtp: {
|
|
50
|
+
send(params: {
|
|
51
|
+
email: string;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
verify(params: {
|
|
54
|
+
email: string;
|
|
55
|
+
code: string;
|
|
56
|
+
}): Promise<HeadlessSignInResult>;
|
|
57
|
+
};
|
|
58
|
+
magicLink: {
|
|
59
|
+
send(params: {
|
|
60
|
+
email: string;
|
|
61
|
+
callbackURL?: string;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
signUp: {
|
|
66
|
+
email(params: {
|
|
67
|
+
email: string;
|
|
68
|
+
password: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
}): Promise<HeadlessSignUpResult>;
|
|
71
|
+
};
|
|
30
72
|
}
|
|
31
73
|
interface SmartHiveProviderProps extends SmartHiveExpoConfig {
|
|
32
74
|
children: React.ReactNode;
|
|
33
75
|
}
|
|
34
76
|
/**
|
|
35
77
|
* Wraps your Expo app and provides auth state to all child components.
|
|
36
|
-
* Tokens are stored in SecureStore.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* <SmartHiveProvider
|
|
40
|
-
* projectId="proj_..."
|
|
41
|
-
* publishableKey="pk_..."
|
|
42
|
-
* baseUrl="https://authcore.smarthivelabs.dev/prod"
|
|
43
|
-
* redirectUri="myapp://auth/callback"
|
|
44
|
-
* >
|
|
45
|
-
* <App />
|
|
46
|
-
* </SmartHiveProvider>
|
|
78
|
+
* Tokens are stored in SecureStore. Supports both:
|
|
79
|
+
* - OAuth 2.0 + PKCE browser redirect via `login()`
|
|
80
|
+
* - Headless direct sign-in via `signIn.*` (no browser, custom login screens)
|
|
47
81
|
*/
|
|
48
82
|
declare function SmartHiveProvider({ children, ...config }: SmartHiveProviderProps): react_jsx_runtime.JSX.Element;
|
|
49
83
|
declare function useAuth(): AuthContextValue;
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,52 @@ function SmartHiveProvider({ children, ...config }) {
|
|
|
111
111
|
(input, init) => client.fetch(input, init),
|
|
112
112
|
[client]
|
|
113
113
|
);
|
|
114
|
+
function wrapHeadlessSignIn(fn) {
|
|
115
|
+
return async (params) => {
|
|
116
|
+
const result = await fn(params);
|
|
117
|
+
setSession({
|
|
118
|
+
accessToken: result.accessToken,
|
|
119
|
+
refreshToken: result.refreshToken,
|
|
120
|
+
expiresAt: result.expiresAt,
|
|
121
|
+
user: result.user
|
|
122
|
+
});
|
|
123
|
+
return result;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const signIn = useMemo(() => {
|
|
127
|
+
const h = client.headless;
|
|
128
|
+
return {
|
|
129
|
+
email: wrapHeadlessSignIn(h.signIn.email.bind(h.signIn)),
|
|
130
|
+
phone: {
|
|
131
|
+
sendOtp: (p) => h.signIn.phone.sendOtp(p),
|
|
132
|
+
verify: wrapHeadlessSignIn(h.signIn.phone.verify.bind(h.signIn.phone))
|
|
133
|
+
},
|
|
134
|
+
emailOtp: {
|
|
135
|
+
send: (p) => h.signIn.emailOtp.send(p),
|
|
136
|
+
verify: wrapHeadlessSignIn(h.signIn.emailOtp.verify.bind(h.signIn.emailOtp))
|
|
137
|
+
},
|
|
138
|
+
magicLink: {
|
|
139
|
+
send: (p) => h.signIn.magicLink.send(p)
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}, [client]);
|
|
143
|
+
const signUp = useMemo(() => {
|
|
144
|
+
const h = client.headless;
|
|
145
|
+
return {
|
|
146
|
+
email: async (params) => {
|
|
147
|
+
const result = await h.signUp.email(params);
|
|
148
|
+
if (!result.requiresVerification) {
|
|
149
|
+
setSession({
|
|
150
|
+
accessToken: result.accessToken,
|
|
151
|
+
refreshToken: result.refreshToken,
|
|
152
|
+
expiresAt: result.expiresAt,
|
|
153
|
+
user: result.user
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}, [client]);
|
|
114
160
|
const value = useMemo(
|
|
115
161
|
() => ({
|
|
116
162
|
client,
|
|
@@ -121,9 +167,11 @@ function SmartHiveProvider({ children, ...config }) {
|
|
|
121
167
|
logout,
|
|
122
168
|
refreshSession,
|
|
123
169
|
getAuthorizationHeader,
|
|
124
|
-
authFetch
|
|
170
|
+
authFetch,
|
|
171
|
+
signIn,
|
|
172
|
+
signUp
|
|
125
173
|
}),
|
|
126
|
-
[client, session, isLoaded, login, logout, refreshSession, getAuthorizationHeader, authFetch]
|
|
174
|
+
[client, session, isLoaded, login, logout, refreshSession, getAuthorizationHeader, authFetch, signIn, signUp]
|
|
127
175
|
);
|
|
128
176
|
return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
|
|
129
177
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { Linking } from \"react-native\";\nimport { AuthRequest } from \"expo-auth-session\";\nimport * as SecureStore from \"expo-secure-store\";\nimport {\n initAuth,\n type AuthSession,\n type AuthStorage,\n type SmartHiveAuthClient,\n type SmartHiveAuthConfig,\n} from \"@smarthivelabs-devs/auth-sdk\";\n\n// ── Secure storage adapter ────────────────────────────────────────────────────\n\nconst secureStorage: AuthStorage = {\n getItem: (key) => SecureStore.getItemAsync(key),\n setItem: (key, value) => SecureStore.setItemAsync(key, value),\n removeItem: (key) => SecureStore.deleteItemAsync(key),\n};\n\nconst storageKeys = {\n pkceVerifier: \"smarthive.auth.pkce_verifier\",\n pkceState: \"smarthive.auth.pkce_state\"\n} as const;\n\n// ── Config ────────────────────────────────────────────────────────────────────\n\nexport interface SmartHiveExpoConfig extends Omit<SmartHiveAuthConfig, \"storage\"> {\n /** Deep link redirect URI, e.g. \"myapp://auth/callback\" */\n redirectUri: string;\n}\n\n/**\n * Build a deep link redirect URI from your Expo app scheme.\n * @example buildRedirectUri(\"myapp\") → \"myapp://auth/callback\"\n */\nexport function buildRedirectUri(scheme: string, path = \"auth/callback\"): string {\n return `${scheme}://${path}`;\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\");\n}\n\n// ── Auth client (Expo flavour) ─────────────────────────────────────────────────\n\n/**\n * Creates an auth client that uses SecureStore for token storage and\n * Linking.openURL for the OAuth redirect (instead of location.assign).\n */\nexport function initExpoAuth(config: SmartHiveExpoConfig): SmartHiveAuthClient {\n const base = initAuth({\n ...config,\n storage: secureStorage,\n temporaryStorage: secureStorage\n } as SmartHiveAuthConfig);\n\n return {\n ...base,\n async login(options) {\n const redirectUri = options?.redirectUri ?? config.redirectUri;\n const authBase = normalizeBaseUrl(config.authDomain ?? config.baseUrl);\n const request = new AuthRequest({\n clientId: config.publishableKey,\n redirectUri,\n responseType: \"code\",\n usePKCE: true,\n state: options?.state,\n extraParams: {\n project_id: config.projectId,\n publishable_key: config.publishableKey\n }\n });\n const url = await request.makeAuthUrlAsync({\n authorizationEndpoint: `${authBase}/api/auth/oauth2/authorize`\n });\n\n if (request.codeVerifier) {\n await secureStorage.setItem(storageKeys.pkceVerifier, request.codeVerifier);\n }\n await secureStorage.setItem(storageKeys.pkceState, request.state);\n await Linking.openURL(url);\n },\n };\n}\n\n// ── Context ────────────────────────────────────────────────────────────────────\n\ninterface AuthContextValue {\n client: SmartHiveAuthClient;\n session: AuthSession | null;\n isLoaded: boolean;\n isSignedIn: boolean;\n login(options?: { redirectUri?: string }): Promise<void>;\n logout(): Promise<void>;\n refreshSession(): Promise<void>;\n getAuthorizationHeader(): Promise<Record<string, string>>;\n authFetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n// ── Provider ───────────────────────────────────────────────────────────────────\n\nexport interface SmartHiveProviderProps extends SmartHiveExpoConfig {\n children: React.ReactNode;\n}\n\n/**\n * Wraps your Expo app and provides auth state to all child components.\n * Tokens are stored in SecureStore. OAuth is handled via deep links.\n *\n * @example\n * <SmartHiveProvider\n * projectId=\"proj_...\"\n * publishableKey=\"pk_...\"\n * baseUrl=\"https://authcore.smarthivelabs.dev/prod\"\n * redirectUri=\"myapp://auth/callback\"\n * >\n * <App />\n * </SmartHiveProvider>\n */\nexport function SmartHiveProvider({ children, ...config }: SmartHiveProviderProps) {\n const configRef = useRef(config);\n const client = useMemo(() => initExpoAuth(configRef.current), []);\n\n const [session, setSession] = useState<AuthSession | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n // Initial session load from SecureStore\n useEffect(() => {\n let cancelled = false;\n client\n .initialize()\n .then(() => client.getSession())\n .then((s) => { if (!cancelled) setSession(s); })\n .catch(() => {})\n .finally(() => { if (!cancelled) setIsLoaded(true); });\n return () => { cancelled = true; };\n }, [client]);\n\n // Deep link listener — catches the OAuth callback redirect back into the app\n useEffect(() => {\n function handleUrl({ url }: { url: string }) {\n client\n .handleCallback({ url })\n .then((s) => setSession(s))\n .catch(() => {});\n }\n\n const sub = Linking.addEventListener(\"url\", handleUrl);\n\n // Handle cold-start: app opened directly from the OAuth redirect URL\n Linking.getInitialURL().then((url) => {\n if (url) handleUrl({ url });\n });\n\n return () => sub.remove();\n }, [client]);\n\n const login = useCallback(\n (options?: { redirectUri?: string }) => client.login(options),\n [client]\n );\n\n const logout = useCallback(async () => {\n await client.logout();\n setSession(null);\n }, [client]);\n\n const refreshSession = useCallback(async () => {\n setSession(await client.refreshSession());\n }, [client]);\n\n const getAuthorizationHeader = useCallback(\n () => client.getAuthorizationHeader(),\n [client]\n );\n\n const authFetch = useCallback(\n (input: string | URL | Request, init?: RequestInit) => client.fetch(input, init),\n [client]\n );\n\n const value = useMemo<AuthContextValue>(\n () => ({\n client,\n session,\n isLoaded,\n isSignedIn: !!session,\n login,\n logout,\n refreshSession,\n getAuthorizationHeader,\n authFetch\n }),\n [client, session, isLoaded, login, logout, refreshSession, getAuthorizationHeader, authFetch]\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n// ── Hooks ──────────────────────────────────────────────────────────────────────\n\nfunction useAuthContext(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth must be used inside <SmartHiveProvider>.\");\n return ctx;\n}\n\nexport function useAuth() { return useAuthContext(); }\nexport function useSession() { return useAuthContext().session; }\nexport function useUser() { return useAuthContext().session?.user ?? null; }\nexport function useIsLoaded() { return useAuthContext().isLoaded; }\n\nexport function useIsSignedIn() {\n const { isLoaded, isSignedIn } = useAuthContext();\n return isLoaded ? isSignedIn : null;\n}\n\nexport function useAuthFetch() {\n return useAuthContext().authFetch;\n}\n\nexport function useAuthorizationHeader() {\n return useAuthContext().getAuthorizationHeader;\n}\n\n// ── Render helpers ─────────────────────────────────────────────────────────────\n\nexport function SignedIn({ children }: { children: React.ReactNode }) {\n const { isLoaded, isSignedIn } = useAuthContext();\n if (!isLoaded || !isSignedIn) return null;\n return <>{children}</>;\n}\n\nexport function SignedOut({ children }: { children: React.ReactNode }) {\n const { isLoaded, isSignedIn } = useAuthContext();\n if (!isLoaded || isSignedIn) return null;\n return <>{children}</>;\n}\n\nexport function AuthLoading({ children }: { children: React.ReactNode }) {\n const { isLoaded } = useAuthContext();\n return isLoaded ? null : <>{children}</>;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { Linking } from \"react-native\";\nimport { AuthRequest } from \"expo-auth-session\";\nimport * as SecureStore from \"expo-secure-store\";\nimport {\n initAuth,\n type AuthSession,\n type AuthStorage,\n type HeadlessClient,\n type HeadlessSignInResult,\n type HeadlessSignUpResult,\n type SmartHiveAuthClient,\n type SmartHiveAuthConfig,\n} from \"@smarthivelabs-devs/auth-sdk\";\n\n// ── Secure storage adapter ────────────────────────────────────────────────────\n\nconst secureStorage: AuthStorage = {\n getItem: (key) => SecureStore.getItemAsync(key),\n setItem: (key, value) => SecureStore.setItemAsync(key, value),\n removeItem: (key) => SecureStore.deleteItemAsync(key),\n};\n\nconst storageKeys = {\n pkceVerifier: \"smarthive.auth.pkce_verifier\",\n pkceState: \"smarthive.auth.pkce_state\"\n} as const;\n\n// ── Config ────────────────────────────────────────────────────────────────────\n\nexport interface SmartHiveExpoConfig extends Omit<SmartHiveAuthConfig, \"storage\"> {\n /** Deep link redirect URI, e.g. \"myapp://auth/callback\" */\n redirectUri: string;\n}\n\n/**\n * Build a deep link redirect URI from your Expo app scheme.\n * @example buildRedirectUri(\"myapp\") → \"myapp://auth/callback\"\n */\nexport function buildRedirectUri(scheme: string, path = \"auth/callback\"): string {\n return `${scheme}://${path}`;\n}\n\nfunction normalizeBaseUrl(baseUrl: string) {\n return baseUrl.replace(/\\/$/, \"\");\n}\n\n// ── Auth client (Expo flavour) ─────────────────────────────────────────────────\n\n/**\n * Creates an auth client that uses SecureStore for token storage and\n * Linking.openURL for the OAuth redirect (instead of location.assign).\n */\nexport function initExpoAuth(config: SmartHiveExpoConfig): SmartHiveAuthClient {\n const base = initAuth({\n ...config,\n storage: secureStorage,\n temporaryStorage: secureStorage\n } as SmartHiveAuthConfig);\n\n return {\n ...base,\n async login(options) {\n const redirectUri = options?.redirectUri ?? config.redirectUri;\n const authBase = normalizeBaseUrl(config.authDomain ?? config.baseUrl);\n const request = new AuthRequest({\n clientId: config.publishableKey,\n redirectUri,\n responseType: \"code\",\n usePKCE: true,\n state: options?.state,\n extraParams: {\n project_id: config.projectId,\n publishable_key: config.publishableKey\n }\n });\n const url = await request.makeAuthUrlAsync({\n authorizationEndpoint: `${authBase}/api/auth/oauth2/authorize`\n });\n\n if (request.codeVerifier) {\n await secureStorage.setItem(storageKeys.pkceVerifier, request.codeVerifier);\n }\n await secureStorage.setItem(storageKeys.pkceState, request.state);\n await Linking.openURL(url);\n },\n };\n}\n\n// ── Context ────────────────────────────────────────────────────────────────────\n\ninterface AuthContextValue {\n client: SmartHiveAuthClient;\n session: AuthSession | null;\n isLoaded: boolean;\n isSignedIn: boolean;\n /** OAuth 2.0 + PKCE browser redirect sign-in (unchanged). */\n login(options?: { redirectUri?: string }): Promise<void>;\n logout(): Promise<void>;\n refreshSession(): Promise<void>;\n getAuthorizationHeader(): Promise<Record<string, string>>;\n authFetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;\n /**\n * Headless (no browser) sign-in methods for custom login screens.\n * Tokens are stored in SecureStore automatically on success.\n */\n signIn: {\n email(params: { email: string; password: string }): Promise<HeadlessSignInResult>;\n phone: {\n sendOtp(params: { phoneNumber: string }): Promise<void>;\n verify(params: { phoneNumber: string; code: string }): Promise<HeadlessSignInResult>;\n };\n emailOtp: {\n send(params: { email: string }): Promise<void>;\n verify(params: { email: string; code: string }): Promise<HeadlessSignInResult>;\n };\n magicLink: {\n send(params: { email: string; callbackURL?: string }): Promise<void>;\n };\n };\n signUp: {\n email(params: { email: string; password: string; name?: string }): Promise<HeadlessSignUpResult>;\n };\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n// ── Provider ───────────────────────────────────────────────────────────────────\n\nexport interface SmartHiveProviderProps extends SmartHiveExpoConfig {\n children: React.ReactNode;\n}\n\n/**\n * Wraps your Expo app and provides auth state to all child components.\n * Tokens are stored in SecureStore. Supports both:\n * - OAuth 2.0 + PKCE browser redirect via `login()`\n * - Headless direct sign-in via `signIn.*` (no browser, custom login screens)\n */\nexport function SmartHiveProvider({ children, ...config }: SmartHiveProviderProps) {\n const configRef = useRef(config);\n const client = useMemo(() => initExpoAuth(configRef.current), []);\n\n const [session, setSession] = useState<AuthSession | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n // Initial session load from SecureStore\n useEffect(() => {\n let cancelled = false;\n client\n .initialize()\n .then(() => client.getSession())\n .then((s) => { if (!cancelled) setSession(s); })\n .catch(() => {})\n .finally(() => { if (!cancelled) setIsLoaded(true); });\n return () => { cancelled = true; };\n }, [client]);\n\n // Deep link listener — catches the OAuth callback redirect back into the app\n useEffect(() => {\n function handleUrl({ url }: { url: string }) {\n client\n .handleCallback({ url })\n .then((s) => setSession(s))\n .catch(() => {});\n }\n\n const sub = Linking.addEventListener(\"url\", handleUrl);\n\n // Handle cold-start: app opened directly from the OAuth redirect URL\n Linking.getInitialURL().then((url) => {\n if (url) handleUrl({ url });\n });\n\n return () => sub.remove();\n }, [client]);\n\n const login = useCallback(\n (options?: { redirectUri?: string }) => client.login(options),\n [client]\n );\n\n const logout = useCallback(async () => {\n await client.logout();\n setSession(null);\n }, [client]);\n\n const refreshSession = useCallback(async () => {\n setSession(await client.refreshSession());\n }, [client]);\n\n const getAuthorizationHeader = useCallback(\n () => client.getAuthorizationHeader(),\n [client]\n );\n\n const authFetch = useCallback(\n (input: string | URL | Request, init?: RequestInit) => client.fetch(input, init),\n [client]\n );\n\n // ── Headless sign-in wrappers — save session + update state ──────────────────\n\n function wrapHeadlessSignIn(\n fn: (p: never) => Promise<HeadlessSignInResult>\n ) {\n return async (params: never) => {\n const result = await fn(params);\n setSession({\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n expiresAt: result.expiresAt,\n user: result.user,\n });\n return result;\n };\n }\n\n const signIn = useMemo<AuthContextValue[\"signIn\"]>(() => {\n const h: HeadlessClient = client.headless;\n return {\n email: wrapHeadlessSignIn(h.signIn.email.bind(h.signIn) as never),\n phone: {\n sendOtp: (p) => h.signIn.phone.sendOtp(p),\n verify: wrapHeadlessSignIn(h.signIn.phone.verify.bind(h.signIn.phone) as never),\n },\n emailOtp: {\n send: (p) => h.signIn.emailOtp.send(p),\n verify: wrapHeadlessSignIn(h.signIn.emailOtp.verify.bind(h.signIn.emailOtp) as never),\n },\n magicLink: {\n send: (p) => h.signIn.magicLink.send(p),\n },\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client]);\n\n const signUp = useMemo<AuthContextValue[\"signUp\"]>(() => {\n const h: HeadlessClient = client.headless;\n return {\n email: async (params) => {\n const result = await h.signUp.email(params);\n if (!result.requiresVerification) {\n setSession({\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n expiresAt: result.expiresAt,\n user: result.user,\n });\n }\n return result;\n },\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client]);\n\n const value = useMemo<AuthContextValue>(\n () => ({\n client,\n session,\n isLoaded,\n isSignedIn: !!session,\n login,\n logout,\n refreshSession,\n getAuthorizationHeader,\n authFetch,\n signIn,\n signUp,\n }),\n [client, session, isLoaded, login, logout, refreshSession, getAuthorizationHeader, authFetch, signIn, signUp]\n );\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n// ── Hooks ──────────────────────────────────────────────────────────────────────\n\nfunction useAuthContext(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth must be used inside <SmartHiveProvider>.\");\n return ctx;\n}\n\nexport function useAuth() { return useAuthContext(); }\nexport function useSession() { return useAuthContext().session; }\nexport function useUser() { return useAuthContext().session?.user ?? null; }\nexport function useIsLoaded() { return useAuthContext().isLoaded; }\n\nexport function useIsSignedIn() {\n const { isLoaded, isSignedIn } = useAuthContext();\n return isLoaded ? isSignedIn : null;\n}\n\nexport function useAuthFetch() {\n return useAuthContext().authFetch;\n}\n\nexport function useAuthorizationHeader() {\n return useAuthContext().getAuthorizationHeader;\n}\n\n// ── Render helpers ─────────────────────────────────────────────────────────────\n\nexport function SignedIn({ children }: { children: React.ReactNode }) {\n const { isLoaded, isSignedIn } = useAuthContext();\n if (!isLoaded || !isSignedIn) return null;\n return <>{children}</>;\n}\n\nexport function SignedOut({ children }: { children: React.ReactNode }) {\n const { isLoaded, isSignedIn } = useAuthContext();\n if (!isLoaded || isSignedIn) return null;\n return <>{children}</>;\n}\n\nexport function AuthLoading({ children }: { children: React.ReactNode }) {\n const { isLoaded } = useAuthContext();\n return isLoaded ? null : <>{children}</>;\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAC5B,YAAY,iBAAiB;AAC7B;AAAA,EACE;AAAA,OAQK;AAoQE,SAkCA,UAlCA;AAhQT,IAAM,gBAA6B;AAAA,EACjC,SAAS,CAAC,QAAoB,yBAAa,GAAG;AAAA,EAC9C,SAAS,CAAC,KAAK,UAAsB,yBAAa,KAAK,KAAK;AAAA,EAC5D,YAAY,CAAC,QAAoB,4BAAgB,GAAG;AACtD;AAEA,IAAM,cAAc;AAAA,EAClB,cAAc;AAAA,EACd,WAAW;AACb;AAaO,SAAS,iBAAiB,QAAgB,OAAO,iBAAyB;AAC/E,SAAO,GAAG,MAAM,MAAM,IAAI;AAC5B;AAEA,SAAS,iBAAiB,SAAiB;AACzC,SAAO,QAAQ,QAAQ,OAAO,EAAE;AAClC;AAQO,SAAS,aAAa,QAAkD;AAC7E,QAAM,OAAO,SAAS;AAAA,IACpB,GAAG;AAAA,IACH,SAAS;AAAA,IACT,kBAAkB;AAAA,EACpB,CAAwB;AAExB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,MAAM,SAAS;AACnB,YAAM,cAAc,SAAS,eAAe,OAAO;AACnD,YAAM,WAAW,iBAAiB,OAAO,cAAc,OAAO,OAAO;AACrE,YAAM,UAAU,IAAI,YAAY;AAAA,QAC9B,UAAU,OAAO;AAAA,QACjB;AAAA,QACA,cAAc;AAAA,QACd,SAAS;AAAA,QACT,OAAO,SAAS;AAAA,QAChB,aAAa;AAAA,UACX,YAAY,OAAO;AAAA,UACnB,iBAAiB,OAAO;AAAA,QAC1B;AAAA,MACF,CAAC;AACD,YAAM,MAAM,MAAM,QAAQ,iBAAiB;AAAA,QACzC,uBAAuB,GAAG,QAAQ;AAAA,MACpC,CAAC;AAED,UAAI,QAAQ,cAAc;AACxB,cAAM,cAAc,QAAQ,YAAY,cAAc,QAAQ,YAAY;AAAA,MAC5E;AACA,YAAM,cAAc,QAAQ,YAAY,WAAW,QAAQ,KAAK;AAChE,YAAM,QAAQ,QAAQ,GAAG;AAAA,IAC3B;AAAA,EACF;AACF;AAsCA,IAAM,cAAc,cAAuC,IAAI;AAcxD,SAAS,kBAAkB,EAAE,UAAU,GAAG,OAAO,GAA2B;AACjF,QAAM,YAAY,OAAO,MAAM;AAC/B,QAAM,SAAS,QAAQ,MAAM,aAAa,UAAU,OAAO,GAAG,CAAC,CAAC;AAEhE,QAAM,CAAC,SAAS,UAAU,IAAI,SAA6B,IAAI;AAC/D,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAG9C,YAAU,MAAM;AACd,QAAI,YAAY;AAChB,WACG,WAAW,EACX,KAAK,MAAM,OAAO,WAAW,CAAC,EAC9B,KAAK,CAAC,MAAM;AAAE,UAAI,CAAC,UAAW,YAAW,CAAC;AAAA,IAAG,CAAC,EAC9C,MAAM,MAAM;AAAA,IAAC,CAAC,EACd,QAAQ,MAAM;AAAE,UAAI,CAAC,UAAW,aAAY,IAAI;AAAA,IAAG,CAAC;AACvD,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAM;AAAA,EACnC,GAAG,CAAC,MAAM,CAAC;AAGX,YAAU,MAAM;AACd,aAAS,UAAU,EAAE,IAAI,GAAoB;AAC3C,aACG,eAAe,EAAE,IAAI,CAAC,EACtB,KAAK,CAAC,MAAM,WAAW,CAAC,CAAC,EACzB,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACnB;AAEA,UAAM,MAAM,QAAQ,iBAAiB,OAAO,SAAS;AAGrD,YAAQ,cAAc,EAAE,KAAK,CAAC,QAAQ;AACpC,UAAI,IAAK,WAAU,EAAE,IAAI,CAAC;AAAA,IAC5B,CAAC;AAED,WAAO,MAAM,IAAI,OAAO;AAAA,EAC1B,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ;AAAA,IACZ,CAAC,YAAuC,OAAO,MAAM,OAAO;AAAA,IAC5D,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,OAAO,OAAO;AACpB,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,iBAAiB,YAAY,YAAY;AAC7C,eAAW,MAAM,OAAO,eAAe,CAAC;AAAA,EAC1C,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,yBAAyB;AAAA,IAC7B,MAAM,OAAO,uBAAuB;AAAA,IACpC,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,YAAY;AAAA,IAChB,CAAC,OAA+B,SAAuB,OAAO,MAAM,OAAO,IAAI;AAAA,IAC/E,CAAC,MAAM;AAAA,EACT;AAIA,WAAS,mBACP,IACA;AACA,WAAO,OAAO,WAAkB;AAC9B,YAAM,SAAS,MAAM,GAAG,MAAM;AAC9B,iBAAW;AAAA,QACT,aAAa,OAAO;AAAA,QACpB,cAAc,OAAO;AAAA,QACrB,WAAW,OAAO;AAAA,QAClB,MAAM,OAAO;AAAA,MACf,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAAS,QAAoC,MAAM;AACvD,UAAM,IAAoB,OAAO;AACjC,WAAO;AAAA,MACL,OAAO,mBAAmB,EAAE,OAAO,MAAM,KAAK,EAAE,MAAM,CAAU;AAAA,MAChE,OAAO;AAAA,QACL,SAAS,CAAC,MAAM,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,QACxC,QAAQ,mBAAmB,EAAE,OAAO,MAAM,OAAO,KAAK,EAAE,OAAO,KAAK,CAAU;AAAA,MAChF;AAAA,MACA,UAAU;AAAA,QACR,MAAM,CAAC,MAAM,EAAE,OAAO,SAAS,KAAK,CAAC;AAAA,QACrC,QAAQ,mBAAmB,EAAE,OAAO,SAAS,OAAO,KAAK,EAAE,OAAO,QAAQ,CAAU;AAAA,MACtF;AAAA,MACA,WAAW;AAAA,QACT,MAAM,CAAC,MAAM,EAAE,OAAO,UAAU,KAAK,CAAC;AAAA,MACxC;AAAA,IACF;AAAA,EAEF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,SAAS,QAAoC,MAAM;AACvD,UAAM,IAAoB,OAAO;AACjC,WAAO;AAAA,MACL,OAAO,OAAO,WAAW;AACvB,cAAM,SAAS,MAAM,EAAE,OAAO,MAAM,MAAM;AAC1C,YAAI,CAAC,OAAO,sBAAsB;AAChC,qBAAW;AAAA,YACT,aAAa,OAAO;AAAA,YACpB,cAAc,OAAO;AAAA,YACrB,WAAW,OAAO;AAAA,YAClB,MAAM,OAAO;AAAA,UACf,CAAC;AAAA,QACH;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EAEF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY,CAAC,CAAC;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,SAAS,UAAU,OAAO,QAAQ,gBAAgB,wBAAwB,WAAW,QAAQ,MAAM;AAAA,EAC9G;AAEA,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAIA,SAAS,iBAAmC;AAC1C,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,kDAAkD;AAC5E,SAAO;AACT;AAEO,SAAS,UAAU;AAAE,SAAO,eAAe;AAAG;AAC9C,SAAS,aAAa;AAAE,SAAO,eAAe,EAAE;AAAS;AACzD,SAAS,UAAU;AAAE,SAAO,eAAe,EAAE,SAAS,QAAQ;AAAM;AACpE,SAAS,cAAc;AAAE,SAAO,eAAe,EAAE;AAAU;AAE3D,SAAS,gBAAgB;AAC9B,QAAM,EAAE,UAAU,WAAW,IAAI,eAAe;AAChD,SAAO,WAAW,aAAa;AACjC;AAEO,SAAS,eAAe;AAC7B,SAAO,eAAe,EAAE;AAC1B;AAEO,SAAS,yBAAyB;AACvC,SAAO,eAAe,EAAE;AAC1B;AAIO,SAAS,SAAS,EAAE,SAAS,GAAkC;AACpE,QAAM,EAAE,UAAU,WAAW,IAAI,eAAe;AAChD,MAAI,CAAC,YAAY,CAAC,WAAY,QAAO;AACrC,SAAO,gCAAG,UAAS;AACrB;AAEO,SAAS,UAAU,EAAE,SAAS,GAAkC;AACrE,QAAM,EAAE,UAAU,WAAW,IAAI,eAAe;AAChD,MAAI,CAAC,YAAY,WAAY,QAAO;AACpC,SAAO,gCAAG,UAAS;AACrB;AAEO,SAAS,YAAY,EAAE,SAAS,GAAkC;AACvE,QAAM,EAAE,SAAS,IAAI,eAAe;AACpC,SAAO,WAAW,OAAO,gCAAG,UAAS;AACvC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smarthivelabs-devs/auth-expo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "SmartHive Auth provider, hooks, and SecureStore integration for React Native / Expo",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@smarthivelabs-devs/auth-sdk": "^
|
|
28
|
+
"@smarthivelabs-devs/auth-sdk": "^1.1.0",
|
|
29
29
|
"expo-auth-session": ">=5",
|
|
30
30
|
"expo-secure-store": ">=12",
|
|
31
31
|
"react": ">=18",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@types/react-native": "^0.73.0",
|
|
37
37
|
"tsup": "^8.3.0",
|
|
38
38
|
"typescript": "^5.7.3",
|
|
39
|
-
"@smarthivelabs-devs/auth-sdk": "^
|
|
39
|
+
"@smarthivelabs-devs/auth-sdk": "^1.1.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsup",
|