@smarthivelabs-devs/auth-expo
v1.5.0
Published
SmartHive Auth provider, hooks, and SecureStore integration for React Native / Expo
Readme
@smarthivelabs-devs/auth-expo
SmartHive Auth for React Native and Expo. Provides a provider, hooks, and components with SecureStore-backed token storage.
Supports two sign-in modes — both in the same package, zero config difference:
| Mode | How it works | Good for |
|---|---|---|
| Headless | Call signIn.* directly — no browser, custom UI | Native mobile apps with branded login screens |
| OAuth redirect | login() opens system browser, deep-links back | Social login, SSO, or when you want the hosted UI |
Installation
npx expo install @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store# npm / pnpm
npm install @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-store
pnpm add @smarthivelabs-devs/auth-expo @smarthivelabs-devs/auth-sdk expo-auth-session expo-secure-storePeer dependencies:
expo-auth-session>=5,expo-secure-store>=12,react>=18,react-native>=0.73
Setup
Wrap your root component with SmartHiveAuthProvider. The redirectUri is only needed for the OAuth redirect flow — you can still set it even if you only use headless.
// app/_layout.tsx
import { SmartHiveAuthProvider, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
export default function Layout() {
return (
<SmartHiveAuthProvider
projectId={process.env.EXPO_PUBLIC_AUTH_PROJECT_ID!}
publishableKey={process.env.EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY!}
baseUrl={process.env.EXPO_PUBLIC_AUTH_BASE_URL!}
redirectUri={buildRedirectUri("myapp")}
>
<Stack />
</SmartHiveAuthProvider>
);
}# .env
EXPO_PUBLIC_AUTH_PROJECT_ID=proj_abc123
EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY=pk_prod_abc123
EXPO_PUBLIC_AUTH_BASE_URL=https://auth.myapp.comHeadless Sign-in (Custom Login Screen)
No browser, no redirect. Call the method, get tokens. Full control of your UI.
Email + Password
import { useAuth } from "@smarthivelabs-devs/auth-expo";
import { useState } from "react";
import { Button, TextInput, View, Text } from "react-native";
export default function LoginScreen() {
const { signIn } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
async function handleSignIn() {
try {
await signIn.email({ email, password });
// Session is saved automatically — user is now signed in
} catch (e: any) {
setError(e.message);
}
}
return (
<View>
<TextInput value={email} onChangeText={setEmail} placeholder="Email" />
<TextInput value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry />
{error ? <Text>{error}</Text> : null}
<Button title="Sign in" onPress={handleSignIn} />
</View>
);
}Phone OTP
import { useAuth } from "@smarthivelabs-devs/auth-expo";
import { useState } from "react";
import { Button, TextInput, View } from "react-native";
export default function PhoneLoginScreen() {
const { signIn } = useAuth();
const [phone, setPhone] = useState("");
const [code, setCode] = useState("");
const [step, setStep] = useState<"phone" | "code">("phone");
async function sendOtp() {
await signIn.phone.sendOtp({ phoneNumber: phone });
setStep("code");
}
async function verifyOtp() {
await signIn.phone.verify({ phoneNumber: phone, code });
// Signed in — session saved automatically
}
if (step === "phone") {
return (
<View>
<TextInput value={phone} onChangeText={setPhone} placeholder="+1234567890" keyboardType="phone-pad" />
<Button title="Send code" onPress={sendOtp} />
</View>
);
}
return (
<View>
<TextInput value={code} onChangeText={setCode} placeholder="Enter code" keyboardType="number-pad" />
<Button title="Verify" onPress={verifyOtp} />
</View>
);
}Email OTP
const { signIn } = useAuth();
// Step 1 — send the code
await signIn.emailOtp.send({ email: "[email protected]" });
// Step 2 — verify (returns session, user is now signed in)
await signIn.emailOtp.verify({ email: "[email protected]", code: "123456" });Magic Link
const { signIn } = useAuth();
// Sends an email — user clicks the link to sign in (no token returned here)
await signIn.magicLink.send({ email: "[email protected]" });Headless Sign-up
import { useAuth } from "@smarthivelabs-devs/auth-expo";
const { signUp } = useAuth();
const result = await signUp.email({
email: "[email protected]",
password: "secret123",
name: "Jane Doe", // optional
});
if (result.requiresVerification) {
// Email verification required — show "check your inbox" screen
// No session yet, tokens are empty
} else {
// Account created and signed in immediately
}Resend verification email
If the user did not receive the initial verification email — or the link expired — call signUp.resendVerificationEmail to trigger a fresh one:
import { useAuth } from "@smarthivelabs-devs/auth-expo";
const { signUp } = useAuth();
// Called from a "Didn't receive the email? Resend" button
await signUp.resendVerificationEmail({ email: "[email protected]" });Throws SmartHiveAuthError with code resend_failed if the server rejects the request.
Social OAuth Sign-in (Google, Apple, GitHub, etc.)
Each social provider uses your project's own credentials — the consent screen shows your app name. Configure credentials in your SmartHive dashboard under Project → OAuth Providers, then call:
import { useAuth } from "@smarthivelabs-devs/auth-expo";
import { Button } from "react-native";
export default function LoginScreen() {
const { signIn } = useAuth();
return (
<>
<Button title="Continue with Google" onPress={() => signIn.social("google")} />
<Button title="Continue with Apple" onPress={() => signIn.social("apple")} />
<Button title="Continue with GitHub" onPress={() => signIn.social("github")} />
</>
);
}signIn.social() calls Linking.openURL() to open the provider's consent screen in the system browser. When the user approves, the provider redirects back to your app's redirectUri deep link with ?access_token=...&refresh_token=.... The SmartHiveAuthProvider deep link listener picks this up automatically and saves the session — no extra setup needed.
Supported providers:
google · apple · github · facebook · twitter · linkedin · microsoft · discord · spotify · twitch · reddit · gitlab · slack · notion · zoom · figma
Deep link required — make sure
redirectUriis set to your app scheme (e.g.myapp://auth/callback) and your scheme is registered inapp.json. See the Deep Link Setup section below.
OAuth Redirect Sign-in (SmartHive hosted page)
The original PKCE flow — redirects to the SmartHive hosted login page and back. Use it for SSO or when you want the hosted login UI.
import { useAuth } from "@smarthivelabs-devs/auth-expo";
import { Button } from "react-native";
export default function LoginScreen() {
const { login } = useAuth();
return <Button title="Sign in with SmartHive" onPress={() => login()} />;
}Deep Link Setup (required for OAuth redirect only)
If you use login(), register a custom scheme in app.json so the browser can redirect back:
{
"expo": {
"scheme": "myapp",
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [{ "scheme": "myapp" }],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}Rebuild after changing app.json:
npx expo prebuildNo extra setup needed for headless sign-in — it works without a deep link.
Sign-out
const { logout } = useAuth();
// Clears SecureStore + invalidates session on the server
await logout();Hooks
useAuth()
Returns the full auth context.
const {
session, // AuthSession | null
isLoaded, // true once initial SecureStore read is done
isSignedIn, // boolean
login, // OAuth redirect sign-in (PKCE, SmartHive hosted page)
logout, // sign out
signIn, // headless + social sign-in methods
signUp, // headless sign-up (email, resendVerificationEmail)
refreshSession, // force a token refresh
authFetch, // authenticated fetch wrapper
getAuthorizationHeader, // () => Promise<{ authorization: string }>
} = useAuth();
// Social sign-in is under signIn.social:
await signIn.social("google");
await signIn.social("apple");
await signIn.social("github");useSession()
const session = useSession(); // AuthSession | null
// session.accessToken, session.refreshToken, session.expiresAt, session.useruseUser()
const user = useUser(); // unknown | nulluseIsLoaded()
true once the initial SecureStore check is complete. Use this to avoid a flash of unauthenticated state on startup.
const isLoaded = useIsLoaded();
if (!isLoaded) return <SplashScreen />;useIsSignedIn()
Returns true (signed in), false (signed out), or null (still loading).
const isSignedIn = useIsSignedIn();
useEffect(() => {
if (isSignedIn === false) router.replace("/login");
}, [isSignedIn]);useAuthFetch()
Authenticated fetch wrapper. Bearer token is injected automatically and refreshed transparently when near expiry.
const authFetch = useAuthFetch();
const res = await authFetch("https://api.myapp.com/protected");useAuthorizationHeader()
Resolves to { authorization: "Bearer <token>" }. Useful for GraphQL clients or custom SDK setup.
const getAuthorizationHeader = useAuthorizationHeader();
const headers = await getAuthorizationHeader();Render Helpers
import { SignedIn, SignedOut, AuthLoading } from "@smarthivelabs-devs/auth-expo";
// Shown only when loaded + authenticated
<SignedIn><Dashboard /></SignedIn>
// Shown only when loaded + not authenticated
<SignedOut><LoginScreen /></SignedOut>
// Shown while the initial SecureStore check is running
<AuthLoading><ActivityIndicator /></AuthLoading>Expo Router Integration
app/
├── _layout.tsx ← SmartHiveAuthProvider here
├── index.tsx ← SignedIn / SignedOut routing
├── login.tsx ← your custom login screen using signIn.*
└── (protected)/
└── dashboard.tsx// app/_layout.tsx
import { Stack } from "expo-router";
import { SmartHiveAuthProvider, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
export default function Layout() {
return (
<SmartHiveAuthProvider
projectId={process.env.EXPO_PUBLIC_AUTH_PROJECT_ID!}
publishableKey={process.env.EXPO_PUBLIC_AUTH_PUBLISHABLE_KEY!}
baseUrl={process.env.EXPO_PUBLIC_AUTH_BASE_URL!}
redirectUri={buildRedirectUri("myapp")}
>
<Stack />
</SmartHiveAuthProvider>
);
}// app/index.tsx
import { SignedIn, SignedOut, AuthLoading } from "@smarthivelabs-devs/auth-expo";
import { Redirect } from "expo-router";
import { ActivityIndicator } from "react-native";
export default function Index() {
return (
<>
<AuthLoading><ActivityIndicator /></AuthLoading>
<SignedIn><Redirect href="/dashboard" /></SignedIn>
<SignedOut><Redirect href="/login" /></SignedOut>
</>
);
}// app/login.tsx — custom screen, no browser redirect
import { useAuth } from "@smarthivelabs-devs/auth-expo";
import { useState } from "react";
import { Button, TextInput, View, Text, StyleSheet } from "react-native";
export default function LoginScreen() {
const { signIn } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function handleSignIn() {
setLoading(true);
setError("");
try {
await signIn.email({ email, password });
} catch (e: any) {
setError(e.message ?? "Sign in failed.");
} finally {
setLoading(false);
}
}
return (
<View style={styles.container}>
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="Email" autoCapitalize="none" />
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry />
{error ? <Text style={styles.error}>{error}</Text> : null}
<Button title={loading ? "Signing in…" : "Sign in"} onPress={handleSignIn} disabled={loading} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24 },
input: { borderWidth: 1, borderColor: "#ccc", borderRadius: 8, padding: 12, marginBottom: 12 },
error: { color: "red", marginBottom: 12 },
});Token Storage
All tokens are stored in expo-secure-store:
- iOS: Keychain Services
- Android: Android Keystore (AES encryption)
PKCE verifier and state (used during OAuth redirect flow) are also stored in SecureStore and deleted after the code exchange completes.
Provider Props
| Prop | Type | Required | Description |
|---|---|---|---|
| projectId | string | Yes | Your SmartHive project ID |
| publishableKey | string | Yes | Your publishable key (pk_prod_*) |
| baseUrl | string | Yes | URL of your SmartHive Auth service |
| redirectUri | string | Yes | Deep link callback URI — used only for OAuth redirect flow |
| authDomain | string | No | Custom branded auth domain |
| children | ReactNode | Yes | Your app tree |
TypeScript Types
import type {
SmartHiveExpoConfig,
SmartHiveAuthProviderProps,
} from "@smarthivelabs-devs/auth-expo";
import type {
AuthSession,
HeadlessClient,
HeadlessSignInResult,
HeadlessSignUpResult,
SmartHiveAuthClient,
} from "@smarthivelabs-devs/auth-sdk";Low-level: initExpoAuth
Direct client without the provider (advanced use):
import { initExpoAuth, buildRedirectUri } from "@smarthivelabs-devs/auth-expo";
const client = initExpoAuth({
projectId: "proj_abc123",
publishableKey: "pk_prod_abc123",
baseUrl: "https://auth.myapp.com",
redirectUri: buildRedirectUri("myapp"),
});
await client.initialize();
// Headless sign-in
const session = await client.headless.signIn.email({ email, password });
// OAuth redirect
await client.login();Social Auth Proxy (White-label Domain)
By default, when a user taps "Sign in with Google", the iOS system prompt shows:
"YourApp wants to use authcore.smarthivelabs.dev to sign in"
And the Google consent screen shows:
"to continue to smarthivelabs.dev"
To show your own domain on both prompts, add two thin proxy routes to your backend and set socialProxyUrl in the provider config. SmartHive Auth does all the OAuth work — your domain just acts as the visible entry point.
Step 1 — Add proxy routes to your backend
// src/routes/socialProxyRoutes.ts
import express from "express";
const router = express.Router();
const SMARTHIVE_BASE = process.env.SMARTHIVE_AUTH_BASE_URL!; // https://authcore.smarthivelabs.dev
const SMARTHIVE_ENV = process.env.SMARTHIVE_AUTH_ENVIRONMENT!; // prod
const SMARTHIVE_PID = process.env.SMARTHIVE_PROJECT_ID!;
const APP_BASE_URL = process.env.APP_BASE_URL!; // https://api.yourapp.com
// Initiation — app calls this, proxy forwards to SmartHive
router.get("/:provider", async (req, res) => {
const target = new URL(`${SMARTHIVE_BASE}/${SMARTHIVE_ENV}/api/auth/social/${req.params.provider}`);
target.searchParams.set("project_id", SMARTHIVE_PID);
if (req.query.redirect_uri) target.searchParams.set("redirect_uri", req.query.redirect_uri as string);
target.searchParams.set("proxy_callback", `${APP_BASE_URL}/api/auth/social/${req.params.provider}/callback`);
res.redirect(target.toString());
});
// Callback — Google/Apple calls this, proxy forwards to SmartHive
router.get("/:provider/callback", async (req, res) => {
const target = new URL(`${SMARTHIVE_BASE}/${SMARTHIVE_ENV}/api/auth/social/${req.params.provider}/callback`);
for (const [k, v] of Object.entries(req.query)) target.searchParams.set(k, v as string);
res.redirect(target.toString());
});
app.use("/api/auth/social", router);Required env vars on your backend:
SMARTHIVE_AUTH_BASE_URL=https://authcore.smarthivelabs.dev
SMARTHIVE_AUTH_ENVIRONMENT=prod
APP_BASE_URL=https://api.yourapp.comStep 2 — Register your callback URL with each provider
In your Google Cloud Console (or Apple/GitHub/etc. developer console), register:
https://api.yourapp.com/api/auth/social/google/callbackinstead of the SmartHive URL. Replace google with each provider you support.
Step 3 — Set socialProxyUrl in your app
<SmartHiveAuthProvider
publishableKey="pk_prod_..."
projectId="your-project-id"
baseUrl="https://authcore.smarthivelabs.dev"
redirectUri="myapp://auth/callback"
socialProxyUrl="https://api.yourapp.com" // ← add this
>
<App />
</SmartHiveAuthProvider>That's it. signIn.social("google") now opens api.yourapp.com/api/auth/social/google, iOS shows your domain, and Google shows "to continue to yourapp.com".
Related Packages
| Package | Use case |
|---|---|
| @smarthivelabs-devs/auth-sdk | Core SDK — framework-agnostic |
| @smarthivelabs-devs/auth-react | React web apps |
| @smarthivelabs-devs/auth-server | Express / Next.js server-side JWT verification |
License
MIT © SmartHive Labs
