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.
- package/README.md +1903 -0
- package/lib/buttons/CreateSocialButton.d.ts +15 -0
- package/lib/buttons/CreateSocialButton.js +158 -0
- package/lib/buttons/SocialLoginButtons.d.ts +2 -0
- package/lib/buttons/SocialLoginButtons.js +22 -0
- package/lib/buttons/SocialLogoutButton.d.ts +3 -0
- package/lib/buttons/SocialLogoutButton.js +11 -0
- package/lib/buttons/graphics/AmazonLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/AmazonLoginButton.js +10 -0
- package/lib/buttons/graphics/AppleLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/AppleLoginButton.js +17 -0
- package/lib/buttons/graphics/BufferLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/BufferLoginButton.js +17 -0
- package/lib/buttons/graphics/DiscordLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/DiscordLoginButton.js +17 -0
- package/lib/buttons/graphics/FacebookLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/FacebookLoginButton.js +10 -0
- package/lib/buttons/graphics/GithubLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/GithubLoginButton.js +10 -0
- package/lib/buttons/graphics/GoogleLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/GoogleLoginButton.js +10 -0
- package/lib/buttons/graphics/InstagramLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/InstagramLoginButton.js +10 -0
- package/lib/buttons/graphics/LinkedInLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/LinkedInLoginButton.js +10 -0
- package/lib/buttons/graphics/MetamaskLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/MetamaskLoginButton.js +25 -0
- package/lib/buttons/graphics/MicrosoftLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/MicrosoftLoginButton.js +10 -0
- package/lib/buttons/graphics/OktaLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/OktaLoginButton.js +17 -0
- package/lib/buttons/graphics/PinterestLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/PinterestLoginButton.js +10 -0
- package/lib/buttons/graphics/SlackLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/SlackLoginButton.js +17 -0
- package/lib/buttons/graphics/SocialButtonStyle.d.ts +8 -0
- package/lib/buttons/graphics/SocialButtonStyle.js +2 -0
- package/lib/buttons/graphics/SocialLoginButton.d.ts +2 -0
- package/lib/buttons/graphics/SocialLoginButton.js +46 -0
- package/lib/buttons/graphics/TelegramLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/TelegramLoginButton.js +17 -0
- package/lib/buttons/graphics/TikTokLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/TikTokLoginButton.js +10 -0
- package/lib/buttons/graphics/XLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/XLoginButton.js +10 -0
- package/lib/buttons/graphics/YahooLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/YahooLoginButton.js +17 -0
- package/lib/buttons/graphics/ZaloLoginButton.d.ts +1 -0
- package/lib/buttons/graphics/ZaloLoginButton.js +17 -0
- package/lib/buttons/singles/AmazonButton.d.ts +2 -0
- package/lib/buttons/singles/AmazonButton.js +33 -0
- package/lib/buttons/singles/FacebookButton.d.ts +2 -0
- package/lib/buttons/singles/FacebookButton.js +55 -0
- package/lib/buttons/singles/GitHubButton.d.ts +2 -0
- package/lib/buttons/singles/GitHubButton.js +19 -0
- package/lib/buttons/singles/GoogleButton.d.ts +2 -0
- package/lib/buttons/singles/GoogleButton.js +44 -0
- package/lib/buttons/singles/InstagramButton.d.ts +2 -0
- package/lib/buttons/singles/InstagramButton.js +21 -0
- package/lib/buttons/singles/LinkedinButton.d.ts +2 -0
- package/lib/buttons/singles/LinkedinButton.js +23 -0
- package/lib/buttons/singles/MicrosoftButton.d.ts +2 -0
- package/lib/buttons/singles/MicrosoftButton.js +68 -0
- package/lib/buttons/singles/PinterestButton.d.ts +2 -0
- package/lib/buttons/singles/PinterestButton.js +22 -0
- package/lib/buttons/singles/TikTokButton.d.ts +2 -0
- package/lib/buttons/singles/TikTokButton.js +18 -0
- package/lib/buttons/singles/XButton.d.ts +2 -0
- package/lib/buttons/singles/XButton.js +24 -0
- package/lib/components/BrandIcons.d.ts +64 -0
- package/lib/components/BrandIcons.js +76 -0
- package/lib/components/ModernSocialButton.d.ts +21 -0
- package/lib/components/ModernSocialButton.js +23 -0
- package/lib/context/SocialLoginContext.d.ts +4 -0
- package/lib/context/SocialLoginContext.js +10 -0
- package/lib/context/SocialLoginWrapper.d.ts +3 -0
- package/lib/context/SocialLoginWrapper.js +137 -0
- package/lib/hooks/removeSocialLogin.d.ts +1 -0
- package/lib/hooks/removeSocialLogin.js +14 -0
- package/lib/hooks/useSocialToken.d.ts +2 -0
- package/lib/hooks/useSocialToken.js +21 -0
- package/lib/hooks/useSocialUser.d.ts +2 -0
- package/lib/hooks/useSocialUser.js +30 -0
- package/lib/index.d.ts +47 -0
- package/lib/index.js +90 -0
- package/lib/models/SocialButtonProps.d.ts +3 -0
- package/lib/models/SocialButtonProps.js +2 -0
- package/lib/models/SocialButtonsProps.d.ts +4 -0
- package/lib/models/SocialButtonsProps.js +2 -0
- package/lib/models/SocialToken.d.ts +6 -0
- package/lib/models/SocialToken.js +2 -0
- package/lib/models/SocialUser.d.ts +5 -0
- package/lib/models/SocialUser.js +2 -0
- package/lib/models/Token.d.ts +6 -0
- package/lib/models/Token.js +2 -0
- package/lib/models/setup/LoginMode.d.ts +13 -0
- package/lib/models/setup/LoginMode.js +17 -0
- package/lib/models/setup/PlatformConfig.d.ts +45 -0
- package/lib/models/setup/PlatformConfig.js +19 -0
- package/lib/models/setup/PlatformType.d.ts +21 -0
- package/lib/models/setup/PlatformType.js +25 -0
- package/lib/models/setup/ProviderType.d.ts +13 -0
- package/lib/models/setup/ProviderType.js +17 -0
- package/lib/models/setup/SocialLoginErrorResponse.d.ts +6 -0
- package/lib/models/setup/SocialLoginErrorResponse.js +2 -0
- package/lib/models/setup/SocialLoginSettings.d.ts +66 -0
- package/lib/models/setup/SocialLoginSettings.js +2 -0
- package/lib/models/setup/SocialParameter.d.ts +3 -0
- package/lib/models/setup/SocialParameter.js +2 -0
- package/lib/services/IRoutingService.d.ts +123 -0
- package/lib/services/IRoutingService.js +2 -0
- package/lib/services/IStorageService.d.ts +33 -0
- package/lib/services/IStorageService.js +2 -0
- package/lib/services/LocalStorageService.d.ts +32 -0
- package/lib/services/LocalStorageService.js +93 -0
- package/lib/services/MockRoutingService.example.d.ts +96 -0
- package/lib/services/MockRoutingService.example.js +153 -0
- package/lib/services/NextAppRouterRoutingService.example.d.ts +75 -0
- package/lib/services/NextAppRouterRoutingService.example.js +128 -0
- package/lib/services/PkceStorageService.d.ts +55 -0
- package/lib/services/PkceStorageService.js +103 -0
- package/lib/services/ReactRouterRoutingService.example.d.ts +69 -0
- package/lib/services/ReactRouterRoutingService.example.js +121 -0
- package/lib/services/TokenStorageService.d.ts +34 -0
- package/lib/services/TokenStorageService.js +90 -0
- package/lib/services/UserStorageService.d.ts +29 -0
- package/lib/services/UserStorageService.js +56 -0
- package/lib/services/WindowRoutingService.d.ts +74 -0
- package/lib/services/WindowRoutingService.js +118 -0
- package/lib/setup/SocialLoginManager.d.ts +12 -0
- package/lib/setup/SocialLoginManager.js +106 -0
- package/lib/setup/getSocialLoginSettings.d.ts +2 -0
- package/lib/setup/getSocialLoginSettings.js +8 -0
- package/lib/setup/setupSocialLogin.d.ts +2 -0
- package/lib/setup/setupSocialLogin.js +62 -0
- package/lib/styles/SocialButton.css +365 -0
- package/lib/utils/pkce.d.ts +18 -0
- package/lib/utils/pkce.js +44 -0
- package/lib/utils/platform.d.ts +30 -0
- package/lib/utils/platform.js +103 -0
- 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)
|