rystem.authentication.social.client 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +1903 -0
  2. package/lib/buttons/CreateSocialButton.d.ts +15 -0
  3. package/lib/buttons/CreateSocialButton.js +158 -0
  4. package/lib/buttons/SocialLoginButtons.d.ts +2 -0
  5. package/lib/buttons/SocialLoginButtons.js +22 -0
  6. package/lib/buttons/SocialLogoutButton.d.ts +3 -0
  7. package/lib/buttons/SocialLogoutButton.js +11 -0
  8. package/lib/buttons/graphics/AmazonLoginButton.d.ts +1 -0
  9. package/lib/buttons/graphics/AmazonLoginButton.js +10 -0
  10. package/lib/buttons/graphics/AppleLoginButton.d.ts +1 -0
  11. package/lib/buttons/graphics/AppleLoginButton.js +17 -0
  12. package/lib/buttons/graphics/BufferLoginButton.d.ts +1 -0
  13. package/lib/buttons/graphics/BufferLoginButton.js +17 -0
  14. package/lib/buttons/graphics/DiscordLoginButton.d.ts +1 -0
  15. package/lib/buttons/graphics/DiscordLoginButton.js +17 -0
  16. package/lib/buttons/graphics/FacebookLoginButton.d.ts +1 -0
  17. package/lib/buttons/graphics/FacebookLoginButton.js +10 -0
  18. package/lib/buttons/graphics/GithubLoginButton.d.ts +1 -0
  19. package/lib/buttons/graphics/GithubLoginButton.js +10 -0
  20. package/lib/buttons/graphics/GoogleLoginButton.d.ts +1 -0
  21. package/lib/buttons/graphics/GoogleLoginButton.js +10 -0
  22. package/lib/buttons/graphics/InstagramLoginButton.d.ts +1 -0
  23. package/lib/buttons/graphics/InstagramLoginButton.js +10 -0
  24. package/lib/buttons/graphics/LinkedInLoginButton.d.ts +1 -0
  25. package/lib/buttons/graphics/LinkedInLoginButton.js +10 -0
  26. package/lib/buttons/graphics/MetamaskLoginButton.d.ts +1 -0
  27. package/lib/buttons/graphics/MetamaskLoginButton.js +25 -0
  28. package/lib/buttons/graphics/MicrosoftLoginButton.d.ts +1 -0
  29. package/lib/buttons/graphics/MicrosoftLoginButton.js +10 -0
  30. package/lib/buttons/graphics/OktaLoginButton.d.ts +1 -0
  31. package/lib/buttons/graphics/OktaLoginButton.js +17 -0
  32. package/lib/buttons/graphics/PinterestLoginButton.d.ts +1 -0
  33. package/lib/buttons/graphics/PinterestLoginButton.js +10 -0
  34. package/lib/buttons/graphics/SlackLoginButton.d.ts +1 -0
  35. package/lib/buttons/graphics/SlackLoginButton.js +17 -0
  36. package/lib/buttons/graphics/SocialButtonStyle.d.ts +8 -0
  37. package/lib/buttons/graphics/SocialButtonStyle.js +2 -0
  38. package/lib/buttons/graphics/SocialLoginButton.d.ts +2 -0
  39. package/lib/buttons/graphics/SocialLoginButton.js +46 -0
  40. package/lib/buttons/graphics/TelegramLoginButton.d.ts +1 -0
  41. package/lib/buttons/graphics/TelegramLoginButton.js +17 -0
  42. package/lib/buttons/graphics/TikTokLoginButton.d.ts +1 -0
  43. package/lib/buttons/graphics/TikTokLoginButton.js +10 -0
  44. package/lib/buttons/graphics/XLoginButton.d.ts +1 -0
  45. package/lib/buttons/graphics/XLoginButton.js +10 -0
  46. package/lib/buttons/graphics/YahooLoginButton.d.ts +1 -0
  47. package/lib/buttons/graphics/YahooLoginButton.js +17 -0
  48. package/lib/buttons/graphics/ZaloLoginButton.d.ts +1 -0
  49. package/lib/buttons/graphics/ZaloLoginButton.js +17 -0
  50. package/lib/buttons/singles/AmazonButton.d.ts +2 -0
  51. package/lib/buttons/singles/AmazonButton.js +33 -0
  52. package/lib/buttons/singles/FacebookButton.d.ts +2 -0
  53. package/lib/buttons/singles/FacebookButton.js +55 -0
  54. package/lib/buttons/singles/GitHubButton.d.ts +2 -0
  55. package/lib/buttons/singles/GitHubButton.js +19 -0
  56. package/lib/buttons/singles/GoogleButton.d.ts +2 -0
  57. package/lib/buttons/singles/GoogleButton.js +44 -0
  58. package/lib/buttons/singles/InstagramButton.d.ts +2 -0
  59. package/lib/buttons/singles/InstagramButton.js +21 -0
  60. package/lib/buttons/singles/LinkedinButton.d.ts +2 -0
  61. package/lib/buttons/singles/LinkedinButton.js +23 -0
  62. package/lib/buttons/singles/MicrosoftButton.d.ts +2 -0
  63. package/lib/buttons/singles/MicrosoftButton.js +68 -0
  64. package/lib/buttons/singles/PinterestButton.d.ts +2 -0
  65. package/lib/buttons/singles/PinterestButton.js +22 -0
  66. package/lib/buttons/singles/TikTokButton.d.ts +2 -0
  67. package/lib/buttons/singles/TikTokButton.js +18 -0
  68. package/lib/buttons/singles/XButton.d.ts +2 -0
  69. package/lib/buttons/singles/XButton.js +24 -0
  70. package/lib/components/BrandIcons.d.ts +64 -0
  71. package/lib/components/BrandIcons.js +76 -0
  72. package/lib/components/ModernSocialButton.d.ts +21 -0
  73. package/lib/components/ModernSocialButton.js +23 -0
  74. package/lib/context/SocialLoginContext.d.ts +4 -0
  75. package/lib/context/SocialLoginContext.js +10 -0
  76. package/lib/context/SocialLoginWrapper.d.ts +3 -0
  77. package/lib/context/SocialLoginWrapper.js +137 -0
  78. package/lib/hooks/removeSocialLogin.d.ts +1 -0
  79. package/lib/hooks/removeSocialLogin.js +14 -0
  80. package/lib/hooks/useSocialToken.d.ts +2 -0
  81. package/lib/hooks/useSocialToken.js +21 -0
  82. package/lib/hooks/useSocialUser.d.ts +2 -0
  83. package/lib/hooks/useSocialUser.js +30 -0
  84. package/lib/index.d.ts +47 -0
  85. package/lib/index.js +90 -0
  86. package/lib/models/SocialButtonProps.d.ts +3 -0
  87. package/lib/models/SocialButtonProps.js +2 -0
  88. package/lib/models/SocialButtonsProps.d.ts +4 -0
  89. package/lib/models/SocialButtonsProps.js +2 -0
  90. package/lib/models/SocialToken.d.ts +6 -0
  91. package/lib/models/SocialToken.js +2 -0
  92. package/lib/models/SocialUser.d.ts +5 -0
  93. package/lib/models/SocialUser.js +2 -0
  94. package/lib/models/Token.d.ts +6 -0
  95. package/lib/models/Token.js +2 -0
  96. package/lib/models/setup/LoginMode.d.ts +13 -0
  97. package/lib/models/setup/LoginMode.js +17 -0
  98. package/lib/models/setup/PlatformConfig.d.ts +45 -0
  99. package/lib/models/setup/PlatformConfig.js +19 -0
  100. package/lib/models/setup/PlatformType.d.ts +21 -0
  101. package/lib/models/setup/PlatformType.js +25 -0
  102. package/lib/models/setup/ProviderType.d.ts +13 -0
  103. package/lib/models/setup/ProviderType.js +17 -0
  104. package/lib/models/setup/SocialLoginErrorResponse.d.ts +6 -0
  105. package/lib/models/setup/SocialLoginErrorResponse.js +2 -0
  106. package/lib/models/setup/SocialLoginSettings.d.ts +66 -0
  107. package/lib/models/setup/SocialLoginSettings.js +2 -0
  108. package/lib/models/setup/SocialParameter.d.ts +3 -0
  109. package/lib/models/setup/SocialParameter.js +2 -0
  110. package/lib/services/IRoutingService.d.ts +123 -0
  111. package/lib/services/IRoutingService.js +2 -0
  112. package/lib/services/IStorageService.d.ts +33 -0
  113. package/lib/services/IStorageService.js +2 -0
  114. package/lib/services/LocalStorageService.d.ts +32 -0
  115. package/lib/services/LocalStorageService.js +93 -0
  116. package/lib/services/MockRoutingService.example.d.ts +96 -0
  117. package/lib/services/MockRoutingService.example.js +153 -0
  118. package/lib/services/NextAppRouterRoutingService.example.d.ts +75 -0
  119. package/lib/services/NextAppRouterRoutingService.example.js +128 -0
  120. package/lib/services/PkceStorageService.d.ts +55 -0
  121. package/lib/services/PkceStorageService.js +103 -0
  122. package/lib/services/ReactRouterRoutingService.example.d.ts +69 -0
  123. package/lib/services/ReactRouterRoutingService.example.js +121 -0
  124. package/lib/services/TokenStorageService.d.ts +34 -0
  125. package/lib/services/TokenStorageService.js +90 -0
  126. package/lib/services/UserStorageService.d.ts +29 -0
  127. package/lib/services/UserStorageService.js +56 -0
  128. package/lib/services/WindowRoutingService.d.ts +74 -0
  129. package/lib/services/WindowRoutingService.js +118 -0
  130. package/lib/setup/SocialLoginManager.d.ts +12 -0
  131. package/lib/setup/SocialLoginManager.js +106 -0
  132. package/lib/setup/getSocialLoginSettings.d.ts +2 -0
  133. package/lib/setup/getSocialLoginSettings.js +8 -0
  134. package/lib/setup/setupSocialLogin.d.ts +2 -0
  135. package/lib/setup/setupSocialLogin.js +62 -0
  136. package/lib/styles/SocialButton.css +365 -0
  137. package/lib/utils/pkce.d.ts +18 -0
  138. package/lib/utils/pkce.js +44 -0
  139. package/lib/utils/platform.d.ts +30 -0
  140. package/lib/utils/platform.js +103 -0
  141. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,1903 @@
1
+ ### [What is Rystem?](https://github.com/KeyserDSoze/Rystem)
2
+
3
+ # rystem.authentication.social.client
4
+
5
+ Framework-agnostic TypeScript library for social authentication with built-in PKCE support for secure OAuth 2.0 flows.
6
+
7
+ **Works with**: React, React Native, Next.js, Expo, Remix, and any JavaScript/TypeScript framework.
8
+
9
+ ### ✨ Key Features
10
+
11
+ - **🔐 PKCE Built-in**: Automatic code_verifier generation for Microsoft OAuth (RFC 7636)
12
+ - **⚛️ React Hooks**: Type-safe hooks for token and user management
13
+ - **🎨 Ready-to-Use Components**: Login buttons, logout, authentication wrapper (React only)
14
+ - **🔄 Automatic Token Refresh**: Handles token expiration seamlessly
15
+ - **📱 Multi-Platform**: Web (React, Next.js), Mobile (React Native, Expo), and any framework via interfaces
16
+ - **🔌 Framework-Agnostic Core**: Inject custom storage and routing services for any platform
17
+
18
+ ## 🆕 What's New - Mobile Platform Support
19
+
20
+ **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.
21
+
22
+ ### Supported Platforms & Providers
23
+
24
+ | Provider | Web (Popup) | Web (Redirect) | React Native iOS | React Native Android | PKCE Support |
25
+ |----------|-------------|----------------|------------------|---------------------|--------------|
26
+ | Microsoft | ✅ | ✅ | ✅ | ✅ | ✅ |
27
+ | Google | ✅ | ✅ | ✅ | ✅ | - |
28
+ | Facebook | ✅ | ✅ | ✅ | ✅ | - |
29
+ | GitHub | ✅ | ✅ | ✅ | ✅ | - |
30
+ | Amazon | ✅ | ✅ | ✅ | ✅ | - |
31
+ | LinkedIn | ✅ | ✅ | ✅ | ✅ | - |
32
+ | X (Twitter) | ✅ | ✅ | ✅ | ✅ | - |
33
+ | TikTok | ✅ | ✅ | ✅ | ✅ | - |
34
+ | Instagram | ✅ | ✅ | ✅ | ✅ | - |
35
+ | Pinterest | ✅ | ✅ | ✅ | ✅ | - |
36
+
37
+ ### How It Works
38
+
39
+ 1. **Auto-Detection**: Library automatically detects platform (Web/iOS/Android) from navigator.userAgent
40
+ 2. **Platform-Specific URIs**: Configure custom redirect URIs per platform (e.g., `msauth://` for iOS, `myapp://` for Android)
41
+ 3. **Login Modes**: Choose Popup (web) or Redirect (mobile) behavior
42
+ 4. **Deep Links**: All buttons support mobile deep link OAuth callbacks
43
+ 5. **No Breaking Changes**: Existing web apps work without modification
44
+
45
+ ### Quick Example
46
+
47
+ ```typescript
48
+ import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.client';
49
+ import { Platform } from 'react-native'; // Only in React Native projects
50
+
51
+ setupSocialLogin(x => {
52
+ x.apiUri = "https://api.yourdomain.com";
53
+
54
+ // Platform configuration (auto-detects if not specified)
55
+ x.platform = {
56
+ type: PlatformType.Auto,
57
+
58
+ // Smart redirect path (auto-detects domain for web)
59
+ redirectPath: Platform.select({
60
+ ios: 'msauth://com.yourapp.bundle/auth', // Complete URI for mobile
61
+ android: 'myapp://oauth/callback', // Complete URI for mobile
62
+ web: '/account/login' // Path only (auto-detects domain)
63
+ }),
64
+
65
+ // Login mode (auto-set based on platform if not specified)
66
+ loginMode: Platform.select({
67
+ ios: LoginMode.Redirect,
68
+ android: LoginMode.Redirect,
69
+ web: LoginMode.Popup
70
+ })
71
+ };
72
+
73
+ x.microsoft.clientId = "your-client-id";
74
+ x.google.clientId = "your-client-id";
75
+ });
76
+ ```
77
+
78
+ 📖 **Full Migration Guide**: See [`PLATFORM_SUPPORT.md`](https://github.com/KeyserDSoze/Rystem/blob/master/src/Authentication/PLATFORM_SUPPORT.md) for detailed setup instructions, OAuth provider configuration, and troubleshooting.
79
+
80
+ ## 📦 Installation
81
+
82
+ ```bash
83
+ npm install rystem.authentication.social.client
84
+ # or
85
+ yarn add rystem.authentication.social.client
86
+ # or
87
+ pnpm add rystem.authentication.social.client
88
+ ```
89
+
90
+ ### 🚀 React Native Setup
91
+
92
+ The library works with **React Native** without any polyfills! You just need to provide custom implementations for:
93
+
94
+ 1. **Storage Service** (use AsyncStorage or Secure Storage instead of localStorage)
95
+ 2. **Routing Service** (use Linking API for deep links)
96
+
97
+ #### Example React Native Setup
98
+
99
+ ```typescript
100
+ // services/ReactNativeStorageService.ts
101
+ import AsyncStorage from '@react-native-async-storage/async-storage';
102
+ import { IStorageService } from 'rystem.authentication.social.react';
103
+
104
+ export class ReactNativeStorageService implements IStorageService {
105
+ async get(key: string): Promise<string | null> {
106
+ try {
107
+ return await AsyncStorage.getItem(key);
108
+ } catch (error) {
109
+ console.error('AsyncStorage get error:', error);
110
+ return null;
111
+ }
112
+ }
113
+
114
+ async set(key: string, value: string): Promise<void> {
115
+ try {
116
+ await AsyncStorage.setItem(key, value);
117
+ } catch (error) {
118
+ console.error('AsyncStorage set error:', error);
119
+ }
120
+ }
121
+
122
+ async remove(key: string): Promise<void> {
123
+ try {
124
+ await AsyncStorage.removeItem(key);
125
+ } catch (error) {
126
+ console.error('AsyncStorage remove error:', error);
127
+ }
128
+ }
129
+
130
+ async has(key: string): Promise<boolean> {
131
+ const value = await this.get(key);
132
+ return value !== null;
133
+ }
134
+
135
+ async clear(): Promise<void> {
136
+ try {
137
+ await AsyncStorage.clear();
138
+ } catch (error) {
139
+ console.error('AsyncStorage clear error:', error);
140
+ }
141
+ }
142
+ }
143
+
144
+ // services/ReactNativeRoutingService.ts
145
+ import { Linking } from 'react-native';
146
+ import { IRoutingService } from 'rystem.authentication.social.react';
147
+
148
+ export class ReactNativeRoutingService implements IRoutingService {
149
+ private currentUrl: URL | null = null;
150
+
151
+ constructor() {
152
+ // Parse initial URL
153
+ Linking.getInitialURL().then(url => {
154
+ if (url) this.currentUrl = new URL(url);
155
+ });
156
+
157
+ // Listen for deep link events
158
+ Linking.addEventListener('url', ({ url }) => {
159
+ this.currentUrl = new URL(url);
160
+ });
161
+ }
162
+
163
+ getSearchParam(key: string): string | null {
164
+ if (!this.currentUrl) return null;
165
+ return this.currentUrl.searchParams.get(key);
166
+ }
167
+
168
+ getAllSearchParams(): URLSearchParams {
169
+ if (!this.currentUrl) return new URLSearchParams();
170
+ return this.currentUrl.searchParams;
171
+ }
172
+
173
+ getCurrentPath(): string {
174
+ if (!this.currentUrl) return '/';
175
+ return this.currentUrl.pathname + this.currentUrl.search;
176
+ }
177
+
178
+ navigateTo(url: string): void {
179
+ // For OAuth URLs, open in browser
180
+ Linking.openURL(url);
181
+ }
182
+
183
+ navigateReplace(path: string): void {
184
+ // React Native navigation - implement with your router (React Navigation, Expo Router)
185
+ console.log('Navigate to:', path);
186
+ }
187
+
188
+ openPopup(url: string, name: string, features: string): Window | null {
189
+ // React Native doesn't support popups - use in-app browser
190
+ Linking.openURL(url);
191
+ return null;
192
+ }
193
+ }
194
+
195
+ // App setup
196
+ import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';
197
+ import { ReactNativeStorageService } from './services/ReactNativeStorageService';
198
+ import { ReactNativeRoutingService } from './services/ReactNativeRoutingService';
199
+
200
+ setupSocialLogin(x => {
201
+ x.apiUri = "https://api.yourdomain.com";
202
+
203
+ // ✅ Provide React Native implementations
204
+ x.storageService = new ReactNativeStorageService();
205
+ x.routingService = new ReactNativeRoutingService();
206
+
207
+ // Platform configuration
208
+ x.platform = {
209
+ type: PlatformType.Auto, // Auto-detects iOS/Android
210
+ redirectPath: Platform.select({
211
+ ios: 'myapp://oauth/callback',
212
+ android: 'myapp://oauth/callback',
213
+ default: '/account/login'
214
+ }),
215
+ loginMode: LoginMode.Redirect // Always redirect for mobile
216
+ };
217
+
218
+ x.microsoft.clientId = "your-client-id";
219
+ x.google.clientId = "your-client-id";
220
+
221
+ x.onLoginFailure = (error) => {
222
+ Alert.alert('Login Failed', error.message);
223
+ };
224
+ });
225
+ ```
226
+
227
+ **Why No Polyfills?**
228
+ - ✅ The library now checks `typeof window !== 'undefined'` before accessing browser APIs
229
+ - ✅ You inject platform-specific implementations via `IStorageService` and `IRoutingService`
230
+ - ✅ No need for hacky polyfills or modifying `global` object
231
+
232
+ ### ⚠️ Important for React Router / Next.js Users
233
+
234
+ 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.
235
+
236
+ **👉 Solution**: Implement a custom `IRoutingService` for your framework.
237
+
238
+ 📖 **See full guide**: [🧭 Custom Routing Service](#-custom-routing-service) section below with ready-to-use implementations for:
239
+ - React Router v6+
240
+ - Next.js App Router (v13+)
241
+ - Unit Testing
242
+
243
+ ## 🚀 Quick Start
244
+
245
+ ### 1. Setup Configuration (main.tsx)
246
+
247
+ ```typescript
248
+ import { SocialLoginWrapper, setupSocialLogin } from 'rystem.authentication.social.react';
249
+ import App from './App';
250
+
251
+ setupSocialLogin(x => {
252
+ // API server URL
253
+ x.apiUri = "https://localhost:7017";
254
+
255
+ // Optional: Custom redirect path (default: "/account/login")
256
+ x.platform = {
257
+ redirectPath: "/account/login" // Auto-detects domain
258
+ };
259
+
260
+ // Configure OAuth providers (only clientId needed for client-side)
261
+ x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
262
+ x.google.clientId = "23769141170-lfs24avv5qrj00m4cbmrm202c0fc6gcg.apps.googleusercontent.com";
263
+ x.facebook.clientId = "345885718092912";
264
+ x.github.clientId = "97154d062f2bb5d28620";
265
+ x.amazon.clientId = "amzn1.application-oa2-client.dffbc466d62c44e49d71ad32f4aecb62";
266
+
267
+ // Error handling callback
268
+ x.onLoginFailure = (error) => {
269
+ console.error(`Login failed: ${error.message} (Code: ${error.code})`);
270
+ alert(`Authentication error: ${error.message}`);
271
+ };
272
+
273
+ // Automatic token refresh when expired
274
+ x.automaticRefresh = true;
275
+ });
276
+
277
+ function Root() {
278
+ return (
279
+ <SocialLoginWrapper>
280
+ <App />
281
+ </SocialLoginWrapper>
282
+ );
283
+ }
284
+
285
+ export default Root;
286
+ ```
287
+
288
+ ### 2. Use in Components
289
+
290
+ ```typescript
291
+ import { useSocialToken, useSocialUser, SocialLoginButtons, SocialLogoutButton } from 'rystem.authentication.social.react';
292
+
293
+ export const App = () => {
294
+ const token = useSocialToken();
295
+ const user = useSocialUser();
296
+
297
+ return (
298
+ <div>
299
+ {token.isExpired ? (
300
+ <div>
301
+ <h3>Please login</h3>
302
+ <SocialLoginButtons />
303
+ </div>
304
+ ) : (
305
+ <div>
306
+ <h3>Welcome, {user.username}</h3>
307
+ <p>Access Token: {token.accessToken}</p>
308
+ <SocialLogoutButton>Logout</SocialLogoutButton>
309
+ </div>
310
+ )}
311
+ </div>
312
+ );
313
+ };
314
+ ```
315
+
316
+ ## 🔐 PKCE Support (Microsoft OAuth)
317
+
318
+ ### Automatic PKCE Implementation
319
+
320
+ The library **automatically** implements PKCE for Microsoft OAuth:
321
+
322
+ 1. **Code Verifier Generation**: When user clicks Microsoft login button
323
+ ```typescript
324
+ const codeVerifier = await generateCodeVerifier(); // 43-128 chars random string
325
+ const codeChallenge = await generateCodeChallenge(codeVerifier); // SHA256 hash
326
+ ```
327
+
328
+ 2. **Session Storage**: Stores `code_verifier` for callback retrieval
329
+ ```typescript
330
+ storeCodeVerifier('microsoft', codeVerifier);
331
+ ```
332
+
333
+ 3. **OAuth Request**: Sends `code_challenge` with S256 method
334
+ ```
335
+ https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
336
+ ?client_id={clientId}
337
+ &response_type=code
338
+ &redirect_uri={redirectUri}
339
+ &code_challenge={codeChallenge}
340
+ &code_challenge_method=S256
341
+ ```
342
+
343
+ 4. **Token Exchange**: Sends `code_verifier` to API server
344
+ ```typescript
345
+ POST /api/Authentication/Social/Token?provider=Microsoft&code={code}&redirectPath=/account/login
346
+ Body: { "code_verifier": "original-verifier" }
347
+ ```
348
+
349
+ 5. **Cleanup**: Removes verifier from sessionStorage after use
350
+
351
+ ### Manual PKCE Usage
352
+
353
+ For custom implementations:
354
+
355
+ ```typescript
356
+ import { generateCodeVerifier, generateCodeChallenge, storeCodeVerifier, getAndRemoveCodeVerifier } from 'rystem.authentication.social.react';
357
+
358
+ // Generate PKCE values
359
+ const codeVerifier = await generateCodeVerifier();
360
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
361
+
362
+ // Store for later retrieval
363
+ storeCodeVerifier('custom-provider', codeVerifier);
364
+
365
+ // Build OAuth URL with code_challenge
366
+ const authUrl = `https://oauth.provider.com/authorize?code_challenge=${codeChallenge}&code_challenge_method=S256`;
367
+ window.location.href = authUrl;
368
+
369
+ // After OAuth callback, retrieve and remove verifier
370
+ const storedVerifier = getAndRemoveCodeVerifier('custom-provider');
371
+ ```
372
+
373
+ ## 🎣 React Hooks
374
+
375
+ ### useSocialToken
376
+
377
+ Get current JWT token for API requests:
378
+
379
+ ```typescript
380
+ const token = useSocialToken();
381
+
382
+ interface Token {
383
+ accessToken: string; // JWT bearer token
384
+ refreshToken: string; // Refresh token for renewal
385
+ isExpired: boolean; // True if token expired
386
+ expiresIn: Date; // Token expiration timestamp
387
+ }
388
+
389
+ // Usage in API calls
390
+ if (!token.isExpired) {
391
+ const response = await fetch('/api/orders', {
392
+ headers: {
393
+ 'Authorization': `Bearer ${token.accessToken}`
394
+ }
395
+ });
396
+ }
397
+ ```
398
+
399
+ ### useSocialUser
400
+
401
+ Get authenticated user information:
402
+
403
+ ```typescript
404
+ const user = useSocialUser();
405
+
406
+ interface SocialUser {
407
+ username: string; // User's email/username
408
+ isAuthenticated: boolean; // True if user is logged in
409
+ // Add custom properties from your API
410
+ }
411
+
412
+ if (user.isAuthenticated) {
413
+ console.log(`Logged in as: ${user.username}`);
414
+ }
415
+ ```
416
+
417
+ ### useContext(SocialLoginContextRefresh)
418
+
419
+ Force token refresh:
420
+
421
+ ```typescript
422
+ import { useContext } from 'react';
423
+ import { SocialLoginContextRefresh } from 'rystem.authentication.social.react';
424
+
425
+ const forceRefresh = useContext(SocialLoginContextRefresh);
426
+
427
+ const handleRefresh = async () => {
428
+ await forceRefresh();
429
+ console.log('Token refreshed!');
430
+ };
431
+ ```
432
+
433
+ ### useContext(SocialLoginContextLogout)
434
+
435
+ Programmatic logout:
436
+
437
+ ```typescript
438
+ import { useContext } from 'react';
439
+ import { SocialLoginContextLogout } from 'rystem.authentication.social.react';
440
+
441
+ const logout = useContext(SocialLoginContextLogout);
442
+
443
+ const handleLogout = async () => {
444
+ await logout();
445
+ window.location.href = '/login';
446
+ };
447
+ ```
448
+
449
+ ## 🎨 UI Components
450
+
451
+ ### SocialLoginButtons
452
+
453
+ Renders all configured provider buttons:
454
+
455
+ ```typescript
456
+ import { SocialLoginButtons } from 'rystem.authentication.social.react';
457
+
458
+ <SocialLoginButtons />
459
+ ```
460
+
461
+ ### Custom Button Order
462
+
463
+ ```typescript
464
+ import {
465
+ SocialLoginButtons,
466
+ MicrosoftButton,
467
+ GoogleButton,
468
+ FacebookButton,
469
+ GitHubButton,
470
+ AmazonButton,
471
+ LinkedinButton,
472
+ XButton,
473
+ TikTokButton,
474
+ InstagramButton,
475
+ PinterestButton
476
+ } from 'rystem.authentication.social.react';
477
+
478
+ const customOrder = [
479
+ MicrosoftButton, // Show Microsoft first
480
+ GoogleButton,
481
+ GitHubButton,
482
+ LinkedinButton,
483
+ FacebookButton,
484
+ AmazonButton,
485
+ XButton,
486
+ TikTokButton,
487
+ InstagramButton,
488
+ PinterestButton
489
+ ];
490
+
491
+ <SocialLoginButtons buttons={customOrder} />
492
+ ```
493
+
494
+ ### Individual Provider Buttons
495
+
496
+ ```typescript
497
+ import { MicrosoftButton, GoogleButton } from 'rystem.authentication.social.react';
498
+
499
+ <div>
500
+ <MicrosoftButton />
501
+ <GoogleButton />
502
+ </div>
503
+ ```
504
+
505
+ ### SocialLogoutButton
506
+
507
+ ```typescript
508
+ import { SocialLogoutButton } from 'rystem.authentication.social.react';
509
+
510
+ <SocialLogoutButton>Sign Out</SocialLogoutButton>
511
+ ```
512
+
513
+ ## 🔧 Advanced Configuration
514
+
515
+ ### Platform Support (Web & Mobile)
516
+
517
+ The library now supports **platform-specific configuration** for Web, iOS, and Android (including React Native):
518
+
519
+ ```typescript
520
+ import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';
521
+
522
+ setupSocialLogin(x => {
523
+ x.apiUri = "https://yourdomain.com";
524
+
525
+ // Platform configuration
526
+ x.platform = {
527
+ type: PlatformType.Auto, // Auto-detect platform (Web/iOS/Android)
528
+
529
+ // Smart redirect path (detects if complete URI or relative path)
530
+ redirectPath: Platform.select({
531
+ web: '/account/login', // Relative path (auto-detects domain)
532
+ ios: 'msauth://com.yourapp.fantasoccer/auth', // Complete URI
533
+ android: 'myapp://oauth/callback', // Complete URI
534
+ default: '/account/login'
535
+ }),
536
+
537
+ // Login mode (popup for web, redirect for mobile)
538
+ loginMode: Platform.select({
539
+ web: LoginMode.Popup,
540
+ ios: LoginMode.Redirect,
541
+ android: LoginMode.Redirect,
542
+ default: LoginMode.Redirect
543
+ })
544
+ };
545
+
546
+ // OAuth providers
547
+ x.microsoft.clientId = "your-client-id";
548
+ x.google.clientId = "your-client-id";
549
+ });
550
+ ```
551
+
552
+ #### React Native Example
553
+
554
+ For **React Native** apps, use platform-specific deep links:
555
+
556
+ ```typescript
557
+ import { Platform } from 'react-native';
558
+ import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';
559
+
560
+ setupSocialLogin(x => {
561
+ x.apiUri = "https://yourdomain.com";
562
+
563
+ x.platform = {
564
+ type: PlatformType.Auto, // Will detect iOS/Android automatically
565
+
566
+ // Deep link redirect paths for mobile
567
+ redirectPath: Platform.select({
568
+ ios: 'msauth://com.keyserdsoze.fantasoccer/auth', // Complete URI
569
+ android: 'fantasoccer://oauth/callback', // Complete URI
570
+ default: '/account/login' // Relative path for web
571
+ }),
572
+
573
+ loginMode: LoginMode.Redirect // Always use redirect for mobile
574
+ };
575
+
576
+ x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
577
+ });
578
+ ```
579
+
580
+ **Important**: Configure deep links in your app:
581
+
582
+ **iOS** (`Info.plist`):
583
+ ```xml
584
+ <key>CFBundleURLTypes</key>
585
+ <array>
586
+ <dict>
587
+ <key>CFBundleURLSchemes</key>
588
+ <array>
589
+ <string>msauth</string>
590
+ </array>
591
+ <key>CFBundleURLName</key>
592
+ <string>com.keyserdsoze.fantasoccer</string>
593
+ </dict>
594
+ </array>
595
+ ```
596
+
597
+ **Android** (`AndroidManifest.xml`):
598
+ ```xml
599
+ <intent-filter>
600
+ <action android:name="android.intent.action.VIEW" />
601
+ <category android:name="android.intent.category.DEFAULT" />
602
+ <category android:name="android.intent.category.BROWSABLE" />
603
+ <data android:scheme="fantasoccer" android:host="oauth" />
604
+ </intent-filter>
605
+ ```
606
+
607
+ ### Login Mode (Popup vs Redirect)
608
+
609
+ Choose between **popup** and **redirect** modes:
610
+
611
+ ```typescript
612
+ // Popup mode (default for web - opens in new window)
613
+ setupSocialLogin(x => {
614
+ x.loginMode = LoginMode.Popup; // or x.platform.loginMode
615
+ });
616
+
617
+ // Redirect mode (default for mobile - navigates in same window)
618
+ setupSocialLogin(x => {
619
+ x.loginMode = LoginMode.Redirect;
620
+ });
621
+ ```
622
+
623
+ **Use Cases:**
624
+ - ✅ **Popup**: Best for desktop web apps (better UX, user stays on page)
625
+ - ✅ **Redirect**: Required for mobile apps, some browsers block popups
626
+
627
+ ### Platform Detection Utilities
628
+
629
+ Use built-in utilities for platform detection:
630
+
631
+ ```typescript
632
+ import {
633
+ detectPlatform,
634
+ isMobilePlatform,
635
+ isReactNative,
636
+ PlatformType
637
+ } from 'rystem.authentication.social.react';
638
+
639
+ // Detect current platform
640
+ const platform = detectPlatform(); // Returns: PlatformType.Web | iOS | Android
641
+
642
+ // Check if mobile
643
+ if (isMobilePlatform(platform)) {
644
+ console.log('Running on mobile');
645
+ }
646
+
647
+ // Check if React Native
648
+ if (isReactNative()) {
649
+ console.log('Running in React Native');
650
+ }
651
+ ```
652
+
653
+ ### Complete Mobile Setup Example
654
+
655
+ ```typescript
656
+ import { setupSocialLogin, PlatformType, LoginMode, detectPlatform } from 'rystem.authentication.social.react';
657
+
658
+ // Detect platform automatically
659
+ const currentPlatform = detectPlatform();
660
+
661
+ setupSocialLogin(x => {
662
+ x.apiUri = "https://api.yourdomain.com";
663
+
664
+ // Configure based on detected platform
665
+ x.platform = {
666
+ type: currentPlatform,
667
+
668
+ redirectUri: (() => {
669
+ switch (currentPlatform) {
670
+ case PlatformType.iOS:
671
+ return 'msauth://com.yourapp.bundle/auth';
672
+ case PlatformType.Android:
673
+ return 'yourapp://oauth/callback';
674
+ default:
675
+ return typeof window !== 'undefined'
676
+ ? window.location.origin
677
+ : 'http://localhost:3000';
678
+ }
679
+ })(),
680
+
681
+ loginMode: currentPlatform === PlatformType.Web
682
+ ? LoginMode.Popup
683
+ : LoginMode.Redirect
684
+ };
685
+
686
+ // OAuth providers
687
+ x.microsoft.clientId = "your-microsoft-client-id";
688
+ x.google.clientId = "your-google-client-id";
689
+
690
+ // Error handling
691
+ x.onLoginFailure = (error) => {
692
+ if (currentPlatform === PlatformType.Web) {
693
+ alert(`Login failed: ${error.message}`);
694
+ } else {
695
+ // Use React Native Alert or Toast
696
+ console.error('Login error:', error);
697
+ }
698
+ };
699
+
700
+ x.automaticRefresh = true;
701
+ });
702
+ ```
703
+
704
+ ## 📱 Mobile OAuth Configuration
705
+
706
+ ### Microsoft Entra ID (for Mobile)
707
+
708
+ 1. Register your mobile app redirect URI in Azure Portal
709
+ 2. For iOS: `msauth://com.yourapp.bundle/auth`
710
+ 3. For Android: `yourapp://oauth/callback`
711
+ 4. Enable "Mobile and desktop applications" platform
712
+ 5. Make sure PKCE is enabled (library handles this automatically)
713
+
714
+ ### Google (for Mobile)
715
+
716
+ 1. Configure OAuth consent screen for mobile
717
+ 2. Add redirect URI: Use reverse client ID for iOS
718
+ 3. Example: `com.googleusercontent.apps.YOUR_CLIENT_ID:/oauth2redirect`
719
+
720
+ ### Deep Link Best Practices
721
+
722
+ **iOS Bundle ID Format:**
723
+ ```
724
+ msauth://com.yourcompany.yourapp/auth
725
+ ```
726
+
727
+ **Android Package Name Format:**
728
+ ```
729
+ yourapp://oauth/callback
730
+ ```
731
+
732
+ ## 🔍 How Platform Configuration Works
733
+
734
+ ### Understanding Redirect URI Resolution
735
+
736
+ When a user clicks a social login button, the library determines the OAuth redirect URI using this **priority order**:
737
+
738
+ ```typescript
739
+ // Priority 1: Explicit platform.redirectUri (highest priority)
740
+ if (settings.platform?.redirectUri) {
741
+ redirectUri = settings.platform.redirectUri;
742
+ }
743
+ // Priority 2: Fallback to redirectDomain + redirectPath
744
+ else {
745
+ redirectUri = `${settings.redirectDomain}${settings.redirectPath || ''}`;
746
+ }
747
+ ```
748
+
749
+ ### Example Flow (Microsoft Login on React Native iOS)
750
+
751
+ 1. **Setup Configuration**:
752
+ ```typescript
753
+ setupSocialLogin(x => {
754
+ x.apiUri = "https://api.yourdomain.com";
755
+ x.redirectDomain = "https://web.yourdomain.com";
756
+ x.redirectPath = "/account/login";
757
+
758
+ x.platform = {
759
+ type: PlatformType.iOS,
760
+ redirectUri: "msauth://com.yourapp.bundle/auth" // Mobile deep link
761
+ };
762
+
763
+ x.microsoft.clientId = "your-client-id";
764
+ });
765
+ ```
766
+
767
+ 2. **User Clicks MicrosoftButton**:
768
+ - Library detects `platform.redirectUri` is set
769
+ - Uses `msauth://com.yourapp.bundle/auth` (NOT `https://web.yourdomain.com/account/login`)
770
+ - Generates PKCE code_verifier and code_challenge
771
+ - Constructs OAuth URL:
772
+ ```
773
+ https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
774
+ ?client_id=your-client-id
775
+ &redirect_uri=msauth%3A%2F%2Fcom.yourapp.bundle%2Fauth
776
+ &code_challenge=<generated>
777
+ &code_challenge_method=S256
778
+ ```
779
+
780
+ 3. **OAuth Provider Redirects**:
781
+ - Microsoft redirects to: `msauth://com.yourapp.bundle/auth?code=ABC123&state=XYZ`
782
+ - iOS deep link handler catches this URL
783
+ - React Native navigation extracts `code` and `state`
784
+
785
+ 4. **Token Exchange**:
786
+ - Library calls API: `POST /api/Authentication/Social/Token?provider=Microsoft&code=ABC123&redirectPath=/account/login`
787
+ - API validates code using PKCE code_verifier
788
+ - Returns JWT access token
789
+
790
+ 5. **User Logged In**:
791
+ - Token stored in AsyncStorage (React Native)
792
+ - `useSocialToken()` and `useSocialUser()` hooks update
793
+ - App navigates to `/account/login` (or dashboard)
794
+
795
+ ### Platform Auto-Detection Logic
796
+
797
+ ```typescript
798
+ export function detectPlatform(): PlatformType {
799
+ // Check if React Native environment
800
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
801
+ // Detect iOS
802
+ if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
803
+ return PlatformType.iOS;
804
+ }
805
+ // Detect Android
806
+ if (/Android/.test(navigator.userAgent)) {
807
+ return PlatformType.Android;
808
+ }
809
+ }
810
+
811
+ // Default to Web
812
+ return PlatformType.Web;
813
+ }
814
+ ```
815
+
816
+ ### When to Use Each Configuration
817
+
818
+ | Scenario | redirectDomain | redirectPath | platform.redirectUri | platform.type |
819
+ |----------|----------------|--------------|---------------------|---------------|
820
+ | **Web SPA** | `https://app.com` | `/account/login` | `undefined` | `Web` or `Auto` |
821
+ | **React Native iOS** | `https://app.com` (fallback) | `/account/login` | `msauth://com.yourapp.bundle/auth` | `iOS` or `Auto` |
822
+ | **React Native Android** | `https://app.com` (fallback) | `/account/login` | `yourapp://oauth/callback` | `Android` or `Auto` |
823
+ | **Multi-Platform (Recommended)** | `https://app.com` | `/account/login` | `Platform.select({ ios: '...', android: '...', web: undefined })` | `Auto` |
824
+
825
+ ### Configuration Best Practices
826
+
827
+ ✅ **DO**:
828
+ - Use `PlatformType.Auto` for automatic detection
829
+ - Set `platform.redirectUri` explicitly for React Native
830
+ - Keep `redirectDomain` and `redirectPath` as fallbacks for web
831
+ - Use `Platform.select()` for cross-platform apps
832
+ - Encode redirect URIs in OAuth URLs (library does this automatically)
833
+
834
+ ❌ **DON'T**:
835
+ - Hardcode platform detection (use `detectPlatform()` instead)
836
+ - Forget to register redirect URIs in OAuth provider consoles
837
+ - Use web redirect URIs (`https://`) for mobile apps
838
+ - Skip Info.plist/AndroidManifest.xml configuration for deep links
839
+
840
+ ### Debugging Platform Configuration
841
+
842
+ Check which redirect URI is being used:
843
+
844
+ ```typescript
845
+ import { getSocialLoginSettings } from 'rystem.authentication.social.react';
846
+
847
+ const settings = getSocialLoginSettings();
848
+ const effectiveRedirectUri = settings.platform?.redirectUri
849
+ || `${settings.redirectDomain}${settings.redirectPath || ''}`;
850
+
851
+ console.log('Platform Type:', settings.platform?.type);
852
+ console.log('Redirect URI:', effectiveRedirectUri);
853
+ console.log('Login Mode:', settings.platform?.loginMode || settings.loginMode);
854
+ ```
855
+
856
+ ## 🆚 Popup vs Redirect Comparison
857
+
858
+ | Feature | Popup Mode | Redirect Mode |
859
+ |---------|-----------|---------------|
860
+ | **Platform** | Web only | Web + Mobile |
861
+ | **User Experience** | Stays on page | Leaves page temporarily |
862
+ | **Browser Support** | May be blocked | Always works |
863
+ | **Mobile Apps** | ❌ Not supported | ✅ Required |
864
+ | **Session Persistence** | ✅ Maintained | ⚠️ Depends on implementation |
865
+ | **Security** | ✅ Same-origin | ✅ PKCE required |
866
+
867
+ ## Error Handling
868
+
869
+ ```typescript
870
+ setupSocialLogin(x => {
871
+ x.onLoginFailure = (error) => {
872
+ switch (error.code) {
873
+ case 3:
874
+ // Error during button click (client-side)
875
+ console.error('Client error:', error.message);
876
+ break;
877
+ case 15:
878
+ // Error during token retrieval from API
879
+ console.error('Token exchange failed:', error.message);
880
+ showNotification('Login failed. Please try again.');
881
+ break;
882
+ case 10:
883
+ // Error fetching user information from API
884
+ console.error('User fetch failed:', error.message);
885
+ break;
886
+ default:
887
+ console.error('Unknown error:', error);
888
+ }
889
+ };
890
+ });
891
+ ```
892
+
893
+ ## 💾 Custom Storage Service
894
+
895
+ 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**.
896
+
897
+ ### Architecture
898
+
899
+ The library uses the **Decorator Pattern** with separation between infrastructure and domain logic:
900
+
901
+ ```
902
+ IStorageService (interface) ← Generic key-value storage
903
+
904
+ LocalStorageService (default) ← Browser localStorage
905
+
906
+ ├── PkceStorageService ← PKCE OAuth logic
907
+ ├── TokenStorageService ← Token + expiry logic
908
+ └── UserStorageService ← User data logic
909
+ ```
910
+
911
+ ### Using Default Storage (localStorage)
912
+
913
+ No configuration needed - the library automatically uses `LocalStorageService`:
914
+
915
+ ```typescript
916
+ import { setupSocialLogin } from 'rystem.authentication.social.react';
917
+
918
+ setupSocialLogin(x => {
919
+ x.apiUri = "https://api.yourdomain.com";
920
+ // storageService is automatically initialized with LocalStorageService
921
+ x.microsoft.clientId = "your-client-id";
922
+ });
923
+ ```
924
+
925
+ ### Creating Custom Storage
926
+
927
+ Implement `IStorageService` for custom storage (secure storage, Redis, etc.):
928
+
929
+ ```typescript
930
+ import { setupSocialLogin, IStorageService } from 'rystem.authentication.social.react';
931
+
932
+ // Example: Secure Storage for React Native
933
+ class SecureStorageService implements IStorageService {
934
+ async get(key: string): Promise<string | null> {
935
+ try {
936
+ // Use expo-secure-store or react-native-keychain
937
+ return await SecureStore.getItemAsync(key);
938
+ } catch (error) {
939
+ console.error('SecureStorage get error:', error);
940
+ return null;
941
+ }
942
+ }
943
+
944
+ async set(key: string, value: string): Promise<void> {
945
+ try {
946
+ await SecureStore.setItemAsync(key, value);
947
+ } catch (error) {
948
+ console.error('SecureStorage set error:', error);
949
+ }
950
+ }
951
+
952
+ async remove(key: string): Promise<void> {
953
+ try {
954
+ await SecureStore.deleteItemAsync(key);
955
+ } catch (error) {
956
+ console.error('SecureStorage remove error:', error);
957
+ }
958
+ }
959
+
960
+ async has(key: string): Promise<boolean> {
961
+ const value = await this.get(key);
962
+ return value !== null;
963
+ }
964
+
965
+ async clear(): Promise<void> {
966
+ // Optional: implement if needed
967
+ }
968
+ }
969
+
970
+ // Configure custom storage
971
+ setupSocialLogin(x => {
972
+ x.apiUri = "https://api.yourdomain.com";
973
+ x.storageService = new SecureStorageService(); // Use secure storage
974
+ x.microsoft.clientId = "your-client-id";
975
+ });
976
+ ```
977
+
978
+ ### Example: In-Memory Storage (Testing)
979
+
980
+ Perfect for unit tests without persisting data:
981
+
982
+ ```typescript
983
+ import { IStorageService } from 'rystem.authentication.social.react';
984
+
985
+ class MockStorageService implements IStorageService {
986
+ private storage = new Map<string, string>();
987
+
988
+ get(key: string): string | null {
989
+ return this.storage.get(key) ?? null;
990
+ }
991
+
992
+ set(key: string, value: string): void {
993
+ this.storage.set(key, value);
994
+ }
995
+
996
+ remove(key: string): void {
997
+ this.storage.delete(key);
998
+ }
999
+
1000
+ has(key: string): boolean {
1001
+ return this.storage.has(key);
1002
+ }
1003
+
1004
+ clear(): void {
1005
+ this.storage.clear();
1006
+ }
1007
+ }
1008
+
1009
+ // Use in tests
1010
+ setupSocialLogin(x => {
1011
+ x.storageService = new MockStorageService();
1012
+ // ... rest of config
1013
+ });
1014
+ ```
1015
+
1016
+ ### Example: Redis Storage (Server-Side)
1017
+
1018
+ For server-side rendering or distributed systems:
1019
+
1020
+ ```typescript
1021
+ import { createClient } from 'redis';
1022
+ import { IStorageService } from 'rystem.authentication.social.react';
1023
+
1024
+ class RedisStorageService implements IStorageService {
1025
+ private client = createClient({ url: 'redis://localhost:6379' });
1026
+
1027
+ constructor() {
1028
+ this.client.connect();
1029
+ }
1030
+
1031
+ async get(key: string): Promise<string | null> {
1032
+ return await this.client.get(key);
1033
+ }
1034
+
1035
+ async set(key: string, value: string): Promise<void> {
1036
+ await this.client.set(key, value, { EX: 3600 }); // 1 hour expiry
1037
+ }
1038
+
1039
+ async remove(key: string): Promise<void> {
1040
+ await this.client.del(key);
1041
+ }
1042
+
1043
+ async has(key: string): Promise<boolean> {
1044
+ const exists = await this.client.exists(key);
1045
+ return exists === 1;
1046
+ }
1047
+
1048
+ async clear(): Promise<void> {
1049
+ await this.client.flushAll();
1050
+ }
1051
+ }
1052
+
1053
+ setupSocialLogin(x => {
1054
+ x.storageService = new RedisStorageService();
1055
+ // ... rest of config
1056
+ });
1057
+ ```
1058
+
1059
+ ### Storage Keys Used
1060
+
1061
+ The library stores data with these keys (backward-compatible):
1062
+
1063
+ | Key | Description | Service |
1064
+ |-----|-------------|---------|
1065
+ | `socialUserToken` | JWT access token + expiry | `TokenStorageService` |
1066
+ | `socialUserToken_expiry` | Token expiration timestamp | `TokenStorageService` |
1067
+ | `socialUser` | User profile data | `UserStorageService` |
1068
+ | `rystem_pkce_{provider}_verifier` | PKCE code verifier | `PkceStorageService` |
1069
+ | `rystem_pkce_{provider}_challenge` | PKCE code challenge (optional) | `PkceStorageService` |
1070
+
1071
+ ### When to Use Custom Storage
1072
+
1073
+ | Scenario | Recommended Storage |
1074
+ |----------|-------------------|
1075
+ | **Web SPA** | `LocalStorageService` (default) |
1076
+ | **React Native Mobile** | `SecureStorageService` (expo-secure-store) |
1077
+ | **Unit Testing** | `MockStorageService` (in-memory) |
1078
+ | **Server-Side Rendering** | `RedisStorageService` or `DatabaseStorageService` |
1079
+ | **Electron Apps** | Custom storage with encryption |
1080
+
1081
+ ### Advanced: Encrypted Storage
1082
+
1083
+ Add encryption layer on top of any storage:
1084
+
1085
+ ```typescript
1086
+ class EncryptedStorageService implements IStorageService {
1087
+ constructor(
1088
+ private baseStorage: IStorageService,
1089
+ private encryptionKey: string
1090
+ ) {}
1091
+
1092
+ async get(key: string): Promise<string | null> {
1093
+ const encrypted = await this.baseStorage.get(key);
1094
+ if (!encrypted) return null;
1095
+ return this.decrypt(encrypted, this.encryptionKey);
1096
+ }
1097
+
1098
+ async set(key: string, value: string): Promise<void> {
1099
+ const encrypted = this.encrypt(value, this.encryptionKey);
1100
+ await this.baseStorage.set(key, encrypted);
1101
+ }
1102
+
1103
+ async remove(key: string): Promise<void> {
1104
+ await this.baseStorage.remove(key);
1105
+ }
1106
+
1107
+ async has(key: string): Promise<boolean> {
1108
+ return await this.baseStorage.has(key);
1109
+ }
1110
+
1111
+ private encrypt(text: string, key: string): string {
1112
+ // Use crypto library (e.g., crypto-js)
1113
+ return CryptoJS.AES.encrypt(text, key).toString();
1114
+ }
1115
+
1116
+ private decrypt(ciphertext: string, key: string): string {
1117
+ const bytes = CryptoJS.AES.decrypt(ciphertext, key);
1118
+ return bytes.toString(CryptoJS.enc.Utf8);
1119
+ }
1120
+ }
1121
+
1122
+ // Usage
1123
+ const secureStorage = new LocalStorageService();
1124
+ const encryptedStorage = new EncryptedStorageService(
1125
+ secureStorage,
1126
+ 'your-encryption-key'
1127
+ );
1128
+
1129
+ setupSocialLogin(x => {
1130
+ x.storageService = encryptedStorage;
1131
+ // ... rest of config
1132
+ });
1133
+ ```
1134
+
1135
+ 📖 **Full Storage Architecture Guide**: See [`STORAGE_ARCHITECTURE.md`](./STORAGE_ARCHITECTURE.md) for detailed technical documentation.
1136
+
1137
+ ---
1138
+
1139
+ ## 🧭 Custom Routing Service
1140
+
1141
+ ## 🧭 Custom Routing Service
1142
+
1143
+ ### Why Routing Service?
1144
+
1145
+ **Problem**: Client-side routing frameworks (**React Router**, **Next.js App Router**, **Remix**) intercept native browser APIs, causing two critical issues:
1146
+
1147
+ 1. **OAuth Callback Detection**: `window.location.search` is empty even when URL contains parameters
1148
+ 2. **Navigation Bypass**: `window.location.href` and `window.history.replaceState()` bypass the router, losing routing state
1149
+
1150
+ **Solution**: The `IRoutingService` abstraction provides a unified interface for:
1151
+ - **URL Parameter Reading** (OAuth callback detection)
1152
+ - **Navigation Operations** (redirects, return URLs, cleanup)
1153
+
1154
+ ### Default Behavior
1155
+
1156
+ By default, the library uses `WindowRoutingService` which uses native browser APIs:
1157
+
1158
+ ```typescript
1159
+ // ✅ Works automatically with:
1160
+ // - Vanilla React (no routing library)
1161
+ // - Standard browser navigation
1162
+ // - Server-side rendered apps
1163
+ // - Next.js Pages Router (with server redirects)
1164
+ setupSocialLogin(x => {
1165
+ // No routingService config needed - uses WindowRoutingService by default
1166
+ x.apiUri = 'https://api.example.com';
1167
+ });
1168
+ ```
1169
+
1170
+ ### When to Use Custom Routing Service
1171
+
1172
+ | Framework | Needs Custom? | Why? | Implementation |
1173
+ |-----------|---------------|------|----------------|
1174
+ | **React Router** | ✅ **YES** | Client-side routing intercepts window APIs | `ReactRouterRoutingService` (see below) |
1175
+ | **Next.js App Router** | ✅ **YES** | Uses router.push/replace for navigation | `NextAppRouterRoutingService` (see below) |
1176
+ | **Next.js Pages Router** | ⚠️ **MAYBE** | Depends on navigation style | Test if return URLs work |
1177
+ | **Remix** | ✅ **YES** | Uses @remix-run/react router | Similar to React Router |
1178
+ | **Vanilla React** | ❌ No | No routing framework | Default works ✅ |
1179
+ | **Server-Side Rendering** | ❌ No | Full page reloads | Default works ✅ |
1180
+
1181
+ 📁 **Ready-to-use example files** are available in [`src/services/`](./src/services/):
1182
+ - [`ReactRouterRoutingService.example.ts`](./src/services/ReactRouterRoutingService.example.ts) - React Router v6+ (unified)
1183
+ - [`NextAppRouterRoutingService.example.ts`](./src/services/NextAppRouterRoutingService.example.ts) - Next.js App Router v13+ (unified)
1184
+ - [`MockRoutingService.example.ts`](./src/services/MockRoutingService.example.ts) - Unit testing with verification methods
1185
+
1186
+ **Copy these files to your project and remove the `.example` extension.**
1187
+
1188
+ ---
1189
+
1190
+ ### 🔧 React Router Implementation
1191
+
1192
+ If you're using **React Router v6+**, use this unified routing service:
1193
+
1194
+ ```typescript
1195
+ import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
1196
+ import { IRoutingService } from 'rystem.authentication.social.react';
1197
+
1198
+ /**
1199
+ * Unified Routing Service for React Router v6+
1200
+ * Handles both URL reading and navigation
1201
+ */
1202
+ export class ReactRouterRoutingService implements IRoutingService {
1203
+ private searchParamsGetter: (() => URLSearchParams) | null = null;
1204
+ private navigateFunc: ((to: string, options?: any) => void) | null = null;
1205
+ private location: any = null;
1206
+
1207
+ /**
1208
+ * Single initialization with all React Router hooks
1209
+ */
1210
+ initialize(
1211
+ searchParamsGetter: () => URLSearchParams,
1212
+ navigateFunc: (to: string, options?: any) => void,
1213
+ location: any
1214
+ ): void {
1215
+ this.searchParamsGetter = searchParamsGetter;
1216
+ this.navigateFunc = navigateFunc;
1217
+ this.location = location;
1218
+ }
1219
+
1220
+ // URL Parameter Reading (OAuth callbacks)
1221
+ getSearchParam(key: string): string | null {
1222
+ return this.searchParamsGetter?.().get(key) || null;
1223
+ }
1224
+
1225
+ getAllSearchParams(): URLSearchParams {
1226
+ return this.searchParamsGetter?.() || new URLSearchParams();
1227
+ }
1228
+
1229
+ // Navigation Operations
1230
+ getCurrentPath(): string {
1231
+ return this.location
1232
+ ? this.location.pathname + this.location.search
1233
+ : window.location.pathname + window.location.search;
1234
+ }
1235
+
1236
+ navigateTo(url: string): void {
1237
+ // External OAuth redirects must use window.location
1238
+ if (url.startsWith('http')) {
1239
+ window.location.href = url;
1240
+ } else {
1241
+ this.navigateFunc?.(url);
1242
+ }
1243
+ }
1244
+
1245
+ navigateReplace(path: string): void {
1246
+ this.navigateFunc?.(path, { replace: true });
1247
+ }
1248
+
1249
+ openPopup(url: string, name: string, features: string): Window | null {
1250
+ return window.open(url, name, features);
1251
+ }
1252
+ }
1253
+ ```
1254
+
1255
+ #### Usage with React Router
1256
+
1257
+ ```typescript
1258
+ import { BrowserRouter, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
1259
+ import { setupSocialLogin, SocialLoginWrapper, MicrosoftButton } from 'rystem.authentication.social.react';
1260
+ import { ReactRouterRoutingService } from './ReactRouterRoutingService';
1261
+
1262
+ // Create singleton instance
1263
+ const routingService = new ReactRouterRoutingService();
1264
+
1265
+ // Setup configuration ONCE at app startup
1266
+ setupSocialLogin(x => {
1267
+ x.apiUri = 'https://api.example.com';
1268
+ x.routingService = routingService; // ✅ One service for everything
1269
+ x.providers = [
1270
+ { provider: ProviderType.Microsoft, clientId: 'your-client-id' }
1271
+ ];
1272
+ });
1273
+
1274
+ // Main App Component
1275
+ function App() {
1276
+ const [searchParams] = useSearchParams();
1277
+ const navigate = useNavigate();
1278
+ const location = useLocation();
1279
+
1280
+ // ✅ Single initialization with all hooks
1281
+ useEffect(() => {
1282
+ routingService.initialize(() => searchParams, navigate, location);
1283
+ }, [searchParams, navigate, location]);
1284
+
1285
+ return (
1286
+ <div>
1287
+ <h1>My App</h1>
1288
+ <MicrosoftButton />
1289
+ </div>
1290
+ );
1291
+ }
1292
+
1293
+ // Wrap with Router
1294
+ const Root = () => (
1295
+ <BrowserRouter>
1296
+ <SocialLoginWrapper>
1297
+ <App />
1298
+ </SocialLoginWrapper>
1299
+ </BrowserRouter>
1300
+ );
1301
+
1302
+ export default Root;
1303
+ ```
1304
+
1305
+ ---
1306
+
1307
+ ### 🔧 Next.js App Router Implementation
1308
+
1309
+ For **Next.js 13+ App Router** with client components:
1310
+
1311
+ ```typescript
1312
+ 'use client';
1313
+
1314
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation';
1315
+ import { IRoutingService } from 'rystem.authentication.social.react';
1316
+
1317
+ /**
1318
+ * Unified Routing Service for Next.js App Router
1319
+ * Handles both URL reading and navigation
1320
+ */
1321
+ export class NextAppRouterRoutingService implements IRoutingService {
1322
+ private router: any = null;
1323
+ private pathname: string | null = null;
1324
+ private searchParams: URLSearchParams | null = null;
1325
+
1326
+ /**
1327
+ * Single initialization with all Next.js hooks
1328
+ */
1329
+ initialize(router: any, pathname: string, searchParams: URLSearchParams | null): void {
1330
+ this.router = router;
1331
+ this.pathname = pathname;
1332
+ this.searchParams = searchParams;
1333
+ }
1334
+
1335
+ // URL Parameter Reading (OAuth callbacks)
1336
+ getSearchParam(key: string): string | null {
1337
+ return this.searchParams?.get(key) || null;
1338
+ }
1339
+
1340
+ getAllSearchParams(): URLSearchParams {
1341
+ return this.searchParams || new URLSearchParams();
1342
+ }
1343
+
1344
+ // Navigation Operations
1345
+ getCurrentPath(): string {
1346
+ if (!this.pathname) return window.location.pathname + window.location.search;
1347
+ const search = this.searchParams?.toString();
1348
+ return search ? `${this.pathname}?${search}` : this.pathname;
1349
+ }
1350
+
1351
+ navigateTo(url: string): void {
1352
+ // External OAuth redirects must use window.location
1353
+ if (url.startsWith('http')) {
1354
+ window.location.href = url;
1355
+ } else {
1356
+ this.router?.push(url);
1357
+ }
1358
+ }
1359
+
1360
+ navigateReplace(path: string): void {
1361
+ this.router?.replace(path);
1362
+ }
1363
+
1364
+ openPopup(url: string, name: string, features: string): Window | null {
1365
+ return window.open(url, name, features);
1366
+ }
1367
+ }
1368
+ ```
1369
+
1370
+ #### Usage with Next.js App Router
1371
+
1372
+ ```typescript
1373
+ 'use client'; // ✅ Must be a Client Component
1374
+
1375
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation';
1376
+ import { useEffect } from 'react';
1377
+ import { setupSocialLogin, SocialLoginWrapper, MicrosoftButton } from 'rystem.authentication.social.react';
1378
+ import { NextAppRouterRoutingService } from './NextAppRouterRoutingService';
1379
+
1380
+ // Create singleton instance
1381
+ const routingService = new NextAppRouterRoutingService();
1382
+
1383
+ // Setup configuration ONCE
1384
+ setupSocialLogin(x => {
1385
+ x.apiUri = 'https://api.example.com';
1386
+ x.routingService = routingService; // ✅ One service for everything
1387
+ x.providers = [
1388
+ { provider: ProviderType.Microsoft, clientId: 'your-client-id' }
1389
+ ];
1390
+ });
1391
+
1392
+ export default function LoginPage() {
1393
+ const router = useRouter();
1394
+ const pathname = usePathname();
1395
+ const searchParams = useSearchParams();
1396
+
1397
+ // ✅ Single initialization with all hooks
1398
+ useEffect(() => {
1399
+ routingService.initialize(router, pathname, searchParams);
1400
+ }, [router, pathname, searchParams]);
1401
+
1402
+ return (
1403
+ <SocialLoginWrapper>
1404
+ <div>
1405
+ <h1>Login</h1>
1406
+ <MicrosoftButton />
1407
+ </div>
1408
+ </SocialLoginWrapper>
1409
+ );
1410
+ }
1411
+ ```
1412
+
1413
+ ---
1414
+
1415
+ ### 🧪 Testing with Mock Routing Service
1416
+
1417
+ For unit tests, use the mock service with verification methods:
1418
+
1419
+ ```typescript
1420
+ import { MockRoutingService } from './MockRoutingService';
1421
+
1422
+ const mockRouting = new MockRoutingService();
1423
+
1424
+ // Setup test data
1425
+ mockRouting.setSearchParam('code', 'test-auth-code');
1426
+ mockRouting.setSearchParam('state', 'microsoft');
1427
+ mockRouting.setCurrentPath('/account/login?tab=oauth');
1428
+
1429
+ setupSocialLogin(x => {
1430
+ x.routingService = mockRouting;
1431
+ // ... rest of test config
1432
+ });
1433
+
1434
+ // Run OAuth flow in test
1435
+ // ...
1436
+
1437
+ // Verify navigation behavior
1438
+ expect(mockRouting.wasNavigateToCalledWith('https://oauth.provider.com')).toBe(true);
1439
+ expect(mockRouting.wasReplaceCalledWith('/dashboard')).toBe(true);
1440
+ expect(mockRouting.getNavigationHistory()).toEqual([
1441
+ 'https://oauth.provider.com',
1442
+ '/dashboard'
1443
+ ]);
1444
+ ```
1445
+
1446
+ ---
1447
+
1448
+ ### ⚠️ Important Notes
1449
+
1450
+ 1. **Single Initialization**: Initialize routing service with ALL framework hooks in one call (not separate calls like before).
1451
+
1452
+ 2. **Singleton Pattern**: Create ONE instance and reuse it. Don't create new instances on every render.
1453
+
1454
+ 3. **Effect Dependencies**: Always include routing hooks in `useEffect` dependency array:
1455
+ ```typescript
1456
+ useEffect(() => {
1457
+ routingService.initialize(/* hooks */);
1458
+ }, [searchParams, navigate, location]); // ✅ All deps
1459
+ ```
1460
+
1461
+ 4. **External OAuth URLs**: OAuth redirects to external providers (e.g., `https://login.microsoftonline.com`) MUST use `window.location.href` regardless of framework.
1462
+
1463
+ 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.
1464
+
1465
+ ---
1466
+
1467
+ ### 🔍 Debugging Routing Service
1468
+
1469
+ Check your routing service is properly initialized:
1470
+
1471
+ ```typescript
1472
+ console.log('Routing Service:', settings.routingService.constructor.name);
1473
+ console.log('Current Path:', settings.routingService.getCurrentPath());
1474
+ console.log('OAuth Code:', settings.routingService.getSearchParam('code'));
1475
+ ```
1476
+
1477
+ You'll see these logs in `SocialLoginWrapper` during OAuth callbacks.
1478
+
1479
+ ---
1480
+
1481
+ ### 📊 What Does This Solve?
1482
+
1483
+ **Before (without custom routing service):**
1484
+ ```typescript
1485
+ // ❌ PROBLEM 1: OAuth callback params not found
1486
+ const code = new URLSearchParams(window.location.search).get('code');
1487
+ // Returns null even though URL is: /login?code=ABC&state=microsoft
1488
+ // (React Router intercepts client-side navigation)
1489
+
1490
+ // ❌ PROBLEM 2: Navigation bypasses router
1491
+ window.location.href = 'https://oauth.provider.com'; // Works but...
1492
+ // ... later:
1493
+ window.history.replaceState({}, '', '/dashboard'); // ❌ React Router doesn't know!
1494
+ // Result: URL changes but component doesn't update, state lost
1495
+ ```
1496
+
1497
+ **After (with custom routing service):**
1498
+ ```typescript
1499
+ // ✅ SOLUTION 1: Framework-aware URL reading
1500
+ const code = routingService.getSearchParam('code');
1501
+ // Uses React Router's useSearchParams internally
1502
+ // Returns 'ABC' correctly!
1503
+
1504
+ // ✅ SOLUTION 2: Framework-aware navigation
1505
+ routingService.navigateTo('https://oauth.provider.com'); // External, uses window.location
1506
+ // ... later:
1507
+ routingService.navigateReplace('/dashboard'); // ✅ Calls navigate(path, {replace: true})
1508
+ // Result: React Router updates correctly, components re-render!
1509
+ ```
1510
+
1511
+ ---
1512
+
1513
+ ### 📋 Architecture Comparison
1514
+
1515
+ | Feature | Before (v0.3.x) | After (v0.4.0) |
1516
+ |---------|-----------------|----------------|
1517
+ | **Services** | `IUrlService` + `INavigationService` | `IRoutingService` (unified) ✅ |
1518
+ | **Settings** | 2 fields (`urlService`, `navigationService`) | 1 field (`routingService`) ✅ |
1519
+ | **Initialization** | 2 separate calls | 1 unified call ✅ |
1520
+ | **Hooks** | Split across 2 services | All in one place ✅ |
1521
+ | **Example Files** | 8 files (4 URL + 4 Nav) | 3 files (unified) ✅ |
1522
+ | **Complexity** | Higher (duplicate patterns) | Lower (single pattern) ✅ |
1523
+
1524
+ ---
1525
+
1526
+ ### Custom API Integration
1527
+
1528
+ ```typescript
1529
+ import { useSocialToken } from 'rystem.authentication.social.react';
1530
+
1531
+ const MyComponent = () => {
1532
+ const token = useSocialToken();
1533
+
1534
+ const fetchProtectedData = async () => {
1535
+ if (token.isExpired) {
1536
+ alert('Please login first');
1537
+ return;
1538
+ }
1539
+
1540
+ try {
1541
+ const response = await fetch('https://api.example.com/protected', {
1542
+ headers: {
1543
+ 'Authorization': `Bearer ${token.accessToken}`,
1544
+ 'Content-Type': 'application/json'
1545
+ }
1546
+ });
1547
+
1548
+ if (response.status === 401) {
1549
+ // Token might be expired, force refresh
1550
+ const forceRefresh = useContext(SocialLoginContextRefresh);
1551
+ await forceRefresh();
1552
+ // Retry request
1553
+ }
1554
+
1555
+ const data = await response.json();
1556
+ return data;
1557
+ } catch (error) {
1558
+ console.error('API error:', error);
1559
+ }
1560
+ };
1561
+
1562
+ return <button onClick={fetchProtectedData}>Load Data</button>;
1563
+ };
1564
+ ```
1565
+
1566
+ ### TypeScript Custom User Model
1567
+
1568
+ ```typescript
1569
+ interface CustomSocialUser {
1570
+ username: string;
1571
+ isAuthenticated: boolean;
1572
+ displayName: string;
1573
+ avatar: string;
1574
+ roles: string[];
1575
+ }
1576
+
1577
+ const MyComponent = () => {
1578
+ const user = useSocialUser<CustomSocialUser>();
1579
+
1580
+ return (
1581
+ <div>
1582
+ <img src={user.avatar} alt={user.displayName} />
1583
+ <p>{user.displayName}</p>
1584
+ <p>Roles: {user.roles.join(', ')}</p>
1585
+ </div>
1586
+ );
1587
+ };
1588
+ ```
1589
+
1590
+ ## 🎨 Dark Mode & Theming
1591
+
1592
+ The modern social login buttons support **automatic dark mode** with three detection methods:
1593
+
1594
+ ### 1. **Automatic Detection** (Recommended)
1595
+
1596
+ The buttons automatically adapt to the user's system preference:
1597
+
1598
+ ```css
1599
+ /* No JavaScript needed - CSS handles it automatically */
1600
+ @media (prefers-color-scheme: dark) {
1601
+ /* Dark mode styles applied automatically */
1602
+ }
1603
+ ```
1604
+
1605
+ ### 2. **Manual Theme Control with `data-theme`**
1606
+
1607
+ Control the theme programmatically by setting the `data-theme` attribute on any parent element:
1608
+
1609
+ ```typescript
1610
+ import { MicrosoftButton } from 'rystem.authentication.social.react';
1611
+
1612
+ export const ThemedLoginPage = () => {
1613
+ const [isDark, setIsDark] = useState(false);
1614
+
1615
+ return (
1616
+ <div data-theme={isDark ? 'dark' : 'light'}>
1617
+ <button onClick={() => setIsDark(!isDark)}>
1618
+ Toggle Theme 🌓
1619
+ </button>
1620
+
1621
+ <MicrosoftButton />
1622
+ <GoogleButton />
1623
+ <GitHubButton />
1624
+ </div>
1625
+ );
1626
+ };
1627
+ ```
1628
+
1629
+ ### 3. **CSS Class Control**
1630
+
1631
+ Use CSS classes for framework integration (Tailwind, etc.):
1632
+
1633
+ ```typescript
1634
+ export const TailwindThemedLogin = () => {
1635
+ return (
1636
+ <div className="dark"> {/* Tailwind dark mode */}
1637
+ <MicrosoftButton />
1638
+ <GoogleButton />
1639
+ </div>
1640
+ );
1641
+ };
1642
+ ```
1643
+
1644
+ ### Theme Priority
1645
+
1646
+ The buttons check for dark mode in this order:
1647
+
1648
+ 1. **`[data-theme="dark"]`** attribute (highest priority)
1649
+ 2. **`.dark-mode`** CSS class
1650
+ 3. **`@media (prefers-color-scheme: dark)`** system preference (fallback)
1651
+
1652
+ ### Custom Theme Colors
1653
+
1654
+ Override the default colors using CSS variables:
1655
+
1656
+ ```css
1657
+ /* Light mode customization */
1658
+ :root {
1659
+ --rsb-microsoft-bg: #2f2f2f;
1660
+ --rsb-microsoft-color: #ffffff;
1661
+ --rsb-hover-brightness: 1.1;
1662
+ }
1663
+
1664
+ /* Dark mode customization */
1665
+ [data-theme="dark"] {
1666
+ --rsb-microsoft-bg: #404040;
1667
+ --rsb-microsoft-color: #e0e0e0;
1668
+ --rsb-hover-brightness: 1.15;
1669
+ }
1670
+
1671
+ /* Specific provider override */
1672
+ [data-theme="dark"] .rystem-social-button--google {
1673
+ background: linear-gradient(135deg, #434343 0%, #363636 100%);
1674
+ }
1675
+ ```
1676
+
1677
+ ### Available CSS Variables
1678
+
1679
+ | Variable | Default (Light) | Default (Dark) | Description |
1680
+ |----------|----------------|----------------|-------------|
1681
+ | `--rsb-background` | Provider brand color | Darker shade | Button background |
1682
+ | `--rsb-text` | `#ffffff` | `#e0e0e0` | Button text color |
1683
+ | `--rsb-hover-brightness` | `1.05` | `1.15` | Hover effect intensity |
1684
+ | `--rsb-focus-ring` | Provider color | Lighter shade | Keyboard focus outline |
1685
+ | `--rsb-shadow` | `rgba(0,0,0,0.1)` | `rgba(0,0,0,0.3)` | Button shadow |
1686
+ | `--rsb-disabled-opacity` | `0.6` | `0.5` | Disabled state opacity |
1687
+
1688
+ ### Framework Integration Examples
1689
+
1690
+ #### **Next.js with `next-themes`**
1691
+
1692
+ ```typescript
1693
+ import { useTheme } from 'next-themes';
1694
+ import { SocialLoginButtons } from 'rystem.authentication.social.react';
1695
+
1696
+ export const NextJsLogin = () => {
1697
+ const { theme, setTheme } = useTheme();
1698
+
1699
+ return (
1700
+ <div data-theme={theme}>
1701
+ <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
1702
+ Toggle Theme
1703
+ </button>
1704
+ <SocialLoginButtons />
1705
+ </div>
1706
+ );
1707
+ };
1708
+ ```
1709
+
1710
+ #### **React Context Theme Provider**
1711
+
1712
+ ```typescript
1713
+ const ThemeContext = createContext({ isDark: false, toggle: () => {} });
1714
+
1715
+ export const ThemeProvider = ({ children }) => {
1716
+ const [isDark, setIsDark] = useState(
1717
+ () => window.matchMedia('(prefers-color-scheme: dark)').matches
1718
+ );
1719
+
1720
+ useEffect(() => {
1721
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1722
+ const handler = (e) => setIsDark(e.matches);
1723
+ mediaQuery.addEventListener('change', handler);
1724
+ return () => mediaQuery.removeEventListener('change', handler);
1725
+ }, []);
1726
+
1727
+ return (
1728
+ <ThemeContext.Provider value={{ isDark, toggle: () => setIsDark(!isDark) }}>
1729
+ <div data-theme={isDark ? 'dark' : 'light'}>
1730
+ {children}
1731
+ </div>
1732
+ </ThemeContext.Provider>
1733
+ );
1734
+ };
1735
+
1736
+ // Usage
1737
+ export const App = () => (
1738
+ <ThemeProvider>
1739
+ <LoginPage />
1740
+ </ThemeProvider>
1741
+ );
1742
+ ```
1743
+
1744
+ #### **Tailwind CSS Integration**
1745
+
1746
+ ```typescript
1747
+ // tailwind.config.js
1748
+ module.exports = {
1749
+ darkMode: 'class', // Enable class-based dark mode
1750
+ // ...
1751
+ };
1752
+
1753
+ // Component
1754
+ export const TailwindLogin = () => {
1755
+ const [darkMode, setDarkMode] = useState(false);
1756
+
1757
+ return (
1758
+ <div className={darkMode ? 'dark' : ''}>
1759
+ <div className="bg-white dark:bg-gray-900 min-h-screen">
1760
+ <button
1761
+ onClick={() => setDarkMode(!darkMode)}
1762
+ className="mb-4 px-4 py-2 bg-gray-200 dark:bg-gray-700"
1763
+ >
1764
+ Toggle Dark Mode
1765
+ </button>
1766
+
1767
+ {/* Buttons automatically adapt to .dark class */}
1768
+ <SocialLoginButtons />
1769
+ </div>
1770
+ </div>
1771
+ );
1772
+ };
1773
+ ```
1774
+
1775
+ ### Accessibility
1776
+
1777
+ The buttons maintain **WCAG 2.1 AA contrast ratios** in both light and dark modes:
1778
+
1779
+ - ✅ **Light mode**: 4.5:1 minimum contrast
1780
+ - ✅ **Dark mode**: 4.5:1 minimum contrast
1781
+ - ✅ **Focus indicators**: 3:1 contrast with background
1782
+ - ✅ **Hover states**: Clearly visible in both modes
1783
+
1784
+ ### Testing Dark Mode
1785
+
1786
+ ```typescript
1787
+ import { render } from '@testing-library/react';
1788
+ import { MicrosoftButton } from 'rystem.authentication.social.react';
1789
+
1790
+ test('button renders correctly in dark mode', () => {
1791
+ const { container } = render(
1792
+ <div data-theme="dark">
1793
+ <MicrosoftButton />
1794
+ </div>
1795
+ );
1796
+
1797
+ const button = container.querySelector('.rystem-social-button--microsoft');
1798
+ expect(button).toBeInTheDocument();
1799
+
1800
+ // Check computed styles
1801
+ const styles = window.getComputedStyle(button);
1802
+ expect(styles.backgroundColor).toBeTruthy();
1803
+ });
1804
+ ```
1805
+
1806
+ ## 📝 Complete Example
1807
+
1808
+ ```typescript
1809
+ import { useState, useContext } from 'react';
1810
+ import {
1811
+ SocialLoginButtons,
1812
+ SocialLoginContextLogout,
1813
+ SocialLoginContextRefresh,
1814
+ SocialLogoutButton,
1815
+ useSocialToken,
1816
+ useSocialUser,
1817
+ MicrosoftButton,
1818
+ GoogleButton,
1819
+ GitHubButton
1820
+ } from 'rystem.authentication.social.react';
1821
+
1822
+ const customButtons = [MicrosoftButton, GoogleButton, GitHubButton];
1823
+
1824
+ export const Dashboard = () => {
1825
+ const token = useSocialToken();
1826
+ const user = useSocialUser();
1827
+ const forceRefresh = useContext(SocialLoginContextRefresh);
1828
+ const logout = useContext(SocialLoginContextLogout);
1829
+ const [count, setCount] = useState(0);
1830
+
1831
+ return (
1832
+ <div className="dashboard">
1833
+ {token.isExpired ? (
1834
+ <div className="login-section">
1835
+ <h2>Welcome! Please login</h2>
1836
+ <SocialLoginButtons buttons={customButtons} />
1837
+ </div>
1838
+ ) : (
1839
+ <div className="user-section">
1840
+ <h2>Welcome back, {user.username}!</h2>
1841
+
1842
+ <div className="token-info">
1843
+ <p><strong>Access Token:</strong> {token.accessToken.substring(0, 20)}...</p>
1844
+ <p><strong>Expires:</strong> {token.expiresIn.toLocaleString()}</p>
1845
+ </div>
1846
+
1847
+ <div className="actions">
1848
+ <button onClick={() => setCount(count + 1)}>
1849
+ Counter: {count}
1850
+ </button>
1851
+ <button onClick={() => forceRefresh()}>
1852
+ 🔄 Force Refresh Token
1853
+ </button>
1854
+ <SocialLogoutButton>
1855
+ 🚪 Logout
1856
+ </SocialLogoutButton>
1857
+ </div>
1858
+ </div>
1859
+ )}
1860
+ </div>
1861
+ );
1862
+ };
1863
+ ```
1864
+
1865
+ ## 🌐 OAuth Provider Configuration
1866
+
1867
+ ### Microsoft Entra ID (Azure AD)
1868
+
1869
+ 1. Go to [Azure Portal](https://portal.azure.com) → Azure Active Directory → App registrations
1870
+ 2. Create new registration (Single-page application)
1871
+ 3. Set **Redirect URI**: `https://yourdomain.com/account/login`
1872
+ 4. Under **Authentication**:
1873
+ - Enable "ID tokens"
1874
+ - Enable "Access tokens"
1875
+ - Add redirect URI with type "Single-page application"
1876
+ 5. Copy **Application (client) ID**
1877
+ 6. **No client secret needed** - PKCE handles security
1878
+
1879
+ ### Google
1880
+
1881
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com)
1882
+ 2. Create OAuth 2.0 Client ID (Web application)
1883
+ 3. Add **Authorized redirect URI**: `https://yourdomain.com/account/login`
1884
+ 4. Copy **Client ID**
1885
+
1886
+ ### GitHub
1887
+
1888
+ 1. Go to [GitHub Settings](https://github.com/settings/developers) → OAuth Apps
1889
+ 2. Create new OAuth App
1890
+ 3. Set **Authorization callback URL**: `https://yourdomain.com/account/login`
1891
+ 4. Copy **Client ID**
1892
+
1893
+ ## 🔗 Related Packages
1894
+
1895
+ - **API Server**: `Rystem.Authentication.Social` - Backend OAuth validation with PKCE support
1896
+ - **Blazor Client**: `Rystem.Authentication.Social.Blazor` - Blazor Server/WASM components
1897
+ - **Abstractions**: `Rystem.Authentication.Social.Abstractions` - Shared models
1898
+
1899
+ ## 📚 More Information
1900
+
1901
+ - **Complete Docs**: [https://rystem.net/mcp/tools/auth-social-typescript.md](https://rystem.net/mcp/tools/auth-social-typescript.md)
1902
+ - **OAuth Flow Diagram**: [https://rystem.net/mcp/prompts/auth-flow.md](https://rystem.net/mcp/prompts/auth-flow.md)
1903
+ - **PKCE RFC**: [RFC 7636](https://tools.ietf.org/html/rfc7636)