npm package: @ridwan-retainer/auth-lite
Description: Lightweight identity layer with anonymous mode and Auth0 integration
Latest version: 0.1.1

# @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.

[![npm version](https://badge.fury.io/js/@ridwan-retainer%2Fauth-lite.svg)](https://www.npmjs.com/package/@ridwan-retainer/auth-lite)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 📋 Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
  - [Anonymous Authentication](#anonymous-authentication)
  - [Auth0 Integration](#auth0-integration)
  - [Credential Management](#credential-management)
  - [User Profile](#user-profile)
  - [Account Deletion](#account-deletion)
- [Advanced Usage](#advanced-usage)
- [Migration Guide](#migration-guide)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [Contributing](#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

```bash
npm install @ridwan-retainer/auth-lite
```

### Peer Dependencies

This package requires the following peer dependencies:

```bash
npm install @react-native-async-storage/async-storage \
            expo-auth-session \
            expo-crypto \
            expo-secure-store \
            expo-web-browser \
            react \
            react-native
```

### Platform-Specific Setup

#### iOS (Expo)
Add to your `app.json`:

```json
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.yourapp.id"
    },
    "scheme": "yourappscheme"
  }
}
```

#### Android (Expo)
Add to your `app.json`:

```json
{
  "expo": {
    "android": {
      "package": "com.yourapp.id"
    },
    "scheme": "yourappscheme"
  }
}
```

## 🚀 Quick Start

### 1. Configure Auth0

```typescript
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

```typescript
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

```typescript
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

```typescript
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

1. **Anonymous State**: User opens app → Anonymous ID generated
2. **Sign In**: User signs in → OAuth flow with Auth0
3. **Token Storage**: Access/refresh tokens stored securely
4. **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.

```typescript
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.

```typescript
import { getOrCreateAnonymousId } from '@ridwan-retainer/auth-lite';

const id = await getOrCreateAnonymousId();
```

#### `useAnonymousId(): AnonymousId | null`
React hook to get the current anonymous ID.

```typescript
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.

```typescript
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).

```typescript
import { clearAnonymousId } from '@ridwan-retainer/auth-lite';

await clearAnonymousId();
```

### Auth0 Integration

#### `initializeAuth0(config: Auth0Config): void`
Initialize Auth0 configuration. Call this once at app startup.

```typescript
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).

```typescript
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:**
```typescript
{
  signIn: () => Promise<Credentials>;
  isLoading: boolean;
  error: Error | null;
}
```

#### `useSignInWithGoogle(): UseSignInReturn`
Hook for Google OAuth sign in.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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:**
```typescript
{
  autoRefresh?: boolean; // Default: true
}
```

#### `getAccessToken(): Promise<string | null>`
Get the current access token (refreshes if expired).

```typescript
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.

```typescript
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.

```typescript
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).

```typescript
import { clearCredentials } from '@ridwan-retainer/auth-lite';

await clearCredentials();
```

### User Profile

#### `getUserProfile(): Promise<UserProfile | null>`
Get the current user's profile information.

```typescript
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:**
```typescript
{
  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.

```typescript
import { getUserId } from '@ridwan-retainer/auth-lite';

const userId = await getUserId();
```

#### `getUserEmail(): Promise<string | null>`
Get just the user's email.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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

```typescript
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

```typescript
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

```typescript
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:

1. **Remove old auth dependencies**
2. **Install @ridwan-retainer/auth-lite and peer dependencies**
3. **Replace initialization code**
4. **Update sign in/out flows**
5. **Migrate token storage** (if needed)

Example migration from Firebase Auth:

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
const credentials = await getCredentials({ autoRefresh: true });
```

### 4. Implement Migration
Set up anonymous-to-authenticated migration:

```typescript
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:

```typescript
useEffect(() => {
  hasValidCredentials().then(setIsAuthenticated);
}, []);
```

## 🐛 Troubleshooting

### Issue: "No credentials found"

**Solution**: User hasn't signed in yet. Show login screen.

```typescript
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.

```typescript
try {
  await refreshCredentials();
} catch (error) {
  await clearCredentials();
  navigation.navigate('Login');
}
```

### Issue: "Invalid redirect URI"

**Solution**: Ensure redirect URI matches Auth0 configuration:

1. Check Auth0 dashboard → Application → Allowed Callback URLs
2. Verify format: `yourscheme://auth`
3. Ensure scheme matches app.json

### Issue: Sign in opens browser but doesn't return

**Solution**: Check deep linking configuration:

```json
// app.json
{
  "expo": {
    "scheme": "yourappscheme"
  }
}
```

### Issue: "Cannot read property 'sub' of null"

**Solution**: Check if user is authenticated before accessing profile:

```typescript
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](https://github.com/RidwanHamid501)

## 🔗 Links

- [npm Package](https://www.npmjs.com/package/@ridwan-retainer/auth-lite)
- [GitHub Repository](https://github.com/RidwanHamid501/app-factory/tree/main/packages/auth-lite)
- [Auth0 Documentation](https://auth0.com/docs)
- [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore/)

## 📞 Support

For issues and questions:
- [GitHub Issues](https://github.com/RidwanHamid501/app-factory/issues)
- [Discussions](https://github.com/RidwanHamid501/app-factory/discussions)