@smarthivelabs-devs/auth-expo 1.0.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 CHANGED
@@ -1,6 +1,13 @@
1
1
  # @smarthivelabs-devs/auth-expo
2
2
 
3
- SmartHive Auth for React Native and Expo. Provides a provider, hooks, and components with SecureStore-backed token storage and deep-link OAuth support.
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 |
4
11
 
5
12
  ---
6
13
 
@@ -10,303 +17,316 @@ SmartHive Auth for React Native and Expo. Provides a provider, hooks, and compon
10
17
  npx expo install @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
11
18
  ```
12
19
 
13
- Or with npm/pnpm (non-Expo managed workflow):
14
-
15
20
  ```bash
21
+ # npm / pnpm
16
22
  npm install @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
17
23
  pnpm add @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
18
24
  ```
19
25
 
20
- > **Peer dependencies required:** `expo-auth-session>=5`, `expo-secure-store>=12`, `react>=18`, `react-native>=0.73`
21
-
22
- ---
23
-
24
- ## Prerequisites
25
-
26
- 1. A SmartHive Auth project — grab `projectId`, `publishableKey`, and `baseUrl` from your dashboard.
27
- 2. A registered deep link scheme for your Expo app (see below).
28
-
29
- ---
30
-
31
- ## Deep Link Setup
32
-
33
- SmartHive Auth uses OAuth 2.0 + PKCE. After login, the server redirects back to your app via a deep link (e.g. `myapp://auth/callback`). You must register a custom scheme in `app.json`:
34
-
35
- ```json
36
- // app.json
37
- {
38
- "expo": {
39
- "scheme": "myapp",
40
- "android": {
41
- "intentFilters": [
42
- {
43
- "action": "VIEW",
44
- "autoVerify": true,
45
- "data": [{ "scheme": "myapp" }],
46
- "category": ["BROWSABLE", "DEFAULT"]
47
- }
48
- ]
49
- }
50
- }
51
- }
52
- ```
53
-
54
- Rebuild your native app after this change:
55
-
56
- ```bash
57
- npx expo prebuild
58
- ```
26
+ > **Peer dependencies:** `expo-auth-session>=5`, `expo-secure-store>=12`, `react>=18`, `react-native>=0.73`
59
27
 
60
28
  ---
61
29
 
62
30
  ## Setup
63
31
 
64
- Wrap your root component with `SmartHiveProvider`. Use `buildRedirectUri` to construct the deep link URI from your scheme.
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.
65
33
 
66
34
  ```tsx
67
- // App.tsx
35
+ // app/_layout.tsx
68
36
  import { SmartHiveProvider, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
69
37
 
70
- const REDIRECT_URI = buildRedirectUri("myapp"); // → "myapp://auth/callback"
71
-
72
- export default function App() {
38
+ export default function Layout() {
73
39
  return (
74
40
  <SmartHiveProvider
75
41
  projectId={process.env.EXPO_PUBLIC_AUTH_PROJECT_ID!}
76
42
  publishableKey={process.env.EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY!}
77
43
  baseUrl={process.env.EXPO_PUBLIC_AUTH_BASE_URL!}
78
- redirectUri={REDIRECT_URI}
44
+ redirectUri={buildRedirectUri("myapp")}
79
45
  >
80
- <RootNavigator />
46
+ <Stack />
81
47
  </SmartHiveProvider>
82
48
  );
83
49
  }
84
50
  ```
85
51
 
86
- ### `buildRedirectUri(scheme, path?)`
87
-
88
- Helper that constructs a deep link URI:
89
-
90
- ```ts
91
- buildRedirectUri("myapp") // → "myapp://auth/callback"
92
- buildRedirectUri("myapp", "auth/done") // → "myapp://auth/done"
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
93
57
  ```
94
58
 
95
59
  ---
96
60
 
97
- ## Provider Props
98
-
99
- | Prop | Type | Required | Description |
100
- |---|---|---|---|
101
- | `projectId` | `string` | Yes | Your SmartHive project ID |
102
- | `publishableKey` | `string` | Yes | Your publishable key |
103
- | `baseUrl` | `string` | Yes | URL of your SmartHive Auth service |
104
- | `redirectUri` | `string` | Yes | Deep link callback URI (e.g. `myapp://auth/callback`) |
105
- | `authDomain` | `string` | No | Custom branded auth domain |
106
- | `children` | `ReactNode` | Yes | Your app tree |
107
-
108
- ---
109
-
110
- ## How OAuth Works in Expo
111
-
112
- 1. User taps **Sign in** → `login()` is called
113
- 2. The SDK builds the OAuth URL with PKCE, saves the verifier in SecureStore, and opens the URL in the system browser via `Linking.openURL()`
114
- 3. The user logs in via the browser
115
- 4. The server redirects to `myapp://auth/callback?code=...&state=...`
116
- 5. `Linking` fires a `url` event — `SmartHiveProvider` handles it automatically, exchanges the code for tokens, and updates the session state
61
+ ## Headless Sign-in (Custom Login Screen)
117
62
 
118
- No manual callback handling is needed — the provider sets up the `Linking` event listener internally.
63
+ No browser, no redirect. Call the method, get tokens. Full control of your UI.
119
64
 
120
- ---
121
-
122
- ## Hooks
123
-
124
- ### `useAuth()`
125
-
126
- Returns the full auth context.
65
+ ### Email + Password
127
66
 
128
67
  ```tsx
129
68
  import { useAuth } from "@smarthivelabs-devs/auth-expo";
69
+ import { useState } from "react";
70
+ import { Button, TextInput, View, Text } from "react-native";
130
71
 
131
- function HomeScreen() {
132
- const {
133
- session, // AuthSession | null
134
- isLoaded, // true once initial SecureStore read is done
135
- isSignedIn, // boolean
136
- login, // (options?) => Promise<void>
137
- logout, // () => Promise<void>
138
- refreshSession, // () => Promise<void>
139
- authFetch, // authenticated fetch wrapper
140
- } = useAuth();
141
-
142
- if (!isLoaded) return <ActivityIndicator />;
143
-
144
- return isSignedIn ? (
145
- <Button title="Sign out" onPress={logout} />
146
- ) : (
147
- <Button title="Sign in" onPress={() => login()} />
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>
148
94
  );
149
95
  }
150
96
  ```
151
97
 
152
- ---
98
+ ### Phone OTP
153
99
 
154
- ### `useSession()`
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
+ }
155
115
 
156
- Returns the current `AuthSession` or `null`.
116
+ async function verifyOtp() {
117
+ await signIn.phone.verify({ phoneNumber: phone, code });
118
+ // Signed in — session saved automatically
119
+ }
157
120
 
158
- ```tsx
159
- import { useSession } from "@smarthivelabs-devs/auth-expo";
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
+ }
160
129
 
161
- function TokenScreen() {
162
- const session = useSession();
163
- if (!session) return <Text>Not signed in</Text>;
164
- return <Text selectable>{session.accessToken}</Text>;
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
+ );
165
136
  }
166
137
  ```
167
138
 
168
- ---
139
+ ### Email OTP
169
140
 
170
- ### `useUser()`
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
+ ```
171
150
 
172
- Returns the user object from the session, or `null`.
151
+ ### Magic Link
173
152
 
174
153
  ```tsx
175
- import { useUser } from "@smarthivelabs-devs/auth-expo";
154
+ const { signIn } = useAuth();
176
155
 
177
- function ProfileScreen() {
178
- const user = useUser();
179
- if (!user) return null;
180
- return <Text>Welcome, {(user as any).email}</Text>;
181
- }
156
+ // Sends an email — user clicks the link to sign in (no token returned here)
157
+ await signIn.magicLink.send({ email: "user@example.com" });
182
158
  ```
183
159
 
184
160
  ---
185
161
 
186
- ### `useIsLoaded()`
187
-
188
- Returns `true` once the initial SecureStore session check is done. Prevents a flash of the signed-out state on app startup.
162
+ ## Headless Sign-up
189
163
 
190
164
  ```tsx
191
- import { useIsLoaded } from "@smarthivelabs-devs/auth-expo";
165
+ import { useAuth } from "@smarthivelabs-devs/auth-expo";
192
166
 
193
- function RootNavigator() {
194
- const isLoaded = useIsLoaded();
195
- if (!isLoaded) return <SplashScreen />;
196
- return <AppNavigator />;
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
197
180
  }
198
181
  ```
199
182
 
200
183
  ---
201
184
 
202
- ### `useIsSignedIn()`
185
+ ## OAuth Redirect Sign-in (Browser)
203
186
 
204
- Returns `true` (signed in), `false` (signed out), or `null` (still loading).
187
+ The original flow is completely unchanged. Use it for social login or when you want the hosted login page.
205
188
 
206
189
  ```tsx
207
- import { useIsSignedIn } from "@smarthivelabs-devs/auth-expo";
190
+ import { useAuth } from "@smarthivelabs-devs/auth-expo";
191
+ import { Button } from "react-native";
208
192
 
209
- function AuthGate({ children }: { children: React.ReactNode }) {
210
- const isSignedIn = useIsSignedIn();
211
- const router = useRouter();
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)
212
200
 
213
- useEffect(() => {
214
- if (isSignedIn === false) router.replace("/login");
215
- }, [isSignedIn]);
201
+ If you use `login()`, register a custom scheme in `app.json` so the browser can redirect back:
216
202
 
217
- if (isSignedIn === null) return <ActivityIndicator />;
218
- if (!isSignedIn) return null;
219
- return <>{children}</>;
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
+ }
220
218
  }
221
219
  ```
222
220
 
223
- ---
221
+ Rebuild after changing `app.json`:
224
222
 
225
- ### `useAuthFetch()`
223
+ ```bash
224
+ npx expo prebuild
225
+ ```
226
226
 
227
- Returns an authenticated `fetch` function. Automatically injects the Bearer token on every request — tokens are refreshed transparently.
227
+ No extra setup needed for headless sign-in it works without a deep link.
228
228
 
229
- ```tsx
230
- import { useAuthFetch } from "@smarthivelabs-devs/auth-expo";
229
+ ---
231
230
 
232
- function DataScreen() {
233
- const authFetch = useAuthFetch();
234
- const [data, setData] = useState(null);
231
+ ## Sign-out
235
232
 
236
- async function load() {
237
- const res = await authFetch("https://api.myapp.com/protected");
238
- setData(await res.json());
239
- }
233
+ ```tsx
234
+ const { logout } = useAuth();
240
235
 
241
- return (
242
- <View>
243
- <Button title="Load data" onPress={load} />
244
- {data && <Text>{JSON.stringify(data)}</Text>}
245
- </View>
246
- );
247
- }
236
+ // Clears SecureStore + invalidates session on the server
237
+ await logout();
248
238
  ```
249
239
 
250
240
  ---
251
241
 
252
- ### `useAuthorizationHeader()`
242
+ ## Hooks
253
243
 
254
- Returns a function that resolves to `{ authorization: "Bearer <token>" }`. Useful for passing to SDKs or GraphQL clients.
244
+ ### `useAuth()`
245
+
246
+ Returns the full auth context.
255
247
 
256
248
  ```tsx
257
- import { useAuthorizationHeader } from "@smarthivelabs-devs/auth-expo";
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
+ ```
258
262
 
259
- function ApolloSetup() {
260
- const getAuthorizationHeader = useAuthorizationHeader();
263
+ ### `useSession()`
261
264
 
262
- const authLink = new ApolloLink(async (operation, forward) => {
263
- const headers = await getAuthorizationHeader();
264
- operation.setContext({ headers });
265
- return forward(operation);
266
- });
265
+ ```tsx
266
+ const session = useSession(); // AuthSession | null
267
+ // session.accessToken, session.refreshToken, session.expiresAt, session.user
268
+ ```
267
269
 
268
- // ...
269
- }
270
+ ### `useUser()`
271
+
272
+ ```tsx
273
+ const user = useUser(); // unknown | null
270
274
  ```
271
275
 
272
- ---
276
+ ### `useIsLoaded()`
273
277
 
274
- ## Render Helpers
278
+ `true` once the initial SecureStore check is complete. Use this to avoid a flash of unauthenticated state on startup.
275
279
 
276
- ### `<SignedIn>`
280
+ ```tsx
281
+ const isLoaded = useIsLoaded();
282
+ if (!isLoaded) return <SplashScreen />;
283
+ ```
277
284
 
278
- Renders children only when auth has loaded **and** a session exists.
285
+ ### `useIsSignedIn()`
286
+
287
+ Returns `true` (signed in), `false` (signed out), or `null` (still loading).
279
288
 
280
289
  ```tsx
281
- import { SignedIn } from "@smarthivelabs-devs/auth-expo";
290
+ const isSignedIn = useIsSignedIn();
282
291
 
283
- <SignedIn>
284
- <UserDashboard />
285
- </SignedIn>
292
+ useEffect(() => {
293
+ if (isSignedIn === false) router.replace("/login");
294
+ }, [isSignedIn]);
286
295
  ```
287
296
 
288
- ### `<SignedOut>`
297
+ ### `useAuthFetch()`
289
298
 
290
- Renders children only when auth has loaded **and** there is no session.
299
+ Authenticated `fetch` wrapper. Bearer token is injected automatically and refreshed transparently when near expiry.
291
300
 
292
301
  ```tsx
293
- import { SignedOut } from "@smarthivelabs-devs/auth-expo";
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.
294
309
 
295
- <SignedOut>
296
- <LandingScreen />
297
- </SignedOut>
310
+ ```tsx
311
+ const getAuthorizationHeader = useAuthorizationHeader();
312
+ const headers = await getAuthorizationHeader();
298
313
  ```
299
314
 
300
- ### `<AuthLoading>`
315
+ ---
301
316
 
302
- Renders children while the initial session check is running.
317
+ ## Render Helpers
303
318
 
304
319
  ```tsx
305
- import { AuthLoading } from "@smarthivelabs-devs/auth-expo";
320
+ import { SignedIn, SignedOut, AuthLoading } from "@smarthivelabs-devs/auth-expo";
306
321
 
307
- <AuthLoading>
308
- <ActivityIndicator size="large" />
309
- </AuthLoading>
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>
310
330
  ```
311
331
 
312
332
  ---
@@ -315,11 +335,11 @@ import { AuthLoading } from "@smarthivelabs-devs/auth-expo";
315
335
 
316
336
  ```
317
337
  app/
318
- ├── _layout.tsx ← SmartHiveProvider here
319
- ├── index.tsx ← SignedIn / SignedOut
320
- ├── login.tsx ← login screen
338
+ ├── _layout.tsx ← SmartHiveProvider here
339
+ ├── index.tsx ← SignedIn / SignedOut routing
340
+ ├── login.tsx your custom login screen using signIn.*
321
341
  └── (protected)/
322
- └── dashboard.tsx ← authenticated screen
342
+ └── dashboard.tsx
323
343
  ```
324
344
 
325
345
  ```tsx
@@ -359,85 +379,70 @@ export default function Index() {
359
379
  ```
360
380
 
361
381
  ```tsx
362
- // app/login.tsx
382
+ // app/login.tsx — custom screen, no browser redirect
363
383
  import { useAuth } from "@smarthivelabs-devs/auth-expo";
364
- import { Button, View } from "react-native";
384
+ import { useState } from "react";
385
+ import { Button, TextInput, View, Text, StyleSheet } from "react-native";
365
386
 
366
387
  export default function LoginScreen() {
367
- const { login } = useAuth();
368
- return (
369
- <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
370
- <Button title="Sign in with SmartHive" onPress={() => login()} />
371
- </View>
372
- );
373
- }
374
- ```
375
-
376
- ```tsx
377
- // app/(protected)/dashboard.tsx
378
- import { useAuth, SignedIn } from "@smarthivelabs-devs/auth-expo";
379
- import { Button, Text, View } from "react-native";
380
-
381
- export default function Dashboard() {
382
- const { logout, authFetch } = useAuth();
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
+ }
383
405
 
384
406
  return (
385
- <SignedIn>
386
- <View>
387
- <Text>Dashboard</Text>
388
- <Button title="Sign out" onPress={logout} />
389
- </View>
390
- </SignedIn>
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>
391
413
  );
392
414
  }
393
- ```
394
-
395
- ---
396
-
397
- ## Low-level: `initExpoAuth`
398
-
399
- If you need the client directly without the provider (advanced use), use `initExpoAuth`:
400
-
401
- ```ts
402
- import { initExpoAuth, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
403
415
 
404
- const client = initExpoAuth({
405
- projectId: "proj_abc123",
406
- publishableKey: "pk_live_abc123",
407
- baseUrl: "https://auth.myapp.com",
408
- redirectUri: buildRedirectUri("myapp"),
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 },
409
420
  });
410
-
411
- await client.initialize();
412
-
413
- // The client behaves the same as SmartHiveAuthClient from auth-sdk,
414
- // but uses SecureStore and Linking instead of localStorage + location.assign
415
421
  ```
416
422
 
417
423
  ---
418
424
 
419
- ## Environment Variables
425
+ ## Token Storage
420
426
 
421
- With Expo's built-in env support (SDK 49+):
427
+ All tokens are stored in `expo-secure-store`:
422
428
 
423
- ```bash
424
- # .env
425
- EXPO_PUBLIC_AUTH_PROJECT_ID=proj_abc123
426
- EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY=pk_live_abc123
427
- EXPO_PUBLIC_AUTH_BASE_URL=https://auth.myapp.com
428
- ```
429
+ - **iOS**: Keychain Services
430
+ - **Android**: Android Keystore (AES encryption)
429
431
 
430
- > Variables prefixed `EXPO_PUBLIC_` are embedded in the app bundle at build time.
432
+ PKCE verifier and state (used during OAuth redirect flow) are also stored in SecureStore and deleted after the code exchange completes.
431
433
 
432
434
  ---
433
435
 
434
- ## Token Storage
435
-
436
- Tokens are stored in `expo-secure-store`, which uses:
437
- - **iOS**: Keychain Services
438
- - **Android**: Keystore System (Android Keystore-backed AES encryption)
436
+ ## Provider Props
439
437
 
440
- PKCE verifier and state are also stored in SecureStore during the login flow and deleted after the code exchange.
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 |
441
446
 
442
447
  ---
443
448
 
@@ -451,12 +456,40 @@ import type {
451
456
 
452
457
  import type {
453
458
  AuthSession,
459
+ HeadlessClient,
460
+ HeadlessSignInResult,
461
+ HeadlessSignUpResult,
454
462
  SmartHiveAuthClient,
455
463
  } from "@smarthivelabs-devs/auth-sdk";
456
464
  ```
457
465
 
458
466
  ---
459
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
+
460
493
  ## Related Packages
461
494
 
462
495
  | Package | Use case |
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. OAuth is handled via deep links.
37
- *
38
- * @example
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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smarthivelabs-devs/auth-expo",
3
- "version": "1.0.0",
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": "^1.0.0",
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": "^1.0.0"
39
+ "@smarthivelabs-devs/auth-sdk": "^1.1.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsup",