Framework-agnostic social authentication library for web and mobile (React, React Native, Next.js, Expo, etc.)
npm install rystem.authentication.social.clientFramework-agnostic TypeScript library for social authentication with built-in PKCE support for secure OAuth 2.0 flows.
Works with: React, React Native, Next.js, Expo, Remix, and any JavaScript/TypeScript framework.
- 🔐 PKCE Built-in: Automatic code_verifier generation for Microsoft OAuth (RFC 7636)
- ⚛️ React Hooks: Type-safe hooks for token and user management
- 🎨 Ready-to-Use Components: Login buttons, logout, authentication wrapper (React only)
- 🔄 Automatic Token Refresh: Handles token expiration seamlessly
- 📱 Multi-Platform: Web (React, Next.js), Mobile (React Native, Expo), and any framework via interfaces
- 🔌 Framework-Agnostic Core: Inject custom storage and routing services for any platform
All social providers now support mobile platforms! Configure platform-specific OAuth redirect URIs for seamless authentication across Web, React Native iOS, and React Native Android.
| Provider | Web (Popup) | Web (Redirect) | React Native iOS | React Native Android | PKCE Support |
|----------|-------------|----------------|------------------|---------------------|--------------|
| Microsoft | ✅ | ✅ | ✅ | ✅ | ✅ |
| Google | ✅ | ✅ | ✅ | ✅ | - |
| Facebook | ✅ | ✅ | ✅ | ✅ | - |
| GitHub | ✅ | ✅ | ✅ | ✅ | - |
| Amazon | ✅ | ✅ | ✅ | ✅ | - |
| LinkedIn | ✅ | ✅ | ✅ | ✅ | - |
| X (Twitter) | ✅ | ✅ | ✅ | ✅ | - |
| TikTok | ✅ | ✅ | ✅ | ✅ | - |
| Instagram | ✅ | ✅ | ✅ | ✅ | - |
| Pinterest | ✅ | ✅ | ✅ | ✅ | - |
1. Auto-Detection: Library automatically detects platform (Web/iOS/Android) from navigator.userAgent
2. Platform-Specific URIs: Configure custom redirect URIs per platform (e.g., msauth:// for iOS, myapp:// for Android)
3. Login Modes: Choose Popup (web) or Redirect (mobile) behavior
4. Deep Links: All buttons support mobile deep link OAuth callbacks
5. No Breaking Changes: Existing web apps work without modification
``typescript
import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.client';
import { Platform } from 'react-native'; // Only in React Native projects
setupSocialLogin(x => {
x.apiUri = "https://api.yourdomain.com";
// Platform configuration (auto-detects if not specified)
x.platform = {
type: PlatformType.Auto,
// Smart redirect path (auto-detects domain for web)
redirectPath: Platform.select({
ios: 'msauth://com.yourapp.bundle/auth', // Complete URI for mobile
android: 'myapp://oauth/callback', // Complete URI for mobile
web: '/account/login' // Path only (auto-detects domain)
}),
// Login mode (auto-set based on platform if not specified)
loginMode: Platform.select({
ios: LoginMode.Redirect,
android: LoginMode.Redirect,
web: LoginMode.Popup
})
};
x.microsoft.clientId = "your-client-id";
x.google.clientId = "your-client-id";
});
`
📖 Full Migration Guide: See PLATFORM_SUPPORT.md for detailed setup instructions, OAuth provider configuration, and troubleshooting.
`bash`
npm install rystem.authentication.social.clientor
yarn add rystem.authentication.social.clientor
pnpm add rystem.authentication.social.client
---
This library is 100% framework-agnostic and works perfectly in React Native, Expo, and any mobile framework! However, setup differs from web because:
1. ❌ No window, document, or localStorage in React Native
2. ❌ Web SDK scripts (Google Sign-In SDK, @azure/msal-browser) don't work in React Native@react-native-google-signin/google-signin
3. ✅ Native SDKs must be used instead (, @react-native-community/msal)
| Component | Web | React Native |
|-----------|-----|--------------|
| Core Services (SocialLoginManager, setupSocialLogin) | ✅ Works | ✅ Works |useSocialToken
| React Hooks (, useSocialUser) | ✅ Works | ✅ Works |MicrosoftButton
| UI Components (, GoogleButton) | ✅ Works | ❌ Web-only |LocalStorageService
| Storage | | Custom IStorageService (AsyncStorage) |WindowRoutingService
| Routing | | Custom IRoutingService (Linking) |BrowserPlatformService
| Platform | | Custom IPlatformService (Dimensions) |
- [ ] 1. Install @react-native-async-storage/async-storageReactNativeStorageService
- [ ] 2. Create implementing IStorageServiceReactNativeRoutingService
- [ ] 3. Create implementing IRoutingServiceReactNativePlatformService
- [ ] 4. Create implementing IPlatformService@react-native-community/msal
- [ ] 5. Install native OAuth SDKs (, @react-native-google-signin/google-signin)SocialLoginManager.Instance(null).updateToken()
- [ ] 6. Create custom login button components
- [ ] 7. Use after successful native login
---
#### 💾 Storage Service (AsyncStorage)
`bash`
npm install @react-native-async-storage/async-storage
`typescript
// services/ReactNativeStorageService.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { IStorageService } from 'rystem.authentication.social.client';
export class ReactNativeStorageService implements IStorageService {
get(key: string): string | null {
// Note: AsyncStorage is async, but IStorageService expects sync
// Use synchronous approach or modify your usage to handle promises
let result: string | null = null;
AsyncStorage.getItem(key).then(value => result = value);
return result;
}
set(key: string, value: string): void {
AsyncStorage.setItem(key, value).catch(error =>
console.error('AsyncStorage set error:', error)
);
}
remove(key: string): void {
AsyncStorage.removeItem(key).catch(error =>
console.error('AsyncStorage remove error:', error)
);
}
has(key: string): boolean {
return this.get(key) !== null;
}
clear(): void {
AsyncStorage.clear().catch(error =>
console.error('AsyncStorage clear error:', error)
);
}
}
`
#### 🧭 Routing Service (Linking API)
`typescript
// services/ReactNativeRoutingService.ts
import { Linking } from 'react-native';
import { IRoutingService } from 'rystem.authentication.social.client';
export class ReactNativeRoutingService implements IRoutingService {
private currentUrl: URL | null = null;
constructor() {
Linking.getInitialURL().then(url => {
if (url) this.currentUrl = new URL(url);
});
Linking.addEventListener('url', ({ url }) => {
this.currentUrl = new URL(url);
});
}
getSearchParam(key: string): string | null {
return this.currentUrl?.searchParams.get(key) ?? null;
}
getAllSearchParams(): URLSearchParams {
return this.currentUrl?.searchParams ?? new URLSearchParams();
}
getCurrentPath(): string {
if (!this.currentUrl) return '/';
return this.currentUrl.pathname + this.currentUrl.search;
}
navigateTo(url: string): void {
Linking.openURL(url);
}
navigateReplace(path: string): void {
// Implement with React Navigation or Expo Router
console.log('Navigate to:', path);
}
openPopup(url: string, name: string, features: string): Window | null {
Linking.openURL(url);
return null;
}
}
`
#### 📐 Platform Service (Dimensions & Events)
`typescript
// services/ReactNativePlatformService.ts
import { Dimensions, EventEmitter } from 'react-native';
import { IPlatformService } from 'rystem.authentication.social.client';
export class ReactNativePlatformService implements IPlatformService {
private eventEmitter = new EventEmitter();
addStorageListener(callback: () => void): void {
this.eventEmitter.addListener('storage', callback);
}
removeStorageListener(callback: () => void): void {
this.eventEmitter.removeListener('storage', callback);
}
getScreenWidth(): number {
return Dimensions.get('window').width;
}
getScreenHeight(): number {
return Dimensions.get('window').height;
}
loadScript(id: string, src: string, onLoad: () => void): HTMLScriptElement | null {
console.warn('Script loading not supported in React Native');
onLoad();
return null;
}
scriptExists(id: string): boolean {
return false;
}
removeScript(scriptElement: HTMLScriptElement): void {
// No-op
}
isPopup(): boolean {
return false;
}
closeWindow(): void {
// No-op
}
}
`
#### ⚙️ Complete Setup
`typescript
// App.tsx
import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.client';
import { ReactNativeStorageService } from './services/ReactNativeStorageService';
import { ReactNativeRoutingService } from './services/ReactNativeRoutingService';
import { ReactNativePlatformService } from './services/ReactNativePlatformService';
import { Platform, Alert } from 'react-native';
setupSocialLogin(x => {
x.apiUri = "https://api.yourdomain.com";
// ✅ Inject React Native implementations
x.storageService = new ReactNativeStorageService();
x.routingService = new ReactNativeRoutingService();
x.platformService = new ReactNativePlatformService();
x.platform = {
type: PlatformType.Auto,
redirectPath: Platform.select({
ios: 'myapp://oauth/callback',
android: 'myapp://oauth/callback',
default: '/account/login'
}),
loginMode: LoginMode.Redirect
};
x.microsoft.clientId = "your-client-id";
x.google.clientId = "your-client-id";
x.onLoginFailure = (error) => {
Alert.alert('Login Failed', error.message);
};
});
`
---
#### 📘 Microsoft Login (Native MSAL)
`bash`
npm install @react-native-community/msal
`typescript
// components/MicrosoftLoginButton.tsx
import React from 'react';
import { Pressable, Text, StyleSheet, Alert } from 'react-native';
import { MSALConfiguration, MSALResult, PublicClientApplication } from '@react-native-community/msal';
import { SocialLoginManager, ProviderType } from 'rystem.authentication.social.client';
const MSAL_CONFIG: MSALConfiguration = {
auth: {
clientId: 'your-microsoft-client-id',
authority: 'https://login.microsoftonline.com/consumers',
},
};
export const MicrosoftLoginButton = () => {
const handleLogin = async () => {
try {
const pca = new PublicClientApplication(MSAL_CONFIG);
await pca.init();
const result: MSALResult = await pca.acquireToken({
scopes: ['openid', 'profile', 'email', 'User.Read'],
});
// ✅ Send token to backend
await SocialLoginManager.Instance(null).updateToken(
ProviderType.Microsoft,
result.idToken
);
console.log('✅ Microsoft login successful');
} catch (error: any) {
console.error('❌ Microsoft login failed:', error);
Alert.alert('Login Failed', error.message || 'Unknown error');
}
};
return (
);
};
const styles = StyleSheet.create({
button: { padding: 12, borderRadius: 8, alignItems: 'center', marginVertical: 8 },
text: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
`
#### 🔴 Google Login (Native SDK)
`bash`
npm install @react-native-google-signin/google-signin
`typescript
// components/GoogleLoginButton.tsx
import React from 'react';
import { Pressable, Text, StyleSheet, Alert } from 'react-native';
import { GoogleSignin, statusCodes } from '@react-native-google-signin/google-signin';
import { SocialLoginManager, ProviderType } from 'rystem.authentication.social.client';
// Configure once at app startup
GoogleSignin.configure({
webClientId: 'your-google-web-client-id.apps.googleusercontent.com',
offlineAccess: true,
forceCodeForRefreshToken: true,
iosClientId: 'your-ios-client-id.apps.googleusercontent.com',
});
export const GoogleLoginButton = () => {
const handleLogin = async () => {
try {
await GoogleSignin.hasPlayServices();
const userInfo = await GoogleSignin.signIn();
if (!userInfo.idToken) throw new Error('No ID token received');
// ✅ Send token to backend
await SocialLoginManager.Instance(null).updateToken(
ProviderType.Google,
userInfo.idToken
);
console.log('✅ Google login successful');
} catch (error: any) {
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
console.log('User cancelled login');
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
Alert.alert('Error', 'Play Services not available');
} else {
console.error('❌ Google login failed:', error);
Alert.alert('Login Failed', error.message);
}
}
};
return (
);
};
const styles = StyleSheet.create({
button: { padding: 12, borderRadius: 8, alignItems: 'center', marginVertical: 8 },
text: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
`
---
`typescript
// screens/LoginScreen.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { MicrosoftLoginButton } from '../components/MicrosoftLoginButton';
import { GoogleLoginButton } from '../components/GoogleLoginButton';
import { useSocialToken, useSocialUser } from 'rystem.authentication.social.client';
export const LoginScreen = () => {
const token = useSocialToken();
const user = useSocialUser();
if (!token.isExpired) {
return (
);
}
return (
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
welcome: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
});
`
---
What You Need:
1. Implement 3 services: IStorageService, IRoutingService, IPlatformServiceSocialLoginManager.Instance(null).updateToken()
2. Install native OAuth SDKs (not web SDKs!)
3. Create custom UI components
4. Call after native login success
What Works Automatically:
- ✅ useSocialToken() / useSocialUser() hooks
- ✅ Token storage and refresh logic
- ✅ Backend communication
Why No window Access Issues:
- ✅ Library uses service abstractions (IStorageService, IRoutingService, IPlatformService)
- ✅ You inject platform-specific implementations
- ✅ No polyfills needed!
---
If you're using React Router or Next.js App Router, OAuth callbacks and navigation may not work correctly due to client-side routing intercepting native browser APIs.
👉 Solution: Implement a custom IRoutingService for your framework.
📖 See full guide: 🧭 Custom Routing Service section below with ready-to-use implementations for:
- React Router v6+
- Next.js App Router (v13+)
- Unit Testing
`typescript
import { SocialLoginWrapper, setupSocialLogin } from 'rystem.authentication.social.client';
import App from './App';
setupSocialLogin(x => {
// 🚀 ONE-LINE SETUP for web applications
// This configures localStorage, window routing, and browser platform APIs
x.useBrowserDefaults();
// API server URL
x.apiUri = "https://localhost:7017";
// Configure OAuth providers (only clientId needed for client-side)
x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
x.google.clientId = "23769141170-lfs24avv5qrj00m4cbmrm202c0fc6gcg.apps.googleusercontent.com";
x.facebook.clientId = "345885718092912";
x.github.clientId = "97154d062f2bb5d28620";
// Error handling callback
x.onLoginFailure = (error) => {
console.error(Login failed: ${error.message} (Code: ${error.code}));Authentication error: ${error.message}
alert();
};
// Automatic token refresh when expired
x.automaticRefresh = true;
});
function Root() {
return (
);
}
export default Root;
`
#### 🤔 What does useBrowserDefaults() do?
This helper method automatically configures three essential services for web browsers:
1. storageService: LocalStorageService → Uses browser localStorage to persist tokens, PKCE verifiers, and user dataroutingService
2. : WindowRoutingService → Uses window.location to read OAuth callback parameters and window.history for navigationplatformService
3. : BrowserPlatformService → Uses window, document, and DOM APIs for popup windows, screen dimensions, and external SDK loading
Why is this needed?
The library is framework-agnostic and works in React, React Native, Next.js, and Expo. Each environment has different APIs:
- Web: Uses window, localStorage, documentAsyncStorage
- React Native: Uses , Linking, Dimensionswindow
- Next.js SSR: May not have available
By requiring explicit service configuration, the library:
- ✅ Forces you to think about your environment
- ✅ Avoids unexpected crashes in non-browser environments
- ✅ Makes it clear which dependencies are being used
- ✅ Allows easy customization (e.g., using React Router instead of window.history)
Alternative: Manual configuration
If you want more control, you can configure services manually:
`typescript
import { LocalStorageService, WindowRoutingService, BrowserPlatformService } from 'rystem.authentication.social.client';
setupSocialLogin(x => {
x.storageService = new LocalStorageService();
x.routingService = new WindowRoutingService();
x.platformService = new BrowserPlatformService();
// ... rest of config
});
`
For React Native setup, see the 📱 React Native Complete Guide section below.
---
`typescript
import { useSocialToken, useSocialUser, SocialLoginButtons, SocialLogoutButton } from 'rystem.authentication.social.client';
export const App = () => {
const token = useSocialToken();
const user = useSocialUser();
return (
Access Token: {token.accessToken}
🔐 PKCE Support (Microsoft OAuth)
$3
The library automatically implements PKCE for Microsoft OAuth:
1. Code Verifier Generation: When user clicks Microsoft login button
`typescript
const codeVerifier = await generateCodeVerifier(); // 43-128 chars random string
const codeChallenge = await generateCodeChallenge(codeVerifier); // SHA256 hash
`2. Session Storage: Stores
code_verifier for callback retrieval
`typescript
storeCodeVerifier('microsoft', codeVerifier);
`3. OAuth Request: Sends
code_challenge with S256 method
`
https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
?client_id={clientId}
&response_type=code
&redirect_uri={redirectUri}
&code_challenge={codeChallenge}
&code_challenge_method=S256
`4. Token Exchange: Sends
code_verifier to API server
`typescript
POST /api/Authentication/Social/Token?provider=Microsoft&code={code}&redirectPath=/account/login
Body: { "code_verifier": "original-verifier" }
`5. Cleanup: Removes verifier from sessionStorage after use
$3
For custom implementations:
`typescript
import { generateCodeVerifier, generateCodeChallenge, storeCodeVerifier, getAndRemoveCodeVerifier } from 'rystem.authentication.social.react';// Generate PKCE values
const codeVerifier = await generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store for later retrieval
storeCodeVerifier('custom-provider', codeVerifier);
// Build OAuth URL with code_challenge
const authUrl =
https://oauth.provider.com/authorize?code_challenge=${codeChallenge}&code_challenge_method=S256;
window.location.href = authUrl;// After OAuth callback, retrieve and remove verifier
const storedVerifier = getAndRemoveCodeVerifier('custom-provider');
`🎣 React Hooks
$3
Get current JWT token for API requests:
`typescript
const token = useSocialToken();interface Token {
accessToken: string; // JWT bearer token
refreshToken: string; // Refresh token for renewal
isExpired: boolean; // True if token expired
expiresIn: Date; // Token expiration timestamp
}
// Usage in API calls
if (!token.isExpired) {
const response = await fetch('/api/orders', {
headers: {
'Authorization':
Bearer ${token.accessToken}
}
});
}
`$3
Get authenticated user information:
`typescript
const user = useSocialUser();interface SocialUser {
username: string; // User's email/username
isAuthenticated: boolean; // True if user is logged in
// Add custom properties from your API
}
if (user.isAuthenticated) {
console.log(
Logged in as: ${user.username});
}
`$3
Force token refresh:
`typescript
import { useContext } from 'react';
import { SocialLoginContextRefresh } from 'rystem.authentication.social.react';const forceRefresh = useContext(SocialLoginContextRefresh);
const handleRefresh = async () => {
await forceRefresh();
console.log('Token refreshed!');
};
`$3
Programmatic logout:
`typescript
import { useContext } from 'react';
import { SocialLoginContextLogout } from 'rystem.authentication.social.react';const logout = useContext(SocialLoginContextLogout);
const handleLogout = async () => {
await logout();
window.location.href = '/login';
};
`🎨 UI Components
$3
Renders all configured provider buttons:
`typescript
import { SocialLoginButtons } from 'rystem.authentication.social.react';
`$3
`typescript
import {
SocialLoginButtons,
MicrosoftButton,
GoogleButton,
FacebookButton,
GitHubButton,
AmazonButton,
LinkedinButton,
XButton,
TikTokButton,
InstagramButton,
PinterestButton
} from 'rystem.authentication.social.react';const customOrder = [
MicrosoftButton, // Show Microsoft first
GoogleButton,
GitHubButton,
LinkedinButton,
FacebookButton,
AmazonButton,
XButton,
TikTokButton,
InstagramButton,
PinterestButton
];
`$3
`typescript
import { MicrosoftButton, GoogleButton } from 'rystem.authentication.social.react';
`$3
`typescript
import { SocialLogoutButton } from 'rystem.authentication.social.react';Sign Out
`🔧 Advanced Configuration
$3
The library now supports platform-specific configuration for Web, iOS, and Android (including React Native):
`typescript
import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';setupSocialLogin(x => {
x.apiUri = "https://yourdomain.com";
// Platform configuration
x.platform = {
type: PlatformType.Auto, // Auto-detect platform (Web/iOS/Android)
// Smart redirect path (detects if complete URI or relative path)
redirectPath: Platform.select({
web: '/account/login', // Relative path (auto-detects domain)
ios: 'msauth://com.yourapp.fantasoccer/auth', // Complete URI
android: 'myapp://oauth/callback', // Complete URI
default: '/account/login'
}),
// Login mode (popup for web, redirect for mobile)
loginMode: Platform.select({
web: LoginMode.Popup,
ios: LoginMode.Redirect,
android: LoginMode.Redirect,
default: LoginMode.Redirect
})
};
// OAuth providers
x.microsoft.clientId = "your-client-id";
x.google.clientId = "your-client-id";
});
`#### React Native Example
For React Native apps, use platform-specific deep links:
`typescript
import { Platform } from 'react-native';
import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';setupSocialLogin(x => {
x.apiUri = "https://yourdomain.com";
x.platform = {
type: PlatformType.Auto, // Will detect iOS/Android automatically
// Deep link redirect paths for mobile
redirectPath: Platform.select({
ios: 'msauth://com.keyserdsoze.fantasoccer/auth', // Complete URI
android: 'fantasoccer://oauth/callback', // Complete URI
default: '/account/login' // Relative path for web
}),
loginMode: LoginMode.Redirect // Always use redirect for mobile
};
x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
});
`Important: Configure deep links in your app:
iOS (
Info.plist):
`xml
CFBundleURLTypes
CFBundleURLSchemes
msauth
CFBundleURLName
com.keyserdsoze.fantasoccer
`Android (
AndroidManifest.xml):
`xml
`$3
Choose between popup and redirect modes:
`typescript
// Popup mode (default for web - opens in new window)
setupSocialLogin(x => {
x.loginMode = LoginMode.Popup; // or x.platform.loginMode
});// Redirect mode (default for mobile - navigates in same window)
setupSocialLogin(x => {
x.loginMode = LoginMode.Redirect;
});
`Use Cases:
- ✅ Popup: Best for desktop web apps (better UX, user stays on page)
- ✅ Redirect: Required for mobile apps, some browsers block popups
$3
Use built-in utilities for platform detection:
`typescript
import {
detectPlatform,
isMobilePlatform,
isReactNative,
PlatformType
} from 'rystem.authentication.social.react';// Detect current platform
const platform = detectPlatform(); // Returns: PlatformType.Web | iOS | Android
// Check if mobile
if (isMobilePlatform(platform)) {
console.log('Running on mobile');
}
// Check if React Native
if (isReactNative()) {
console.log('Running in React Native');
}
`$3
`typescript
import { setupSocialLogin, PlatformType, LoginMode, detectPlatform } from 'rystem.authentication.social.react';// Detect platform automatically
const currentPlatform = detectPlatform();
setupSocialLogin(x => {
x.apiUri = "https://api.yourdomain.com";
// Configure based on detected platform
x.platform = {
type: currentPlatform,
redirectUri: (() => {
switch (currentPlatform) {
case PlatformType.iOS:
return 'msauth://com.yourapp.bundle/auth';
case PlatformType.Android:
return 'yourapp://oauth/callback';
default:
return typeof window !== 'undefined'
? window.location.origin
: 'http://localhost:3000';
}
})(),
loginMode: currentPlatform === PlatformType.Web
? LoginMode.Popup
: LoginMode.Redirect
};
// OAuth providers
x.microsoft.clientId = "your-microsoft-client-id";
x.google.clientId = "your-google-client-id";
// Error handling
x.onLoginFailure = (error) => {
if (currentPlatform === PlatformType.Web) {
alert(
Login failed: ${error.message});
} else {
// Use React Native Alert or Toast
console.error('Login error:', error);
}
};
x.automaticRefresh = true;
});
`📱 Mobile OAuth Configuration
$3
1. Register your mobile app redirect URI in Azure Portal
2. For iOS:
msauth://com.yourapp.bundle/auth
3. For Android: yourapp://oauth/callback
4. Enable "Mobile and desktop applications" platform
5. Make sure PKCE is enabled (library handles this automatically)$3
1. Configure OAuth consent screen for mobile
2. Add redirect URI: Use reverse client ID for iOS
3. Example:
com.googleusercontent.apps.YOUR_CLIENT_ID:/oauth2redirect$3
iOS Bundle ID Format:
`
msauth://com.yourcompany.yourapp/auth
`Android Package Name Format:
`
yourapp://oauth/callback
`🔍 How Platform Configuration Works
$3
When a user clicks a social login button, the library determines the OAuth redirect URI using this priority order:
`typescript
// Priority 1: Explicit platform.redirectUri (highest priority)
if (settings.platform?.redirectUri) {
redirectUri = settings.platform.redirectUri;
}
// Priority 2: Fallback to redirectDomain + redirectPath
else {
redirectUri = ${settings.redirectDomain}${settings.redirectPath || ''};
}
`$3
1. Setup Configuration:
`typescript
setupSocialLogin(x => {
x.apiUri = "https://api.yourdomain.com";
x.redirectDomain = "https://web.yourdomain.com";
x.redirectPath = "/account/login";
x.platform = {
type: PlatformType.iOS,
redirectUri: "msauth://com.yourapp.bundle/auth" // Mobile deep link
};
x.microsoft.clientId = "your-client-id";
});
`2. User Clicks MicrosoftButton:
- Library detects
platform.redirectUri is set
- Uses msauth://com.yourapp.bundle/auth (NOT https://web.yourdomain.com/account/login)
- Generates PKCE code_verifier and code_challenge
- Constructs OAuth URL:
`
https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
?client_id=your-client-id
&redirect_uri=msauth%3A%2F%2Fcom.yourapp.bundle%2Fauth
&code_challenge=
&code_challenge_method=S256
`3. OAuth Provider Redirects:
- Microsoft redirects to:
msauth://com.yourapp.bundle/auth?code=ABC123&state=XYZ
- iOS deep link handler catches this URL
- React Native navigation extracts code and state4. Token Exchange:
- Library calls API:
POST /api/Authentication/Social/Token?provider=Microsoft&code=ABC123&redirectPath=/account/login
- API validates code using PKCE code_verifier
- Returns JWT access token5. User Logged In:
- Token stored in AsyncStorage (React Native)
-
useSocialToken() and useSocialUser() hooks update
- App navigates to /account/login (or dashboard)$3
`typescript
export function detectPlatform(): PlatformType {
// Check if React Native environment
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
// Detect iOS
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
return PlatformType.iOS;
}
// Detect Android
if (/Android/.test(navigator.userAgent)) {
return PlatformType.Android;
}
}
// Default to Web
return PlatformType.Web;
}
`$3
| Scenario | redirectDomain | redirectPath | platform.redirectUri | platform.type |
|----------|----------------|--------------|---------------------|---------------|
| Web SPA |
https://app.com | /account/login | undefined | Web or Auto |
| React Native iOS | https://app.com (fallback) | /account/login | msauth://com.yourapp.bundle/auth | iOS or Auto |
| React Native Android | https://app.com (fallback) | /account/login | yourapp://oauth/callback | Android or Auto |
| Multi-Platform (Recommended) | https://app.com | /account/login | Platform.select({ ios: '...', android: '...', web: undefined }) | Auto |$3
✅ DO:
- Use
PlatformType.Auto for automatic detection
- Set platform.redirectUri explicitly for React Native
- Keep redirectDomain and redirectPath as fallbacks for web
- Use Platform.select() for cross-platform apps
- Encode redirect URIs in OAuth URLs (library does this automatically)❌ DON'T:
- Hardcode platform detection (use
detectPlatform() instead)
- Forget to register redirect URIs in OAuth provider consoles
- Use web redirect URIs (https://) for mobile apps
- Skip Info.plist/AndroidManifest.xml configuration for deep links$3
Check which redirect URI is being used:
`typescript
import { getSocialLoginSettings } from 'rystem.authentication.social.react';const settings = getSocialLoginSettings();
const effectiveRedirectUri = settings.platform?.redirectUri
||
${settings.redirectDomain}${settings.redirectPath || ''};console.log('Platform Type:', settings.platform?.type);
console.log('Redirect URI:', effectiveRedirectUri);
console.log('Login Mode:', settings.platform?.loginMode || settings.loginMode);
`🆚 Popup vs Redirect Comparison
| Feature | Popup Mode | Redirect Mode |
|---------|-----------|---------------|
| Platform | Web only | Web + Mobile |
| User Experience | Stays on page | Leaves page temporarily |
| Browser Support | May be blocked | Always works |
| Mobile Apps | ❌ Not supported | ✅ Required |
| Session Persistence | ✅ Maintained | ⚠️ Depends on implementation |
| Security | ✅ Same-origin | ✅ PKCE required |
Error Handling
`typescript
setupSocialLogin(x => {
x.onLoginFailure = (error) => {
switch (error.code) {
case 3:
// Error during button click (client-side)
console.error('Client error:', error.message);
break;
case 15:
// Error during token retrieval from API
console.error('Token exchange failed:', error.message);
showNotification('Login failed. Please try again.');
break;
case 10:
// Error fetching user information from API
console.error('User fetch failed:', error.message);
break;
default:
console.error('Unknown error:', error);
}
};
});
`💾 Custom Storage Service
By default, the library uses localStorage for persisting tokens, PKCE verifiers, and user data. You can customize this for secure storage (mobile), testing, or server-side storage.
$3
The library uses the Decorator Pattern with separation between infrastructure and domain logic:
`
IStorageService (interface) ← Generic key-value storage
↓
LocalStorageService (default) ← Browser localStorage
↓
├── PkceStorageService ← PKCE OAuth logic
├── TokenStorageService ← Token + expiry logic
└── UserStorageService ← User data logic
`$3
No configuration needed - the library automatically uses
LocalStorageService:`typescript
import { setupSocialLogin } from 'rystem.authentication.social.react';setupSocialLogin(x => {
x.apiUri = "https://api.yourdomain.com";
// storageService is automatically initialized with LocalStorageService
x.microsoft.clientId = "your-client-id";
});
`$3
Implement
IStorageService for custom storage (secure storage, Redis, etc.):`typescript
import { setupSocialLogin, IStorageService } from 'rystem.authentication.social.react';// Example: Secure Storage for React Native
class SecureStorageService implements IStorageService {
async get(key: string): Promise {
try {
// Use expo-secure-store or react-native-keychain
return await SecureStore.getItemAsync(key);
} catch (error) {
console.error('SecureStorage get error:', error);
return null;
}
}
async set(key: string, value: string): Promise {
try {
await SecureStore.setItemAsync(key, value);
} catch (error) {
console.error('SecureStorage set error:', error);
}
}
async remove(key: string): Promise {
try {
await SecureStore.deleteItemAsync(key);
} catch (error) {
console.error('SecureStorage remove error:', error);
}
}
async has(key: string): Promise {
const value = await this.get(key);
return value !== null;
}
async clear(): Promise {
// Optional: implement if needed
}
}
// Configure custom storage
setupSocialLogin(x => {
x.apiUri = "https://api.yourdomain.com";
x.storageService = new SecureStorageService(); // Use secure storage
x.microsoft.clientId = "your-client-id";
});
`$3
Perfect for unit tests without persisting data:
`typescript
import { IStorageService } from 'rystem.authentication.social.react';class MockStorageService implements IStorageService {
private storage = new Map();
get(key: string): string | null {
return this.storage.get(key) ?? null;
}
set(key: string, value: string): void {
this.storage.set(key, value);
}
remove(key: string): void {
this.storage.delete(key);
}
has(key: string): boolean {
return this.storage.has(key);
}
clear(): void {
this.storage.clear();
}
}
// Use in tests
setupSocialLogin(x => {
x.storageService = new MockStorageService();
// ... rest of config
});
`$3
For server-side rendering or distributed systems:
`typescript
import { createClient } from 'redis';
import { IStorageService } from 'rystem.authentication.social.react';class RedisStorageService implements IStorageService {
private client = createClient({ url: 'redis://localhost:6379' });
constructor() {
this.client.connect();
}
async get(key: string): Promise {
return await this.client.get(key);
}
async set(key: string, value: string): Promise {
await this.client.set(key, value, { EX: 3600 }); // 1 hour expiry
}
async remove(key: string): Promise {
await this.client.del(key);
}
async has(key: string): Promise {
const exists = await this.client.exists(key);
return exists === 1;
}
async clear(): Promise {
await this.client.flushAll();
}
}
setupSocialLogin(x => {
x.storageService = new RedisStorageService();
// ... rest of config
});
`$3
The library stores data with these keys (backward-compatible):
| Key | Description | Service |
|-----|-------------|---------|
|
socialUserToken | JWT access token + expiry | TokenStorageService |
| socialUserToken_expiry | Token expiration timestamp | TokenStorageService |
| socialUser | User profile data | UserStorageService |
| rystem_pkce_{provider}_verifier | PKCE code verifier | PkceStorageService |
| rystem_pkce_{provider}_challenge | PKCE code challenge (optional) | PkceStorageService |$3
| Scenario | Recommended Storage |
|----------|-------------------|
| Web SPA |
LocalStorageService (default) |
| React Native Mobile | SecureStorageService (expo-secure-store) |
| Unit Testing | MockStorageService (in-memory) |
| Server-Side Rendering | RedisStorageService or DatabaseStorageService |
| Electron Apps | Custom storage with encryption |$3
Add encryption layer on top of any storage:
`typescript
class EncryptedStorageService implements IStorageService {
constructor(
private baseStorage: IStorageService,
private encryptionKey: string
) {}
async get(key: string): Promise {
const encrypted = await this.baseStorage.get(key);
if (!encrypted) return null;
return this.decrypt(encrypted, this.encryptionKey);
}
async set(key: string, value: string): Promise {
const encrypted = this.encrypt(value, this.encryptionKey);
await this.baseStorage.set(key, encrypted);
}
async remove(key: string): Promise {
await this.baseStorage.remove(key);
}
async has(key: string): Promise {
return await this.baseStorage.has(key);
}
private encrypt(text: string, key: string): string {
// Use crypto library (e.g., crypto-js)
return CryptoJS.AES.encrypt(text, key).toString();
}
private decrypt(ciphertext: string, key: string): string {
const bytes = CryptoJS.AES.decrypt(ciphertext, key);
return bytes.toString(CryptoJS.enc.Utf8);
}
}// Usage
const secureStorage = new LocalStorageService();
const encryptedStorage = new EncryptedStorageService(
secureStorage,
'your-encryption-key'
);
setupSocialLogin(x => {
x.storageService = encryptedStorage;
// ... rest of config
});
`STORAGE_ARCHITECTURE.md for detailed technical documentation.---
🧭 Custom Routing Service
🧭 Custom Routing Service
$3
Problem: Client-side routing frameworks (React Router, Next.js App Router, Remix) intercept native browser APIs, causing two critical issues:
1. OAuth Callback Detection:
window.location.search is empty even when URL contains parameters
2. Navigation Bypass: window.location.href and window.history.replaceState() bypass the router, losing routing stateSolution: The
IRoutingService abstraction provides a unified interface for:
- URL Parameter Reading (OAuth callback detection)
- Navigation Operations (redirects, return URLs, cleanup)$3
By default, the library uses
WindowRoutingService which uses native browser APIs:`typescript
// ✅ Works automatically with:
// - Vanilla React (no routing library)
// - Standard browser navigation
// - Server-side rendered apps
// - Next.js Pages Router (with server redirects)
setupSocialLogin(x => {
// No routingService config needed - uses WindowRoutingService by default
x.apiUri = 'https://api.example.com';
});
`$3
| Framework | Needs Custom? | Why? | Implementation |
|-----------|---------------|------|----------------|
| React Router | ✅ YES | Client-side routing intercepts window APIs |
ReactRouterRoutingService (see below) |
| Next.js App Router | ✅ YES | Uses router.push/replace for navigation | NextAppRouterRoutingService (see below) |
| Next.js Pages Router | ⚠️ MAYBE | Depends on navigation style | Test if return URLs work |
| Remix | ✅ YES | Uses @remix-run/react router | Similar to React Router |
| Vanilla React | ❌ No | No routing framework | Default works ✅ |
| Server-Side Rendering | ❌ No | Full page reloads | Default works ✅ |src/services/:
- ReactRouterRoutingService.example.ts - React Router v6+ (unified)
- NextAppRouterRoutingService.example.ts - Next.js App Router v13+ (unified)
- MockRoutingService.example.ts - Unit testing with verification methodsCopy these files to your project and remove the
.example extension.---
$3
If you're using React Router v6+, use this unified routing service:
`typescript
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { IRoutingService } from 'rystem.authentication.social.react';/**
* Unified Routing Service for React Router v6+
* Handles both URL reading and navigation
*/
export class ReactRouterRoutingService implements IRoutingService {
private searchParamsGetter: (() => URLSearchParams) | null = null;
private navigateFunc: ((to: string, options?: any) => void) | null = null;
private location: any = null;
/**
* Single initialization with all React Router hooks
*/
initialize(
searchParamsGetter: () => URLSearchParams,
navigateFunc: (to: string, options?: any) => void,
location: any
): void {
this.searchParamsGetter = searchParamsGetter;
this.navigateFunc = navigateFunc;
this.location = location;
}
// URL Parameter Reading (OAuth callbacks)
getSearchParam(key: string): string | null {
return this.searchParamsGetter?.().get(key) || null;
}
getAllSearchParams(): URLSearchParams {
return this.searchParamsGetter?.() || new URLSearchParams();
}
// Navigation Operations
getCurrentPath(): string {
return this.location
? this.location.pathname + this.location.search
: window.location.pathname + window.location.search;
}
navigateTo(url: string): void {
// External OAuth redirects must use window.location
if (url.startsWith('http')) {
window.location.href = url;
} else {
this.navigateFunc?.(url);
}
}
navigateReplace(path: string): void {
this.navigateFunc?.(path, { replace: true });
}
openPopup(url: string, name: string, features: string): Window | null {
return window.open(url, name, features);
}
}
`#### Usage with React Router
`typescript
import { BrowserRouter, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { setupSocialLogin, SocialLoginWrapper, MicrosoftButton } from 'rystem.authentication.social.react';
import { ReactRouterRoutingService } from './ReactRouterRoutingService';// Create singleton instance
const routingService = new ReactRouterRoutingService();
// Setup configuration ONCE at app startup
setupSocialLogin(x => {
x.apiUri = 'https://api.example.com';
x.routingService = routingService; // ✅ One service for everything
x.providers = [
{ provider: ProviderType.Microsoft, clientId: 'your-client-id' }
];
});
// Main App Component
function App() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
// ✅ Single initialization with all hooks
useEffect(() => {
routingService.initialize(() => searchParams, navigate, location);
}, [searchParams, navigate, location]);
return (
My App
);
}// Wrap with Router
const Root = () => (
);
export default Root;
`---
$3
For Next.js 13+ App Router with client components:
`typescript
'use client';import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { IRoutingService } from 'rystem.authentication.social.react';
/**
* Unified Routing Service for Next.js App Router
* Handles both URL reading and navigation
*/
export class NextAppRouterRoutingService implements IRoutingService {
private router: any = null;
private pathname: string | null = null;
private searchParams: URLSearchParams | null = null;
/**
* Single initialization with all Next.js hooks
*/
initialize(router: any, pathname: string, searchParams: URLSearchParams | null): void {
this.router = router;
this.pathname = pathname;
this.searchParams = searchParams;
}
// URL Parameter Reading (OAuth callbacks)
getSearchParam(key: string): string | null {
return this.searchParams?.get(key) || null;
}
getAllSearchParams(): URLSearchParams {
return this.searchParams || new URLSearchParams();
}
// Navigation Operations
getCurrentPath(): string {
if (!this.pathname) return window.location.pathname + window.location.search;
const search = this.searchParams?.toString();
return search ?
${this.pathname}?${search} : this.pathname;
} navigateTo(url: string): void {
// External OAuth redirects must use window.location
if (url.startsWith('http')) {
window.location.href = url;
} else {
this.router?.push(url);
}
}
navigateReplace(path: string): void {
this.router?.replace(path);
}
openPopup(url: string, name: string, features: string): Window | null {
return window.open(url, name, features);
}
}
`#### Usage with Next.js App Router
`typescript
'use client'; // ✅ Must be a Client Componentimport { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { setupSocialLogin, SocialLoginWrapper, MicrosoftButton } from 'rystem.authentication.social.react';
import { NextAppRouterRoutingService } from './NextAppRouterRoutingService';
// Create singleton instance
const routingService = new NextAppRouterRoutingService();
// Setup configuration ONCE
setupSocialLogin(x => {
x.apiUri = 'https://api.example.com';
x.routingService = routingService; // ✅ One service for everything
x.providers = [
{ provider: ProviderType.Microsoft, clientId: 'your-client-id' }
];
});
export default function LoginPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// ✅ Single initialization with all hooks
useEffect(() => {
routingService.initialize(router, pathname, searchParams);
}, [router, pathname, searchParams]);
return (
Login
);
}
`---
$3
For unit tests, use the mock service with verification methods:
`typescript
import { MockRoutingService } from './MockRoutingService';const mockRouting = new MockRoutingService();
// Setup test data
mockRouting.setSearchParam('code', 'test-auth-code');
mockRouting.setSearchParam('state', 'microsoft');
mockRouting.setCurrentPath('/account/login?tab=oauth');
setupSocialLogin(x => {
x.routingService = mockRouting;
// ... rest of test config
});
// Run OAuth flow in test
// ...
// Verify navigation behavior
expect(mockRouting.wasNavigateToCalledWith('https://oauth.provider.com')).toBe(true);
expect(mockRouting.wasReplaceCalledWith('/dashboard')).toBe(true);
expect(mockRouting.getNavigationHistory()).toEqual([
'https://oauth.provider.com',
'/dashboard'
]);
`---
$3
1. Single Initialization: Initialize routing service with ALL framework hooks in one call (not separate calls like before).
2. Singleton Pattern: Create ONE instance and reuse it. Don't create new instances on every render.
3. Effect Dependencies: Always include routing hooks in
useEffect dependency array:
`typescript
useEffect(() => {
routingService.initialize(/ hooks /);
}, [searchParams, navigate, location]); // ✅ All deps
`4. External OAuth URLs: OAuth redirects to external providers (e.g.,
https://login.microsoftonline.com) MUST use window.location.href regardless of framework.5. Return URL Feature: The routing service handles saving the current page before OAuth and returning after login. If this doesn't work, check console for initialization warnings.
---
$3
Check your routing service is properly initialized:
`typescript
console.log('Routing Service:', settings.routingService.constructor.name);
console.log('Current Path:', settings.routingService.getCurrentPath());
console.log('OAuth Code:', settings.routingService.getSearchParam('code'));
`You'll see these logs in
SocialLoginWrapper during OAuth callbacks.---
$3
Before (without custom routing service):
`typescript
// ❌ PROBLEM 1: OAuth callback params not found
const code = new URLSearchParams(window.location.search).get('code');
// Returns null even though URL is: /login?code=ABC&state=microsoft
// (React Router intercepts client-side navigation)// ❌ PROBLEM 2: Navigation bypasses router
window.location.href = 'https://oauth.provider.com'; // Works but...
// ... later:
window.history.replaceState({}, '', '/dashboard'); // ❌ React Router doesn't know!
// Result: URL changes but component doesn't update, state lost
`After (with custom routing service):
`typescript
// ✅ SOLUTION 1: Framework-aware URL reading
const code = routingService.getSearchParam('code');
// Uses React Router's useSearchParams internally
// Returns 'ABC' correctly!// ✅ SOLUTION 2: Framework-aware navigation
routingService.navigateTo('https://oauth.provider.com'); // External, uses window.location
// ... later:
routingService.navigateReplace('/dashboard'); // ✅ Calls navigate(path, {replace: true})
// Result: React Router updates correctly, components re-render!
`---
$3
| Feature | Before (v0.3.x) | After (v0.4.0) |
|---------|-----------------|----------------|
| Services |
IUrlService + INavigationService | IRoutingService (unified) ✅ |
| Settings | 2 fields (urlService, navigationService) | 1 field (routingService) ✅ |
| Initialization | 2 separate calls | 1 unified call ✅ |
| Hooks | Split across 2 services | All in one place ✅ |
| Example Files | 8 files (4 URL + 4 Nav) | 3 files (unified) ✅ |
| Complexity | Higher (duplicate patterns) | Lower (single pattern) ✅ |---
$3
`typescript
import { useSocialToken } from 'rystem.authentication.social.react';const MyComponent = () => {
const token = useSocialToken();
const fetchProtectedData = async () => {
if (token.isExpired) {
alert('Please login first');
return;
}
try {
const response = await fetch('https://api.example.com/protected', {
headers: {
'Authorization':
Bearer ${token.accessToken},
'Content-Type': 'application/json'
}
}); if (response.status === 401) {
// Token might be expired, force refresh
const forceRefresh = useContext(SocialLoginContextRefresh);
await forceRefresh();
// Retry request
}
const data = await response.json();
return data;
} catch (error) {
console.error('API error:', error);
}
};
return ;
};
`$3
`typescript
interface CustomSocialUser {
username: string;
isAuthenticated: boolean;
displayName: string;
avatar: string;
roles: string[];
}const MyComponent = () => {
const user = useSocialUser();
return (

{user.displayName}
Roles: {user.roles.join(', ')}
);
};
`🎨 Dark Mode & Theming
The modern social login buttons support automatic dark mode with three detection methods:
$3
The buttons automatically adapt to the user's system preference:
`css
/ No JavaScript needed - CSS handles it automatically /
@media (prefers-color-scheme: dark) {
/ Dark mode styles applied automatically /
}
`$3
Control the theme programmatically by setting the
data-theme attribute on any parent element:`typescript
import { MicrosoftButton } from 'rystem.authentication.social.react';export const ThemedLoginPage = () => {
const [isDark, setIsDark] = useState(false);
return (
);
};
`$3
Use CSS classes for framework integration (Tailwind, etc.):
`typescript
export const TailwindThemedLogin = () => {
return (
{/ Tailwind dark mode /}
);
};
`$3
The buttons check for dark mode in this order:
1.
[data-theme="dark"] attribute (highest priority)
2. .dark-mode CSS class
3. @media (prefers-color-scheme: dark) system preference (fallback)$3
Override the default colors using CSS variables:
`css
/ Light mode customization /
:root {
--rsb-microsoft-bg: #2f2f2f;
--rsb-microsoft-color: #ffffff;
--rsb-hover-brightness: 1.1;
}/ Dark mode customization /
[data-theme="dark"] {
--rsb-microsoft-bg: #404040;
--rsb-microsoft-color: #e0e0e0;
--rsb-hover-brightness: 1.15;
}
/ Specific provider override /
[data-theme="dark"] .rystem-social-button--google {
background: linear-gradient(135deg, #434343 0%, #363636 100%);
}
`$3
| Variable | Default (Light) | Default (Dark) | Description |
|----------|----------------|----------------|-------------|
|
--rsb-background | Provider brand color | Darker shade | Button background |
| --rsb-text | #ffffff | #e0e0e0 | Button text color |
| --rsb-hover-brightness | 1.05 | 1.15 | Hover effect intensity |
| --rsb-focus-ring | Provider color | Lighter shade | Keyboard focus outline |
| --rsb-shadow | rgba(0,0,0,0.1) | rgba(0,0,0,0.3) | Button shadow |
| --rsb-disabled-opacity | 0.6 | 0.5 | Disabled state opacity |$3
#### Next.js with
next-themes`typescript
import { useTheme } from 'next-themes';
import { SocialLoginButtons } from 'rystem.authentication.social.react';export const NextJsLogin = () => {
const { theme, setTheme } = useTheme();
return (
);
};
`#### React Context Theme Provider
`typescript
const ThemeContext = createContext({ isDark: false, toggle: () => {} });export const ThemeProvider = ({ children }) => {
const [isDark, setIsDark] = useState(
() => window.matchMedia('(prefers-color-scheme: dark)').matches
);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => setIsDark(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
return (
setIsDark(!isDark) }}>
{children}
);
};
// Usage
export const App = () => (
);
`#### Tailwind CSS Integration
`typescript
// tailwind.config.js
module.exports = {
darkMode: 'class', // Enable class-based dark mode
// ...
};// Component
export const TailwindLogin = () => {
const [darkMode, setDarkMode] = useState(false);
return (
onClick={() => setDarkMode(!darkMode)}
className="mb-4 px-4 py-2 bg-gray-200 dark:bg-gray-700"
>
Toggle Dark Mode
{/ Buttons automatically adapt to .dark class /}
);
};
`$3
The buttons maintain WCAG 2.1 AA contrast ratios in both light and dark modes:
- ✅ Light mode: 4.5:1 minimum contrast
- ✅ Dark mode: 4.5:1 minimum contrast
- ✅ Focus indicators: 3:1 contrast with background
- ✅ Hover states: Clearly visible in both modes
$3
`typescript
import { render } from '@testing-library/react';
import { MicrosoftButton } from 'rystem.authentication.social.react';test('button renders correctly in dark mode', () => {
const { container } = render(
);
const button = container.querySelector('.rystem-social-button--microsoft');
expect(button).toBeInTheDocument();
// Check computed styles
const styles = window.getComputedStyle(button);
expect(styles.backgroundColor).toBeTruthy();
});
`📝 Complete Example
``typescriptconst customButtons = [MicrosoftButton, GoogleButton, GitHubButton];
export const Dashboard = () => {
const token = useSocialToken();
const user = useSocialUser();
const forceRefresh = useContext(SocialLoginContextRefresh);
const logout = useContext(SocialLoginContextLogout);
const [count, setCount] = useState(0);
return (