@ridwan-retainer/auth-lite
v0.1.1
Published
Lightweight identity layer with anonymous mode and Auth0 integration
Downloads
168
Maintainers
Readme
@ridwan-retainer/auth-lite
A lightweight, production-ready authentication library for React Native applications with support for anonymous users and Auth0 integration. Built with TypeScript for type safety and developer experience.
📋 Table of Contents
- Features
- Installation
- Quick Start
- Core Concepts
- API Reference
- Advanced Usage
- Migration Guide
- Best Practices
- Troubleshooting
- Contributing
✨ Features
- 🆔 Anonymous Authentication: Automatically generate and persist anonymous user IDs for instant onboarding
- 🔐 Auth0 Integration: Full OAuth 2.0 / OIDC support with social login (Google, Apple, Facebook)
- 🔒 Secure Storage: Credentials stored securely using Expo SecureStore
- ♻️ Token Management: Automatic token refresh and validation
- 👤 User Profiles: Built-in user profile management and retrieval
- 🗑️ Account Deletion: Complete account deletion flow with confirmations
- 🔄 Migration Support: Seamless anonymous-to-authenticated account linking
- 📱 React Hooks: Easy-to-use hooks for all authentication flows
- 📝 TypeScript: Full type definitions for excellent IDE support
- ✅ Well-tested: Comprehensive test coverage
📦 Installation
npm install @ridwan-retainer/auth-litePeer Dependencies
This package requires the following peer dependencies:
npm install @react-native-async-storage/async-storage \
expo-auth-session \
expo-crypto \
expo-secure-store \
expo-web-browser \
react \
react-nativePlatform-Specific Setup
iOS (Expo)
Add to your app.json:
{
"expo": {
"ios": {
"bundleIdentifier": "com.yourapp.id"
},
"scheme": "yourappscheme"
}
}Android (Expo)
Add to your app.json:
{
"expo": {
"android": {
"package": "com.yourapp.id"
},
"scheme": "yourappscheme"
}
}🚀 Quick Start
1. Configure Auth0
import { initializeAuth0 } from '@ridwan-retainer/auth-lite';
// Initialize Auth0 configuration
initializeAuth0({
domain: 'your-tenant.auth0.com',
clientId: 'your-client-id',
redirectUri: 'yourappscheme://auth',
});2. Set Up Anonymous Authentication
import { initializeAnonymousId } from '@ridwan-retainer/auth-lite';
import { useEffect } from 'react';
function App() {
useEffect(() => {
// Initialize anonymous ID on app start
initializeAnonymousId();
}, []);
return <YourApp />;
}3. Implement Sign In
import { useSignIn, useIsAnonymous } from '@ridwan-retainer/auth-lite';
import { Button, Text, View } from 'react-native';
function AuthScreen() {
const { signIn, isLoading, error } = useSignIn();
const isAnonymous = useIsAnonymous();
const handleSignIn = async () => {
try {
const credentials = await signIn();
console.log('Signed in:', credentials);
} catch (err) {
console.error('Sign in failed:', err);
}
};
return (
<View>
{isAnonymous && <Text>You're browsing anonymously</Text>}
<Button
title={isLoading ? 'Signing in...' : 'Sign In with Auth0'}
onPress={handleSignIn}
disabled={isLoading}
/>
{error && <Text>Error: {error.message}</Text>}
</View>
);
}4. Implement Social Login
import {
useSignInWithGoogle,
useSignInWithApple,
useSignInWithFacebook,
} from '@ridwan-retainer/auth-lite';
function SocialLoginButtons() {
const { signIn: signInWithGoogle, isLoading: googleLoading } = useSignInWithGoogle();
const { signIn: signInWithApple, isLoading: appleLoading } = useSignInWithApple();
const { signIn: signInWithFacebook, isLoading: facebookLoading } = useSignInWithFacebook();
return (
<View>
<Button
title="Continue with Google"
onPress={() => signInWithGoogle()}
disabled={googleLoading}
/>
<Button
title="Continue with Apple"
onPress={() => signInWithApple()}
disabled={appleLoading}
/>
<Button
title="Continue with Facebook"
onPress={() => signInWithFacebook()}
disabled={facebookLoading}
/>
</View>
);
}🧠 Core Concepts
Anonymous Users
The library automatically generates and persists an anonymous ID for users who haven't signed in. This allows you to:
- Track user behavior before authentication
- Provide immediate access to app features
- Seamlessly migrate to authenticated accounts
Authentication Flow
- Anonymous State: User opens app → Anonymous ID generated
- Sign In: User signs in → OAuth flow with Auth0
- Token Storage: Access/refresh tokens stored securely
- Migration: Anonymous data linked to authenticated account
Credential Management
Credentials are automatically managed:
- Access Tokens: Short-lived, used for API requests
- Refresh Tokens: Long-lived, used to obtain new access tokens
- Automatic Refresh: Tokens refreshed before expiration
- Secure Storage: All tokens stored in platform-secure storage
📚 API Reference
Anonymous Authentication
generateAnonymousId(): AnonymousId
Generates a new anonymous ID using cryptographic random values.
import { generateAnonymousId } from '@ridwan-retainer/auth-lite';
const anonymousId = generateAnonymousId();
console.log(anonymousId); // "anon_1a2b3c4d..."getOrCreateAnonymousId(): Promise<AnonymousId>
Gets existing anonymous ID or creates a new one if none exists.
import { getOrCreateAnonymousId } from '@ridwan-retainer/auth-lite';
const id = await getOrCreateAnonymousId();useAnonymousId(): AnonymousId | null
React hook to get the current anonymous ID.
import { useAnonymousId } from '@ridwan-retainer/auth-lite';
function Profile() {
const anonymousId = useAnonymousId();
return <Text>Your ID: {anonymousId}</Text>;
}useIsAnonymous(): boolean
React hook to check if user is anonymous.
import { useIsAnonymous } from '@ridwan-retainer/auth-lite';
function WelcomeBanner() {
const isAnonymous = useIsAnonymous();
if (isAnonymous) {
return <Text>Sign in to save your progress!</Text>;
}
return <Text>Welcome back!</Text>;
}clearAnonymousId(): Promise<void>
Clears the stored anonymous ID (useful during sign out).
import { clearAnonymousId } from '@ridwan-retainer/auth-lite';
await clearAnonymousId();Auth0 Integration
initializeAuth0(config: Auth0Config): void
Initialize Auth0 configuration. Call this once at app startup.
import { initializeAuth0 } from '@ridwan-retainer/auth-lite';
initializeAuth0({
domain: 'your-tenant.auth0.com',
clientId: 'your-client-id',
redirectUri: 'yourapp://auth',
// Optional: additional scopes
scopes: ['openid', 'profile', 'email', 'offline_access'],
});useSignIn(): UseSignInReturn
Hook for general Auth0 sign in (shows Auth0 Universal Login).
import { useSignIn } from '@ridwan-retainer/auth-lite';
function SignInButton() {
const { signIn, isLoading, error } = useSignIn();
const handleSignIn = async () => {
try {
const credentials = await signIn();
// Navigate to authenticated screen
} catch (err) {
console.error('Sign in error:', err);
}
};
return (
<>
<Button onPress={handleSignIn} disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</Button>
{error && <Text style={{color: 'red'}}>{error.message}</Text>}
</>
);
}Return Type:
{
signIn: () => Promise<Credentials>;
isLoading: boolean;
error: Error | null;
}useSignInWithGoogle(): UseSignInReturn
Hook for Google OAuth sign in.
import { useSignInWithGoogle } from '@ridwan-retainer/auth-lite';
function GoogleSignInButton() {
const { signIn, isLoading } = useSignInWithGoogle();
return (
<Button onPress={() => signIn()} disabled={isLoading}>
Continue with Google
</Button>
);
}useSignInWithApple(): UseSignInReturn
Hook for Apple Sign In.
import { useSignInWithApple } from '@ridwan-retainer/auth-lite';
function AppleSignInButton() {
const { signIn, isLoading } = useSignInWithApple();
return (
<Button onPress={() => signIn()} disabled={isLoading}>
Continue with Apple
</Button>
);
}useSignInWithFacebook(): UseSignInReturn
Hook for Facebook OAuth sign in.
import { useSignInWithFacebook } from '@ridwan-retainer/auth-lite';
function FacebookSignInButton() {
const { signIn, isLoading } = useSignInWithFacebook();
return (
<Button onPress={() => signIn()} disabled={isLoading}>
Continue with Facebook
</Button>
);
}signOut(): Promise<void>
Signs out the current user and clears all stored credentials.
import { signOut } from '@ridwan-retainer/auth-lite';
async function handleSignOut() {
await signOut();
// Navigate to login screen
}Credential Management
getCredentials(options?: GetCredentialsOptions): Promise<Credentials | null>
Retrieves stored credentials with optional auto-refresh.
import { getCredentials } from '@ridwan-retainer/auth-lite';
// Get credentials and auto-refresh if expired
const credentials = await getCredentials({ autoRefresh: true });
if (credentials) {
// Make API call with credentials.accessToken
}Options:
{
autoRefresh?: boolean; // Default: true
}getAccessToken(): Promise<string | null>
Get the current access token (refreshes if expired).
import { getAccessToken } from '@ridwan-retainer/auth-lite';
const token = await getAccessToken();
if (token) {
// Use token for API calls
fetch('https://api.example.com/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});
}hasValidCredentials(): Promise<boolean>
Check if user has valid (non-expired) credentials.
import { hasValidCredentials } from '@ridwan-retainer/auth-lite';
const isAuthenticated = await hasValidCredentials();
if (isAuthenticated) {
// Show authenticated content
} else {
// Show login prompt
}refreshCredentials(): Promise<Credentials>
Manually refresh the access token using the refresh token.
import { refreshCredentials } from '@ridwan-retainer/auth-lite';
try {
const newCredentials = await refreshCredentials();
console.log('Token refreshed:', newCredentials.accessToken);
} catch (error) {
// Refresh failed, user needs to sign in again
console.error('Refresh failed:', error);
}clearCredentials(): Promise<void>
Clear all stored credentials (use during sign out).
import { clearCredentials } from '@ridwan-retainer/auth-lite';
await clearCredentials();User Profile
getUserProfile(): Promise<UserProfile | null>
Get the current user's profile information.
import { getUserProfile } from '@ridwan-retainer/auth-lite';
const profile = await getUserProfile();
if (profile) {
console.log('Name:', profile.name);
console.log('Email:', profile.email);
console.log('Picture:', profile.picture);
}UserProfile Type:
{
sub: string; // User ID
name?: string; // Full name
given_name?: string; // First name
family_name?: string; // Last name
email?: string; // Email address
email_verified?: boolean;
picture?: string; // Profile picture URL
[key: string]: any; // Additional claims
}getUserId(): Promise<string | null>
Get just the user ID.
import { getUserId } from '@ridwan-retainer/auth-lite';
const userId = await getUserId();getUserEmail(): Promise<string | null>
Get just the user's email.
import { getUserEmail } from '@ridwan-retainer/auth-lite';
const email = await getUserEmail();Account Deletion
useDeletion(callback: DeletionCallback): UseDeletionResult
Hook for handling account deletion with confirmation flow.
import { useDeletion } from '@ridwan-retainer/auth-lite';
function DeleteAccountScreen() {
const { requestDeletion, isDeleting } = useDeletion(async (userId) => {
// Call your API to delete the account
await fetch(`https://api.example.com/users/${userId}`, {
method: 'DELETE',
});
});
const handleDelete = async () => {
try {
await requestDeletion();
// Account deleted, navigate away
} catch (error) {
console.error('Deletion failed:', error);
}
};
return (
<Button onPress={handleDelete} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete Account'}
</Button>
);
}useConfirmation(warning?: DeletionWarning): UseConfirmationResult
Hook for account deletion confirmation dialog.
import { useConfirmation } from '@ridwan-retainer/auth-lite';
function DeleteAccountButton() {
const { showConfirmation } = useConfirmation({
title: 'Delete Account?',
message: 'This action cannot be undone. All your data will be permanently deleted.',
confirmText: 'Delete',
cancelText: 'Cancel',
});
const handleDelete = async () => {
const confirmed = await showConfirmation();
if (confirmed) {
// Proceed with deletion
}
};
return <Button onPress={handleDelete}>Delete Account</Button>;
}Migration
checkAndMigrate(): Promise<boolean>
Check if anonymous-to-authenticated migration is needed and perform it.
import { checkAndMigrate, setMigrationCallback } from '@ridwan-retainer/auth-lite';
// Set up migration callback
setMigrationCallback(async (anonymousId, authenticatedId) => {
// Call your API to migrate data
await fetch('https://api.example.com/migrate', {
method: 'POST',
body: JSON.stringify({ from: anonymousId, to: authenticatedId }),
});
});
// After sign in, check for migration
const migrated = await checkAndMigrate();
if (migrated) {
console.log('User data migrated successfully');
}🔧 Advanced Usage
Custom Authentication Guard
import { hasValidCredentials } from '@ridwan-retainer/auth-lite';
import { useEffect, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
function useAuthGuard() {
const [isChecking, setIsChecking] = useState(true);
const navigation = useNavigation();
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
const isAuthenticated = await hasValidCredentials();
if (!isAuthenticated) {
navigation.replace('Login');
}
setIsChecking(false);
};
return { isChecking };
}
// Usage in protected screens
function ProtectedScreen() {
const { isChecking } = useAuthGuard();
if (isChecking) {
return <LoadingScreen />;
}
return <YourContent />;
}Automatic Token Refresh with Axios
import axios from 'axios';
import { getAccessToken, refreshCredentials } from '@ridwan-retainer/auth-lite';
const api = axios.create({
baseURL: 'https://api.example.com',
});
// Request interceptor to add token
api.interceptors.request.use(async (config) => {
const token = await getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor to handle 401 errors
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
await refreshCredentials();
const token = await getAccessToken();
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh failed, redirect to login
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;Context Provider Pattern
import React, { createContext, useContext, useEffect, useState } from 'react';
import {
hasValidCredentials,
getUserProfile,
signOut,
type UserProfile,
} from '@ridwan-retainer/auth-lite';
interface AuthContextValue {
user: UserProfile | null;
isAuthenticated: boolean;
isLoading: boolean;
signOut: () => Promise<void>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<UserProfile | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadUser();
}, []);
const loadUser = async () => {
try {
const authenticated = await hasValidCredentials();
setIsAuthenticated(authenticated);
if (authenticated) {
const profile = await getUserProfile();
setUser(profile);
}
} finally {
setIsLoading(false);
}
};
const handleSignOut = async () => {
await signOut();
setUser(null);
setIsAuthenticated(false);
};
return (
<AuthContext.Provider
value={{
user,
isAuthenticated,
isLoading,
signOut: handleSignOut,
refreshUser: loadUser,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}📖 Migration Guide
Migrating from v0.0.x to v0.1.0
No breaking changes. This is the initial release.
Migrating from Other Auth Libraries
If you're migrating from another authentication library:
- Remove old auth dependencies
- Install @ridwan-retainer/auth-lite and peer dependencies
- Replace initialization code
- Update sign in/out flows
- Migrate token storage (if needed)
Example migration from Firebase Auth:
// Before (Firebase Auth)
import auth from '@react-native-firebase/auth';
async function signIn() {
const result = await auth().signInWithEmailAndPassword(email, password);
return result.user;
}
// After (@ridwan-retainer/auth-lite)
import { useSignIn } from '@ridwan-retainer/auth-lite';
function SignInComponent() {
const { signIn } = useSignIn();
const handleSignIn = async () => {
const credentials = await signIn();
return credentials;
};
}✅ Best Practices
1. Initialize Early
Initialize Auth0 configuration as early as possible in your app lifecycle:
// App.tsx
import { initializeAuth0, initializeAnonymousId } from '@ridwan-retainer/auth-lite';
initializeAuth0({
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENT_ID,
redirectUri: 'myapp://auth',
});
initializeAnonymousId();2. Handle Errors Gracefully
Always handle authentication errors:
const { signIn, error } = useSignIn();
useEffect(() => {
if (error) {
// Show user-friendly error message
Alert.alert('Sign In Failed', error.message);
}
}, [error]);3. Use Auto-Refresh
Always enable auto-refresh when getting credentials:
const credentials = await getCredentials({ autoRefresh: true });4. Implement Migration
Set up anonymous-to-authenticated migration:
setMigrationCallback(async (anonId, authId) => {
// Migrate user data on your backend
await api.post('/migrate', { from: anonId, to: authId });
});5. Secure Storage Only
Never store tokens in AsyncStorage - the library uses SecureStore automatically.
6. Check Authentication State
Check authentication on app startup and navigation:
useEffect(() => {
hasValidCredentials().then(setIsAuthenticated);
}, []);🐛 Troubleshooting
Issue: "No credentials found"
Solution: User hasn't signed in yet. Show login screen.
const credentials = await getCredentials();
if (!credentials) {
navigation.navigate('Login');
}Issue: "Refresh token expired"
Solution: User needs to sign in again. Clear credentials and show login.
try {
await refreshCredentials();
} catch (error) {
await clearCredentials();
navigation.navigate('Login');
}Issue: "Invalid redirect URI"
Solution: Ensure redirect URI matches Auth0 configuration:
- Check Auth0 dashboard → Application → Allowed Callback URLs
- Verify format:
yourscheme://auth - Ensure scheme matches app.json
Issue: Sign in opens browser but doesn't return
Solution: Check deep linking configuration:
// app.json
{
"expo": {
"scheme": "yourappscheme"
}
}Issue: "Cannot read property 'sub' of null"
Solution: Check if user is authenticated before accessing profile:
const profile = await getUserProfile();
if (profile) {
console.log(profile.sub);
}🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
MIT © Ridwan Hamid
🔗 Links
📞 Support
For issues and questions:
