rystem.authentication.social.client 0.5.0 → 0.5.4
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 +361 -66
- package/lib/buttons/CreateSocialButton.js +26 -25
- package/lib/context/SocialLoginWrapper.js +15 -16
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -1
- package/lib/models/setup/SocialLoginSettings.d.ts +81 -8
- package/lib/services/BrowserPlatformService.d.ts +38 -0
- package/lib/services/BrowserPlatformService.js +117 -0
- package/lib/services/IPlatformBrowserService.d.ts +0 -0
- package/lib/services/IPlatformBrowserService.js +1 -0
- package/lib/services/IPlatformService.d.ts +89 -0
- package/lib/services/IPlatformService.js +2 -0
- package/lib/setup/setupSocialLogin.d.ts +27 -0
- package/lib/setup/setupSocialLogin.js +78 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,87 +87,111 @@ yarn add rystem.authentication.social.client
|
|
|
87
87
|
pnpm add rystem.authentication.social.client
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📱 React Native Complete Guide
|
|
93
|
+
|
|
94
|
+
This library is **100% framework-agnostic** and works perfectly in **React Native**, **Expo**, and any mobile framework! However, setup differs from web because:
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
1. ❌ **No `window`, `document`, or `localStorage`** in React Native
|
|
97
|
+
2. ❌ **Web SDK scripts** (Google Sign-In SDK, `@azure/msal-browser`) don't work in React Native
|
|
98
|
+
3. ✅ **Native SDKs** must be used instead (`@react-native-google-signin/google-signin`, `@react-native-community/msal`)
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
2. **Routing Service** (use Linking API for deep links)
|
|
100
|
+
### 🎯 Architecture Overview
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
| Component | Web | React Native |
|
|
103
|
+
|-----------|-----|--------------|
|
|
104
|
+
| **Core Services** (`SocialLoginManager`, `setupSocialLogin`) | ✅ Works | ✅ Works |
|
|
105
|
+
| **React Hooks** (`useSocialToken`, `useSocialUser`) | ✅ Works | ✅ Works |
|
|
106
|
+
| **UI Components** (`MicrosoftButton`, `GoogleButton`) | ✅ Works | ❌ **Web-only** |
|
|
107
|
+
| **Storage** | `LocalStorageService` | Custom `IStorageService` (AsyncStorage) |
|
|
108
|
+
| **Routing** | `WindowRoutingService` | Custom `IRoutingService` (Linking) |
|
|
109
|
+
| **Platform** | `BrowserPlatformService` | Custom `IPlatformService` (Dimensions) |
|
|
110
|
+
|
|
111
|
+
### 📋 Setup Checklist
|
|
112
|
+
|
|
113
|
+
- [ ] 1. Install `@react-native-async-storage/async-storage`
|
|
114
|
+
- [ ] 2. Create `ReactNativeStorageService` implementing `IStorageService`
|
|
115
|
+
- [ ] 3. Create `ReactNativeRoutingService` implementing `IRoutingService`
|
|
116
|
+
- [ ] 4. Create `ReactNativePlatformService` implementing `IPlatformService`
|
|
117
|
+
- [ ] 5. Install native OAuth SDKs (`@react-native-community/msal`, `@react-native-google-signin/google-signin`)
|
|
118
|
+
- [ ] 6. Create custom login button components
|
|
119
|
+
- [ ] 7. Use `SocialLoginManager.Instance(null).updateToken()` after successful native login
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 🔧 Step 1-4: Implementing Platform Services
|
|
124
|
+
|
|
125
|
+
#### 💾 Storage Service (AsyncStorage)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm install @react-native-async-storage/async-storage
|
|
129
|
+
```
|
|
98
130
|
|
|
99
131
|
```typescript
|
|
100
132
|
// services/ReactNativeStorageService.ts
|
|
101
133
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
102
|
-
import { IStorageService } from 'rystem.authentication.social.
|
|
134
|
+
import { IStorageService } from 'rystem.authentication.social.client';
|
|
103
135
|
|
|
104
136
|
export class ReactNativeStorageService implements IStorageService {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
137
|
+
get(key: string): string | null {
|
|
138
|
+
// Note: AsyncStorage is async, but IStorageService expects sync
|
|
139
|
+
// Use synchronous approach or modify your usage to handle promises
|
|
140
|
+
let result: string | null = null;
|
|
141
|
+
AsyncStorage.getItem(key).then(value => result = value);
|
|
142
|
+
return result;
|
|
112
143
|
}
|
|
113
144
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.error('AsyncStorage set error:', error);
|
|
119
|
-
}
|
|
145
|
+
set(key: string, value: string): void {
|
|
146
|
+
AsyncStorage.setItem(key, value).catch(error =>
|
|
147
|
+
console.error('AsyncStorage set error:', error)
|
|
148
|
+
);
|
|
120
149
|
}
|
|
121
150
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.error('AsyncStorage remove error:', error);
|
|
127
|
-
}
|
|
151
|
+
remove(key: string): void {
|
|
152
|
+
AsyncStorage.removeItem(key).catch(error =>
|
|
153
|
+
console.error('AsyncStorage remove error:', error)
|
|
154
|
+
);
|
|
128
155
|
}
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return value !== null;
|
|
157
|
+
has(key: string): boolean {
|
|
158
|
+
return this.get(key) !== null;
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.error('AsyncStorage clear error:', error);
|
|
140
|
-
}
|
|
161
|
+
clear(): void {
|
|
162
|
+
AsyncStorage.clear().catch(error =>
|
|
163
|
+
console.error('AsyncStorage clear error:', error)
|
|
164
|
+
);
|
|
141
165
|
}
|
|
142
166
|
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### 🧭 Routing Service (Linking API)
|
|
143
170
|
|
|
171
|
+
```typescript
|
|
144
172
|
// services/ReactNativeRoutingService.ts
|
|
145
173
|
import { Linking } from 'react-native';
|
|
146
|
-
import { IRoutingService } from 'rystem.authentication.social.
|
|
174
|
+
import { IRoutingService } from 'rystem.authentication.social.client';
|
|
147
175
|
|
|
148
176
|
export class ReactNativeRoutingService implements IRoutingService {
|
|
149
177
|
private currentUrl: URL | null = null;
|
|
150
178
|
|
|
151
179
|
constructor() {
|
|
152
|
-
// Parse initial URL
|
|
153
180
|
Linking.getInitialURL().then(url => {
|
|
154
181
|
if (url) this.currentUrl = new URL(url);
|
|
155
182
|
});
|
|
156
183
|
|
|
157
|
-
// Listen for deep link events
|
|
158
184
|
Linking.addEventListener('url', ({ url }) => {
|
|
159
185
|
this.currentUrl = new URL(url);
|
|
160
186
|
});
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
getSearchParam(key: string): string | null {
|
|
164
|
-
|
|
165
|
-
return this.currentUrl.searchParams.get(key);
|
|
190
|
+
return this.currentUrl?.searchParams.get(key) ?? null;
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
getAllSearchParams(): URLSearchParams {
|
|
169
|
-
|
|
170
|
-
return this.currentUrl.searchParams;
|
|
194
|
+
return this.currentUrl?.searchParams ?? new URLSearchParams();
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
getCurrentPath(): string {
|
|
@@ -176,43 +200,97 @@ export class ReactNativeRoutingService implements IRoutingService {
|
|
|
176
200
|
}
|
|
177
201
|
|
|
178
202
|
navigateTo(url: string): void {
|
|
179
|
-
// For OAuth URLs, open in browser
|
|
180
203
|
Linking.openURL(url);
|
|
181
204
|
}
|
|
182
205
|
|
|
183
206
|
navigateReplace(path: string): void {
|
|
184
|
-
//
|
|
207
|
+
// Implement with React Navigation or Expo Router
|
|
185
208
|
console.log('Navigate to:', path);
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
openPopup(url: string, name: string, features: string): Window | null {
|
|
189
|
-
// React Native doesn't support popups - use in-app browser
|
|
190
212
|
Linking.openURL(url);
|
|
191
213
|
return null;
|
|
192
214
|
}
|
|
193
215
|
}
|
|
216
|
+
```
|
|
194
217
|
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
#### 📐 Platform Service (Dimensions & Events)
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// services/ReactNativePlatformService.ts
|
|
222
|
+
import { Dimensions, EventEmitter } from 'react-native';
|
|
223
|
+
import { IPlatformService } from 'rystem.authentication.social.client';
|
|
224
|
+
|
|
225
|
+
export class ReactNativePlatformService implements IPlatformService {
|
|
226
|
+
private eventEmitter = new EventEmitter();
|
|
227
|
+
|
|
228
|
+
addStorageListener(callback: () => void): void {
|
|
229
|
+
this.eventEmitter.addListener('storage', callback);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
removeStorageListener(callback: () => void): void {
|
|
233
|
+
this.eventEmitter.removeListener('storage', callback);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getScreenWidth(): number {
|
|
237
|
+
return Dimensions.get('window').width;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getScreenHeight(): number {
|
|
241
|
+
return Dimensions.get('window').height;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
loadScript(id: string, src: string, onLoad: () => void): HTMLScriptElement | null {
|
|
245
|
+
console.warn('Script loading not supported in React Native');
|
|
246
|
+
onLoad();
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
scriptExists(id: string): boolean {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
removeScript(scriptElement: HTMLScriptElement): void {
|
|
255
|
+
// No-op
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
isPopup(): boolean {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
closeWindow(): void {
|
|
263
|
+
// No-op
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### ⚙️ Complete Setup
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// App.tsx
|
|
272
|
+
import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.client';
|
|
197
273
|
import { ReactNativeStorageService } from './services/ReactNativeStorageService';
|
|
198
274
|
import { ReactNativeRoutingService } from './services/ReactNativeRoutingService';
|
|
275
|
+
import { ReactNativePlatformService } from './services/ReactNativePlatformService';
|
|
276
|
+
import { Platform, Alert } from 'react-native';
|
|
199
277
|
|
|
200
278
|
setupSocialLogin(x => {
|
|
201
279
|
x.apiUri = "https://api.yourdomain.com";
|
|
202
280
|
|
|
203
|
-
// ✅
|
|
281
|
+
// ✅ Inject React Native implementations
|
|
204
282
|
x.storageService = new ReactNativeStorageService();
|
|
205
283
|
x.routingService = new ReactNativeRoutingService();
|
|
284
|
+
x.platformService = new ReactNativePlatformService();
|
|
206
285
|
|
|
207
|
-
// Platform configuration
|
|
208
286
|
x.platform = {
|
|
209
|
-
type: PlatformType.Auto,
|
|
287
|
+
type: PlatformType.Auto,
|
|
210
288
|
redirectPath: Platform.select({
|
|
211
289
|
ios: 'myapp://oauth/callback',
|
|
212
290
|
android: 'myapp://oauth/callback',
|
|
213
291
|
default: '/account/login'
|
|
214
292
|
}),
|
|
215
|
-
loginMode: LoginMode.Redirect
|
|
293
|
+
loginMode: LoginMode.Redirect
|
|
216
294
|
};
|
|
217
295
|
|
|
218
296
|
x.microsoft.clientId = "your-client-id";
|
|
@@ -224,10 +302,190 @@ setupSocialLogin(x => {
|
|
|
224
302
|
});
|
|
225
303
|
```
|
|
226
304
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### 🎨 Step 5-6: Native Login Buttons
|
|
308
|
+
|
|
309
|
+
#### 📘 Microsoft Login (Native MSAL)
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm install @react-native-community/msal
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// components/MicrosoftLoginButton.tsx
|
|
317
|
+
import React from 'react';
|
|
318
|
+
import { Pressable, Text, StyleSheet, Alert } from 'react-native';
|
|
319
|
+
import { MSALConfiguration, MSALResult, PublicClientApplication } from '@react-native-community/msal';
|
|
320
|
+
import { SocialLoginManager, ProviderType } from 'rystem.authentication.social.client';
|
|
321
|
+
|
|
322
|
+
const MSAL_CONFIG: MSALConfiguration = {
|
|
323
|
+
auth: {
|
|
324
|
+
clientId: 'your-microsoft-client-id',
|
|
325
|
+
authority: 'https://login.microsoftonline.com/consumers',
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const MicrosoftLoginButton = () => {
|
|
330
|
+
const handleLogin = async () => {
|
|
331
|
+
try {
|
|
332
|
+
const pca = new PublicClientApplication(MSAL_CONFIG);
|
|
333
|
+
await pca.init();
|
|
334
|
+
|
|
335
|
+
const result: MSALResult = await pca.acquireToken({
|
|
336
|
+
scopes: ['openid', 'profile', 'email', 'User.Read'],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ✅ Send token to backend
|
|
340
|
+
await SocialLoginManager.Instance(null).updateToken(
|
|
341
|
+
ProviderType.Microsoft,
|
|
342
|
+
result.idToken
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
console.log('✅ Microsoft login successful');
|
|
346
|
+
} catch (error: any) {
|
|
347
|
+
console.error('❌ Microsoft login failed:', error);
|
|
348
|
+
Alert.alert('Login Failed', error.message || 'Unknown error');
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<Pressable style={[styles.button, { backgroundColor: '#2f2f2f' }]} onPress={handleLogin}>
|
|
354
|
+
<Text style={styles.text}>🪟 Sign in with Microsoft</Text>
|
|
355
|
+
</Pressable>
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const styles = StyleSheet.create({
|
|
360
|
+
button: { padding: 12, borderRadius: 8, alignItems: 'center', marginVertical: 8 },
|
|
361
|
+
text: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### 🔴 Google Login (Native SDK)
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npm install @react-native-google-signin/google-signin
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// components/GoogleLoginButton.tsx
|
|
373
|
+
import React from 'react';
|
|
374
|
+
import { Pressable, Text, StyleSheet, Alert } from 'react-native';
|
|
375
|
+
import { GoogleSignin, statusCodes } from '@react-native-google-signin/google-signin';
|
|
376
|
+
import { SocialLoginManager, ProviderType } from 'rystem.authentication.social.client';
|
|
377
|
+
|
|
378
|
+
// Configure once at app startup
|
|
379
|
+
GoogleSignin.configure({
|
|
380
|
+
webClientId: 'your-google-web-client-id.apps.googleusercontent.com',
|
|
381
|
+
offlineAccess: true,
|
|
382
|
+
forceCodeForRefreshToken: true,
|
|
383
|
+
iosClientId: 'your-ios-client-id.apps.googleusercontent.com',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
export const GoogleLoginButton = () => {
|
|
387
|
+
const handleLogin = async () => {
|
|
388
|
+
try {
|
|
389
|
+
await GoogleSignin.hasPlayServices();
|
|
390
|
+
const userInfo = await GoogleSignin.signIn();
|
|
391
|
+
|
|
392
|
+
if (!userInfo.idToken) throw new Error('No ID token received');
|
|
393
|
+
|
|
394
|
+
// ✅ Send token to backend
|
|
395
|
+
await SocialLoginManager.Instance(null).updateToken(
|
|
396
|
+
ProviderType.Google,
|
|
397
|
+
userInfo.idToken
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
console.log('✅ Google login successful');
|
|
401
|
+
} catch (error: any) {
|
|
402
|
+
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
|
|
403
|
+
console.log('User cancelled login');
|
|
404
|
+
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
|
|
405
|
+
Alert.alert('Error', 'Play Services not available');
|
|
406
|
+
} else {
|
|
407
|
+
console.error('❌ Google login failed:', error);
|
|
408
|
+
Alert.alert('Login Failed', error.message);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<Pressable style={[styles.button, { backgroundColor: '#4285F4' }]} onPress={handleLogin}>
|
|
415
|
+
<Text style={styles.text}>🔵 Sign in with Google</Text>
|
|
416
|
+
</Pressable>
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const styles = StyleSheet.create({
|
|
421
|
+
button: { padding: 12, borderRadius: 8, alignItems: 'center', marginVertical: 8 },
|
|
422
|
+
text: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
### 📱 Step 7: Usage in Your App
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// screens/LoginScreen.tsx
|
|
432
|
+
import React from 'react';
|
|
433
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
434
|
+
import { MicrosoftLoginButton } from '../components/MicrosoftLoginButton';
|
|
435
|
+
import { GoogleLoginButton } from '../components/GoogleLoginButton';
|
|
436
|
+
import { useSocialToken, useSocialUser } from 'rystem.authentication.social.client';
|
|
437
|
+
|
|
438
|
+
export const LoginScreen = () => {
|
|
439
|
+
const token = useSocialToken();
|
|
440
|
+
const user = useSocialUser();
|
|
441
|
+
|
|
442
|
+
if (!token.isExpired) {
|
|
443
|
+
return (
|
|
444
|
+
<View style={styles.container}>
|
|
445
|
+
<Text style={styles.welcome}>Welcome, {user.username}!</Text>
|
|
446
|
+
<Text>Provider: {user.provider}</Text>
|
|
447
|
+
<Text>Email: {user.email}</Text>
|
|
448
|
+
</View>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<View style={styles.container}>
|
|
454
|
+
<Text style={styles.title}>Choose Login Method</Text>
|
|
455
|
+
<MicrosoftLoginButton />
|
|
456
|
+
<GoogleLoginButton />
|
|
457
|
+
</View>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const styles = StyleSheet.create({
|
|
462
|
+
container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
|
|
463
|
+
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
|
|
464
|
+
welcome: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
### ✅ React Native Summary
|
|
471
|
+
|
|
472
|
+
**What You Need:**
|
|
473
|
+
1. Implement 3 services: `IStorageService`, `IRoutingService`, `IPlatformService`
|
|
474
|
+
2. Install native OAuth SDKs (not web SDKs!)
|
|
475
|
+
3. Create custom UI components
|
|
476
|
+
4. Call `SocialLoginManager.Instance(null).updateToken()` after native login success
|
|
477
|
+
|
|
478
|
+
**What Works Automatically:**
|
|
479
|
+
- ✅ `useSocialToken()` / `useSocialUser()` hooks
|
|
480
|
+
- ✅ Token storage and refresh logic
|
|
481
|
+
- ✅ Backend communication
|
|
482
|
+
|
|
483
|
+
**Why No `window` Access Issues:**
|
|
484
|
+
- ✅ Library uses service abstractions (`IStorageService`, `IRoutingService`, `IPlatformService`)
|
|
485
|
+
- ✅ You inject platform-specific implementations
|
|
486
|
+
- ✅ No polyfills needed!
|
|
487
|
+
|
|
488
|
+
---
|
|
231
489
|
|
|
232
490
|
### ⚠️ Important for React Router / Next.js Users
|
|
233
491
|
|
|
@@ -240,36 +498,34 @@ If you're using **React Router** or **Next.js App Router**, OAuth callbacks and
|
|
|
240
498
|
- Next.js App Router (v13+)
|
|
241
499
|
- Unit Testing
|
|
242
500
|
|
|
243
|
-
## 🚀 Quick Start
|
|
501
|
+
## 🚀 Quick Start (Web Applications)
|
|
244
502
|
|
|
245
503
|
### 1. Setup Configuration (main.tsx)
|
|
246
504
|
|
|
247
505
|
```typescript
|
|
248
|
-
import { SocialLoginWrapper, setupSocialLogin } from 'rystem.authentication.social.
|
|
506
|
+
import { SocialLoginWrapper, setupSocialLogin } from 'rystem.authentication.social.client';
|
|
249
507
|
import App from './App';
|
|
250
508
|
|
|
251
509
|
setupSocialLogin(x => {
|
|
510
|
+
// 🚀 ONE-LINE SETUP for web applications
|
|
511
|
+
// This configures localStorage, window routing, and browser platform APIs
|
|
512
|
+
x.useBrowserDefaults();
|
|
513
|
+
|
|
252
514
|
// API server URL
|
|
253
515
|
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
|
-
|
|
516
|
+
|
|
260
517
|
// Configure OAuth providers (only clientId needed for client-side)
|
|
261
518
|
x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
|
|
262
519
|
x.google.clientId = "23769141170-lfs24avv5qrj00m4cbmrm202c0fc6gcg.apps.googleusercontent.com";
|
|
263
520
|
x.facebook.clientId = "345885718092912";
|
|
264
521
|
x.github.clientId = "97154d062f2bb5d28620";
|
|
265
|
-
|
|
266
|
-
|
|
522
|
+
|
|
267
523
|
// Error handling callback
|
|
268
524
|
x.onLoginFailure = (error) => {
|
|
269
525
|
console.error(`Login failed: ${error.message} (Code: ${error.code})`);
|
|
270
526
|
alert(`Authentication error: ${error.message}`);
|
|
271
527
|
};
|
|
272
|
-
|
|
528
|
+
|
|
273
529
|
// Automatic token refresh when expired
|
|
274
530
|
x.automaticRefresh = true;
|
|
275
531
|
});
|
|
@@ -285,10 +541,49 @@ function Root() {
|
|
|
285
541
|
export default Root;
|
|
286
542
|
```
|
|
287
543
|
|
|
544
|
+
#### 🤔 What does `useBrowserDefaults()` do?
|
|
545
|
+
|
|
546
|
+
This helper method **automatically configures three essential services** for web browsers:
|
|
547
|
+
|
|
548
|
+
1. **`storageService`**: `LocalStorageService` → Uses browser `localStorage` to persist tokens, PKCE verifiers, and user data
|
|
549
|
+
2. **`routingService`**: `WindowRoutingService` → Uses `window.location` to read OAuth callback parameters and `window.history` for navigation
|
|
550
|
+
3. **`platformService`**: `BrowserPlatformService` → Uses `window`, `document`, and DOM APIs for popup windows, screen dimensions, and external SDK loading
|
|
551
|
+
|
|
552
|
+
**Why is this needed?**
|
|
553
|
+
The library is **framework-agnostic** and works in **React**, **React Native**, **Next.js**, and **Expo**. Each environment has different APIs:
|
|
554
|
+
- **Web**: Uses `window`, `localStorage`, `document`
|
|
555
|
+
- **React Native**: Uses `AsyncStorage`, `Linking`, `Dimensions`
|
|
556
|
+
- **Next.js SSR**: May not have `window` available
|
|
557
|
+
|
|
558
|
+
By requiring explicit service configuration, the library:
|
|
559
|
+
- ✅ Forces you to think about your environment
|
|
560
|
+
- ✅ Avoids unexpected crashes in non-browser environments
|
|
561
|
+
- ✅ Makes it clear which dependencies are being used
|
|
562
|
+
- ✅ Allows easy customization (e.g., using React Router instead of `window.history`)
|
|
563
|
+
|
|
564
|
+
**Alternative: Manual configuration**
|
|
565
|
+
|
|
566
|
+
If you want more control, you can configure services manually:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
import { LocalStorageService, WindowRoutingService, BrowserPlatformService } from 'rystem.authentication.social.client';
|
|
570
|
+
|
|
571
|
+
setupSocialLogin(x => {
|
|
572
|
+
x.storageService = new LocalStorageService();
|
|
573
|
+
x.routingService = new WindowRoutingService();
|
|
574
|
+
x.platformService = new BrowserPlatformService();
|
|
575
|
+
// ... rest of config
|
|
576
|
+
});
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**For React Native setup**, see the [📱 React Native Complete Guide](#-react-native-complete-guide) section below.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
288
583
|
### 2. Use in Components
|
|
289
584
|
|
|
290
585
|
```typescript
|
|
291
|
-
import { useSocialToken, useSocialUser, SocialLoginButtons, SocialLogoutButton } from 'rystem.authentication.social.
|
|
586
|
+
import { useSocialToken, useSocialUser, SocialLoginButtons, SocialLogoutButton } from 'rystem.authentication.social.client';
|
|
292
587
|
|
|
293
588
|
export const App = () => {
|
|
294
589
|
const token = useSocialToken();
|
|
@@ -15,8 +15,10 @@ const CreateSocialButton = ({ provider, className = '', redirect_uri, children,
|
|
|
15
15
|
// Clean up any leftover popup result from previous attempts
|
|
16
16
|
// Note: Popup mode uses native localStorage for cross-window communication (storage event)
|
|
17
17
|
// The popup processes token exchange and saves the result (success or error)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
|
|
19
|
+
const social_result = `social_result_${provider}`;
|
|
20
|
+
localStorage.removeItem(social_result);
|
|
21
|
+
}
|
|
20
22
|
}, [provider]);
|
|
21
23
|
(0, react_1.useEffect)(() => {
|
|
22
24
|
// Note: Popup window callback (detect code + write to localStorage)
|
|
@@ -32,37 +34,27 @@ const CreateSocialButton = ({ provider, className = '', redirect_uri, children,
|
|
|
32
34
|
!isSdkLoaded && load();
|
|
33
35
|
}, [isSdkLoaded]);
|
|
34
36
|
(0, react_1.useEffect)(() => () => {
|
|
35
|
-
if (scriptNodeRef.current)
|
|
36
|
-
scriptNodeRef.current
|
|
37
|
+
if (scriptNodeRef.current) {
|
|
38
|
+
settings.platformService.removeScript(scriptNodeRef.current);
|
|
39
|
+
}
|
|
37
40
|
}, []);
|
|
38
41
|
const checkIsExistsSDKScript = (0, react_1.useCallback)(() => {
|
|
39
|
-
return
|
|
40
|
-
}, []);
|
|
41
|
-
const insertScript = (0, react_1.useCallback)((d, s = 'script', id, jsSrc, cb) => {
|
|
42
|
-
const sdkScriptTag = d.createElement(s);
|
|
43
|
-
sdkScriptTag.id = id;
|
|
44
|
-
sdkScriptTag.src = jsSrc;
|
|
45
|
-
sdkScriptTag.async = true;
|
|
46
|
-
sdkScriptTag.defer = true;
|
|
47
|
-
sdkScriptTag.crossorigin = "anonymous";
|
|
48
|
-
const scriptNode = document.getElementsByTagName('script')[0];
|
|
49
|
-
scriptNodeRef.current = sdkScriptTag;
|
|
50
|
-
scriptNode &&
|
|
51
|
-
scriptNode.parentNode &&
|
|
52
|
-
scriptNode.parentNode.insertBefore(sdkScriptTag, scriptNode);
|
|
53
|
-
sdkScriptTag.onload = cb;
|
|
54
|
-
}, []);
|
|
42
|
+
return settings.platformService.scriptExists(scriptId);
|
|
43
|
+
}, [scriptId]);
|
|
55
44
|
const load = (0, react_1.useCallback)(() => {
|
|
56
45
|
if (checkIsExistsSDKScript()) {
|
|
57
46
|
setIsSdkLoaded(true);
|
|
58
47
|
}
|
|
59
48
|
else {
|
|
60
|
-
|
|
49
|
+
const scriptElement = settings.platformService.loadScript(scriptId, scriptUri ?? "", () => {
|
|
61
50
|
onScriptLoad && onScriptLoad();
|
|
62
51
|
setIsSdkLoaded(true);
|
|
63
52
|
});
|
|
53
|
+
if (scriptElement) {
|
|
54
|
+
scriptNodeRef.current = scriptElement;
|
|
55
|
+
}
|
|
64
56
|
}
|
|
65
|
-
}, [checkIsExistsSDKScript,
|
|
57
|
+
}, [checkIsExistsSDKScript, scriptId, scriptUri, onScriptLoad]);
|
|
66
58
|
const handlePostMessage = (0, react_1.useCallback)(async (code) => {
|
|
67
59
|
if (code)
|
|
68
60
|
__1.SocialLoginManager.Instance(null).updateToken(provider, code);
|
|
@@ -75,6 +67,10 @@ const CreateSocialButton = ({ provider, className = '', redirect_uri, children,
|
|
|
75
67
|
settings.onLoginFailure({ code: 7, message: x, provider: provider });
|
|
76
68
|
};
|
|
77
69
|
const onChangeLocalStorage = (0, react_1.useCallback)(() => {
|
|
70
|
+
if (typeof window === 'undefined') {
|
|
71
|
+
console.warn('CreateSocialButton: window not available (React Native or SSR?)');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
78
74
|
window.removeEventListener('storage', onChangeLocalStorage, false);
|
|
79
75
|
// Popup mode: The popup processes the token exchange and saves the result
|
|
80
76
|
// We just need to read the result and update the UI (or show error)
|
|
@@ -131,11 +127,12 @@ const CreateSocialButton = ({ provider, className = '', redirect_uri, children,
|
|
|
131
127
|
}
|
|
132
128
|
else {
|
|
133
129
|
// Popup mode: open in popup window using routing service
|
|
134
|
-
|
|
130
|
+
settings.platformService.addStorageListener(onChangeLocalStorage);
|
|
131
|
+
// Calculate popup position
|
|
135
132
|
const width = 450;
|
|
136
133
|
const height = 730;
|
|
137
|
-
const left =
|
|
138
|
-
const top =
|
|
134
|
+
const left = settings.platformService.getScreenWidth() / 2 - width / 2;
|
|
135
|
+
const top = settings.platformService.getScreenHeight() / 2 - height / 2;
|
|
139
136
|
const features = 'menubar=no,location=no,resizable=no,scrollbars=no,status=no, width=' +
|
|
140
137
|
width +
|
|
141
138
|
', height=' +
|
|
@@ -152,6 +149,10 @@ const CreateSocialButton = ({ provider, className = '', redirect_uri, children,
|
|
|
152
149
|
isSdkLoaded,
|
|
153
150
|
onChangeLocalStorage,
|
|
154
151
|
loginMode,
|
|
152
|
+
load,
|
|
153
|
+
onClick,
|
|
154
|
+
handlePostMessage,
|
|
155
|
+
handleError,
|
|
155
156
|
]);
|
|
156
157
|
return ((0, jsx_runtime_1.jsx)("div", { className: className, onClick: onLogin, children: children }, provider.toString()));
|
|
157
158
|
};
|
|
@@ -23,13 +23,15 @@ const SocialLoginWrapper = (c) => {
|
|
|
23
23
|
const loginMode = settings.platform?.loginMode || LoginMode_1.LoginMode.Popup;
|
|
24
24
|
// Use routingService to read URL parameters (supports React Router, Next.js, etc.)
|
|
25
25
|
const routingService = settings.routingService;
|
|
26
|
+
const storageService = settings.storageService;
|
|
27
|
+
const platformService = settings.platformService;
|
|
26
28
|
const code = routingService.getSearchParam(queryCode);
|
|
27
29
|
const stateParam = routingService.getSearchParam(queryState);
|
|
28
30
|
console.log('🔍 SocialLoginWrapper: OAuth callback detection:', {
|
|
29
31
|
code: code ? 'present' : 'missing',
|
|
30
32
|
state: stateParam,
|
|
31
33
|
loginMode,
|
|
32
|
-
isPopup:
|
|
34
|
+
isPopup: platformService.isPopup(),
|
|
33
35
|
alreadyProcessed: callbackProcessedRef.current,
|
|
34
36
|
routingService: routingService.constructor.name
|
|
35
37
|
});
|
|
@@ -44,43 +46,41 @@ const SocialLoginWrapper = (c) => {
|
|
|
44
46
|
const providerName = ProviderType_1.ProviderType[providerType];
|
|
45
47
|
// Mark as processed BEFORE making API call
|
|
46
48
|
callbackProcessedRef.current = true;
|
|
47
|
-
// Check if we're in a popup window
|
|
48
|
-
|
|
49
|
-
if (window.opener) {
|
|
49
|
+
// Check if we're in a popup window using platformService
|
|
50
|
+
if (platformService.isPopup()) {
|
|
50
51
|
// POPUP MODE: Process token exchange IN THE POPUP, then save result and close
|
|
51
52
|
console.log('✅ [Popup] Detected popup window, processing token exchange in popup');
|
|
52
|
-
const social_code = `social_code_${providerType}`;
|
|
53
53
|
const social_result = `social_result_${providerType}`;
|
|
54
54
|
__1.SocialLoginManager.Instance(null).updateToken(providerType, code)
|
|
55
55
|
.then(() => {
|
|
56
56
|
console.log('✅ [Popup] Token exchange successful, saving success result');
|
|
57
|
-
// Save success result for main window
|
|
58
|
-
|
|
57
|
+
// Save success result for main window using storage service
|
|
58
|
+
storageService.set(social_result, JSON.stringify({
|
|
59
59
|
success: true,
|
|
60
60
|
provider: providerName
|
|
61
61
|
}));
|
|
62
|
-
// Close popup after ensuring
|
|
62
|
+
// Close popup after ensuring storage is written
|
|
63
63
|
setTimeout(() => {
|
|
64
64
|
console.log('🔒 [Popup] Closing popup after successful login');
|
|
65
|
-
|
|
65
|
+
platformService.closeWindow();
|
|
66
66
|
}, 100);
|
|
67
67
|
})
|
|
68
68
|
.catch((error) => {
|
|
69
69
|
console.error('❌ [Popup] Token exchange failed:', error);
|
|
70
|
-
// Save error result for main window
|
|
71
|
-
|
|
70
|
+
// Save error result for main window using storage service
|
|
71
|
+
storageService.set(social_result, JSON.stringify({
|
|
72
72
|
success: false,
|
|
73
73
|
provider: providerName,
|
|
74
74
|
error: error.message || 'Token exchange failed'
|
|
75
75
|
}));
|
|
76
|
-
// Close popup after ensuring
|
|
76
|
+
// Close popup after ensuring storage is written
|
|
77
77
|
setTimeout(() => {
|
|
78
78
|
console.log('🔒 [Popup] Closing popup after error');
|
|
79
|
-
|
|
79
|
+
platformService.closeWindow();
|
|
80
80
|
}, 100);
|
|
81
81
|
});
|
|
82
82
|
// Note: The main window's CreateSocialButton will receive the storage event
|
|
83
|
-
// and read the result (success or error) from
|
|
83
|
+
// and read the result (success or error) from storage
|
|
84
84
|
}
|
|
85
85
|
else {
|
|
86
86
|
// REDIRECT MODE: Process token exchange directly in main window
|
|
@@ -89,7 +89,6 @@ const SocialLoginWrapper = (c) => {
|
|
|
89
89
|
.then(() => {
|
|
90
90
|
console.log('✅ Token exchange successful');
|
|
91
91
|
// Get return URL saved before OAuth redirect using storage service
|
|
92
|
-
const storageService = settings.storageService;
|
|
93
92
|
const returnUrl = storageService.get('social_login_return_url');
|
|
94
93
|
storageService.remove('social_login_return_url');
|
|
95
94
|
if (returnUrl) {
|
|
@@ -116,7 +115,7 @@ const SocialLoginWrapper = (c) => {
|
|
|
116
115
|
});
|
|
117
116
|
}
|
|
118
117
|
// Clean return URL on error using storage service
|
|
119
|
-
|
|
118
|
+
storageService.remove('social_login_return_url');
|
|
120
119
|
// Clean URL even on error using routing service
|
|
121
120
|
const cleanPath = routingService.getCurrentPath().split('?')[0];
|
|
122
121
|
routingService.navigateReplace(cleanPath);
|
package/lib/index.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export { TokenStorageService } from './services/TokenStorageService';
|
|
|
40
40
|
export { UserStorageService } from './services/UserStorageService';
|
|
41
41
|
export type { IRoutingService } from './services/IRoutingService';
|
|
42
42
|
export { WindowRoutingService } from './services/WindowRoutingService';
|
|
43
|
+
export type { IPlatformService } from './services/IPlatformService';
|
|
44
|
+
export { BrowserPlatformService } from './services/BrowserPlatformService';
|
|
43
45
|
export { PlatformType } from './models/setup/PlatformType';
|
|
44
46
|
export { LoginMode } from './models/setup/LoginMode';
|
|
45
47
|
export type { PlatformConfig, PlatformSelector } from './models/setup/PlatformConfig';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.buildRedirectUri = exports.isReactNative = exports.isMobilePlatform = exports.getDefaultRedirectUri = exports.detectPlatform = exports.LoginMode = exports.PlatformType = exports.BrowserPlatformService = exports.WindowRoutingService = exports.UserStorageService = exports.TokenStorageService = exports.PkceStorageService = exports.LocalStorageService = exports.PinterestIcon = exports.TikTokIcon = exports.InstagramIcon = exports.AmazonIcon = exports.LinkedInIcon = exports.XIcon = exports.GitHubIcon = exports.FacebookIcon = exports.GoogleIcon = exports.MicrosoftIcon = exports.ModernSocialButton = exports.generateCodeChallenge = exports.generateCodeVerifier = exports.SocialLoginButtons = exports.CreateSocialButton = exports.ProviderType = exports.SocialLoginManager = exports.setupSocialLogin = exports.getSocialLoginSettings = exports.removeSocialLogin = exports.useSocialUser = exports.useSocialToken = exports.SocialLoginContextLogout = exports.SocialLoginContextRefresh = exports.SocialLoginContextUpdate = exports.SocialLoginWrapper = exports.SocialLogoutButton = exports.PinterestButton = exports.InstagramButton = exports.TikTokButton = exports.XButton = exports.LinkedinButton = exports.GitHubButton = exports.FacebookButton = exports.MicrosoftButton = exports.GoogleButton = exports.AmazonButton = void 0;
|
|
4
|
+
exports.selectByPlatform = void 0;
|
|
4
5
|
var AmazonButton_1 = require("./buttons/singles/AmazonButton");
|
|
5
6
|
Object.defineProperty(exports, "AmazonButton", { enumerable: true, get: function () { return AmazonButton_1.AmazonButton; } });
|
|
6
7
|
var GoogleButton_1 = require("./buttons/singles/GoogleButton");
|
|
@@ -75,6 +76,8 @@ var UserStorageService_1 = require("./services/UserStorageService");
|
|
|
75
76
|
Object.defineProperty(exports, "UserStorageService", { enumerable: true, get: function () { return UserStorageService_1.UserStorageService; } });
|
|
76
77
|
var WindowRoutingService_1 = require("./services/WindowRoutingService");
|
|
77
78
|
Object.defineProperty(exports, "WindowRoutingService", { enumerable: true, get: function () { return WindowRoutingService_1.WindowRoutingService; } });
|
|
79
|
+
var BrowserPlatformService_1 = require("./services/BrowserPlatformService");
|
|
80
|
+
Object.defineProperty(exports, "BrowserPlatformService", { enumerable: true, get: function () { return BrowserPlatformService_1.BrowserPlatformService; } });
|
|
78
81
|
// Platform and login mode support
|
|
79
82
|
var PlatformType_1 = require("./models/setup/PlatformType");
|
|
80
83
|
Object.defineProperty(exports, "PlatformType", { enumerable: true, get: function () { return PlatformType_1.PlatformType; } });
|
|
@@ -2,6 +2,7 @@ import { SocialLoginErrorResponse, SocialParameter } from "../..";
|
|
|
2
2
|
import { PlatformConfig } from "./PlatformConfig";
|
|
3
3
|
import { IStorageService } from "../../services/IStorageService";
|
|
4
4
|
import { IRoutingService } from "../../services/IRoutingService";
|
|
5
|
+
import { IPlatformService } from "../../services/IPlatformService";
|
|
5
6
|
export interface SocialLoginSettings {
|
|
6
7
|
apiUri: string;
|
|
7
8
|
automaticRefresh: boolean;
|
|
@@ -10,26 +11,73 @@ export interface SocialLoginSettings {
|
|
|
10
11
|
title: string | null;
|
|
11
12
|
/**
|
|
12
13
|
* Storage service for persisting tokens, PKCE verifiers, etc.
|
|
13
|
-
* Default: LocalStorageService (browser localStorage)
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* ⚠️ **Required**: Must be configured explicitly or via `useBrowserDefaults()`.
|
|
16
|
+
*
|
|
17
|
+
* Web: Use `x.useBrowserDefaults()` or manually set `new LocalStorageService()`
|
|
18
|
+
* React Native: Implement `IStorageService` with AsyncStorage
|
|
19
|
+
*
|
|
20
|
+
* @example Web (automatic)
|
|
21
|
+
* x.useBrowserDefaults();
|
|
22
|
+
*
|
|
23
|
+
* @example Web (manual)
|
|
24
|
+
* x.storageService = new LocalStorageService();
|
|
25
|
+
*
|
|
26
|
+
* @example React Native
|
|
27
|
+
* x.storageService = new ReactNativeStorageService();
|
|
17
28
|
*/
|
|
18
29
|
storageService: IStorageService;
|
|
19
30
|
/**
|
|
20
31
|
* Routing service for URL parameter reading and navigation
|
|
21
|
-
*
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ **Required**: Must be configured explicitly or via `useBrowserDefaults()`.
|
|
34
|
+
*
|
|
35
|
+
* Web: Use `x.useBrowserDefaults()` or manually set `new WindowRoutingService()`
|
|
36
|
+
* React Native: Implement `IRoutingService` with Linking API
|
|
22
37
|
*
|
|
23
38
|
* IMPORTANT: Required for React Router, Next.js App Router, and other client-side routing frameworks
|
|
24
39
|
* to properly handle OAuth callbacks, redirects, and return URLs.
|
|
25
40
|
*
|
|
26
|
-
* @example
|
|
27
|
-
*
|
|
41
|
+
* @example Web (automatic)
|
|
42
|
+
* x.useBrowserDefaults();
|
|
28
43
|
*
|
|
29
|
-
* @example
|
|
30
|
-
* routingService
|
|
44
|
+
* @example Web (manual)
|
|
45
|
+
* x.routingService = new WindowRoutingService();
|
|
46
|
+
*
|
|
47
|
+
* @example React Router
|
|
48
|
+
* x.routingService = new ReactRouterRoutingService();
|
|
49
|
+
*
|
|
50
|
+
* @example Next.js App Router
|
|
51
|
+
* x.routingService = new NextAppRouterRoutingService();
|
|
52
|
+
*
|
|
53
|
+
* @example React Native
|
|
54
|
+
* x.routingService = new ReactNativeRoutingService();
|
|
31
55
|
*/
|
|
32
56
|
routingService: IRoutingService;
|
|
57
|
+
/**
|
|
58
|
+
* Platform service for environment-specific operations (events, dimensions, scripts)
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ **Required**: Must be configured explicitly or via `useBrowserDefaults()`.
|
|
61
|
+
*
|
|
62
|
+
* Web: Use `x.useBrowserDefaults()` or manually set `new BrowserPlatformService()`
|
|
63
|
+
* React Native: Implement `IPlatformService` with Dimensions, EventEmitter
|
|
64
|
+
*
|
|
65
|
+
* Used by UI components for:
|
|
66
|
+
* - Storage event listeners (popup ↔ main window communication)
|
|
67
|
+
* - Screen dimensions (popup positioning)
|
|
68
|
+
* - External script loading (Google SDK, Facebook SDK)
|
|
69
|
+
* - Window operations (popup detection, closing)
|
|
70
|
+
*
|
|
71
|
+
* @example Web (automatic)
|
|
72
|
+
* x.useBrowserDefaults();
|
|
73
|
+
*
|
|
74
|
+
* @example Web (manual)
|
|
75
|
+
* x.platformService = new BrowserPlatformService();
|
|
76
|
+
*
|
|
77
|
+
* @example React Native
|
|
78
|
+
* x.platformService = new ReactNativePlatformService();
|
|
79
|
+
*/
|
|
80
|
+
platformService: IPlatformService;
|
|
33
81
|
/**
|
|
34
82
|
* Platform configuration (Web, iOS, Android)
|
|
35
83
|
*
|
|
@@ -58,6 +106,31 @@ export interface SocialLoginSettings {
|
|
|
58
106
|
instagram: SocialParameter;
|
|
59
107
|
pinterest: SocialParameter;
|
|
60
108
|
tiktok: SocialParameter;
|
|
109
|
+
/**
|
|
110
|
+
* 🚀 Helper method for WEB applications
|
|
111
|
+
*
|
|
112
|
+
* Automatically configures browser-based services:
|
|
113
|
+
* - `storageService`: LocalStorageService (uses browser localStorage)
|
|
114
|
+
* - `routingService`: WindowRoutingService (uses window.location and window.history)
|
|
115
|
+
* - `platformService`: BrowserPlatformService (uses window, document, DOM APIs)
|
|
116
|
+
*
|
|
117
|
+
* ⚠️ **Web only**: Do NOT call this in React Native applications!
|
|
118
|
+
*
|
|
119
|
+
* @example Basic web setup
|
|
120
|
+
* setupSocialLogin(x => {
|
|
121
|
+
* x.useBrowserDefaults(); // ✅ One line to configure all browser services
|
|
122
|
+
* x.apiUri = "https://api.example.com";
|
|
123
|
+
* x.microsoft.clientId = "your-client-id";
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* @example With custom routing (React Router, Next.js)
|
|
127
|
+
* setupSocialLogin(x => {
|
|
128
|
+
* x.useBrowserDefaults();
|
|
129
|
+
* x.routingService = new ReactRouterRoutingService(); // Override routing only
|
|
130
|
+
* // ... rest of config
|
|
131
|
+
* });
|
|
132
|
+
*/
|
|
133
|
+
useBrowserDefaults(): void;
|
|
61
134
|
}
|
|
62
135
|
export interface IIdentityTransformer<TIdentity> {
|
|
63
136
|
toPlain: (input: TIdentity | any) => any;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IPlatformService } from './IPlatformService';
|
|
2
|
+
/**
|
|
3
|
+
* Browser implementation of IPlatformService
|
|
4
|
+
* Uses native window, document, and DOM APIs
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* const platformService = new BrowserPlatformService();
|
|
9
|
+
*
|
|
10
|
+
* // Listen for storage events (popup communication)
|
|
11
|
+
* platformService.addStorageListener(() => {
|
|
12
|
+
* console.log('Storage changed!');
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // Get screen dimensions
|
|
16
|
+
* const width = platformService.getScreenWidth();
|
|
17
|
+
*
|
|
18
|
+
* // Load external script (e.g., Google SDK)
|
|
19
|
+
* platformService.loadScript('google-sdk', 'https://apis.google.com/js/api.js', () => {
|
|
20
|
+
* console.log('Google SDK loaded!');
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class BrowserPlatformService implements IPlatformService {
|
|
25
|
+
/**
|
|
26
|
+
* Check if browser APIs are available
|
|
27
|
+
*/
|
|
28
|
+
private isAvailable;
|
|
29
|
+
addStorageListener(callback: () => void): void;
|
|
30
|
+
removeStorageListener(callback: () => void): void;
|
|
31
|
+
getScreenWidth(): number;
|
|
32
|
+
getScreenHeight(): number;
|
|
33
|
+
loadScript(id: string, src: string, onLoad: () => void): HTMLScriptElement | null;
|
|
34
|
+
scriptExists(id: string): boolean;
|
|
35
|
+
removeScript(scriptElement: HTMLScriptElement): void;
|
|
36
|
+
isPopup(): boolean;
|
|
37
|
+
closeWindow(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BrowserPlatformService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Browser implementation of IPlatformService
|
|
6
|
+
* Uses native window, document, and DOM APIs
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const platformService = new BrowserPlatformService();
|
|
11
|
+
*
|
|
12
|
+
* // Listen for storage events (popup communication)
|
|
13
|
+
* platformService.addStorageListener(() => {
|
|
14
|
+
* console.log('Storage changed!');
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Get screen dimensions
|
|
18
|
+
* const width = platformService.getScreenWidth();
|
|
19
|
+
*
|
|
20
|
+
* // Load external script (e.g., Google SDK)
|
|
21
|
+
* platformService.loadScript('google-sdk', 'https://apis.google.com/js/api.js', () => {
|
|
22
|
+
* console.log('Google SDK loaded!');
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
class BrowserPlatformService {
|
|
27
|
+
/**
|
|
28
|
+
* Check if browser APIs are available
|
|
29
|
+
*/
|
|
30
|
+
isAvailable() {
|
|
31
|
+
return typeof window !== 'undefined';
|
|
32
|
+
}
|
|
33
|
+
// ===== Storage Event Handling =====
|
|
34
|
+
addStorageListener(callback) {
|
|
35
|
+
if (!this.isAvailable()) {
|
|
36
|
+
console.warn('BrowserPlatformService: window not available (SSR?)');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
window.addEventListener('storage', callback, false);
|
|
40
|
+
}
|
|
41
|
+
removeStorageListener(callback) {
|
|
42
|
+
if (!this.isAvailable()) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
window.removeEventListener('storage', callback, false);
|
|
46
|
+
}
|
|
47
|
+
// ===== Screen Dimensions =====
|
|
48
|
+
getScreenWidth() {
|
|
49
|
+
if (!this.isAvailable() || !window.screen) {
|
|
50
|
+
return 1024; // Default fallback
|
|
51
|
+
}
|
|
52
|
+
return window.screen.width;
|
|
53
|
+
}
|
|
54
|
+
getScreenHeight() {
|
|
55
|
+
if (!this.isAvailable() || !window.screen) {
|
|
56
|
+
return 768; // Default fallback
|
|
57
|
+
}
|
|
58
|
+
return window.screen.height;
|
|
59
|
+
}
|
|
60
|
+
// ===== Script Loading =====
|
|
61
|
+
loadScript(id, src, onLoad) {
|
|
62
|
+
if (!this.isAvailable() || typeof document === 'undefined') {
|
|
63
|
+
console.warn('BrowserPlatformService: document not available (SSR?)');
|
|
64
|
+
onLoad(); // Call callback anyway to avoid hanging
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// Check if script already exists
|
|
68
|
+
if (this.scriptExists(id)) {
|
|
69
|
+
onLoad();
|
|
70
|
+
return document.getElementById(id);
|
|
71
|
+
}
|
|
72
|
+
const script = document.createElement('script');
|
|
73
|
+
script.id = id;
|
|
74
|
+
script.src = src;
|
|
75
|
+
script.async = true;
|
|
76
|
+
script.defer = true;
|
|
77
|
+
script.crossOrigin = 'anonymous';
|
|
78
|
+
script.onload = onLoad;
|
|
79
|
+
const firstScript = document.getElementsByTagName('script')[0];
|
|
80
|
+
if (firstScript && firstScript.parentNode) {
|
|
81
|
+
firstScript.parentNode.insertBefore(script, firstScript);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
document.head.appendChild(script);
|
|
85
|
+
}
|
|
86
|
+
return script;
|
|
87
|
+
}
|
|
88
|
+
scriptExists(id) {
|
|
89
|
+
if (!this.isAvailable() || typeof document === 'undefined') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return !!document.getElementById(id);
|
|
93
|
+
}
|
|
94
|
+
removeScript(scriptElement) {
|
|
95
|
+
if (!this.isAvailable() || typeof document === 'undefined') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (scriptElement && scriptElement.parentNode) {
|
|
99
|
+
scriptElement.parentNode.removeChild(scriptElement);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ===== Window Operations =====
|
|
103
|
+
isPopup() {
|
|
104
|
+
if (!this.isAvailable()) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return window.opener !== null;
|
|
108
|
+
}
|
|
109
|
+
closeWindow() {
|
|
110
|
+
if (!this.isAvailable()) {
|
|
111
|
+
console.warn('BrowserPlatformService: window not available (SSR?)');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
window.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.BrowserPlatformService = BrowserPlatformService;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic service for environment-specific operations
|
|
3
|
+
* Abstracts browser/native APIs for cross-platform support
|
|
4
|
+
*
|
|
5
|
+
* Implementations:
|
|
6
|
+
* - BrowserPlatformService (web): Uses window, document, storage events
|
|
7
|
+
* - ReactNativePlatformService (mobile): Uses EventEmitter, Dimensions, Linking
|
|
8
|
+
*
|
|
9
|
+
* Use cases:
|
|
10
|
+
* - Web: DOM manipulation, popup windows, storage events
|
|
11
|
+
* - React Native: Custom events, screen dimensions, deep links
|
|
12
|
+
* - SSR: No-op implementations to avoid crashes
|
|
13
|
+
*/
|
|
14
|
+
export interface IPlatformService {
|
|
15
|
+
/**
|
|
16
|
+
* Add listener for storage changes (used for popup → main window communication)
|
|
17
|
+
* Web: Uses window.addEventListener('storage', ...)
|
|
18
|
+
* React Native: Uses custom event emitter or no-op
|
|
19
|
+
*
|
|
20
|
+
* @param callback Function to call when storage changes
|
|
21
|
+
*/
|
|
22
|
+
addStorageListener(callback: () => void): void;
|
|
23
|
+
/**
|
|
24
|
+
* Remove storage event listener
|
|
25
|
+
* Web: Uses window.removeEventListener('storage', ...)
|
|
26
|
+
* React Native: Removes custom event listener
|
|
27
|
+
*
|
|
28
|
+
* @param callback Function to remove
|
|
29
|
+
*/
|
|
30
|
+
removeStorageListener(callback: () => void): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get screen width for popup positioning
|
|
33
|
+
* Web: Returns window.screen.width
|
|
34
|
+
* React Native: Returns Dimensions.get('window').width
|
|
35
|
+
*
|
|
36
|
+
* @returns Screen width in pixels
|
|
37
|
+
*/
|
|
38
|
+
getScreenWidth(): number;
|
|
39
|
+
/**
|
|
40
|
+
* Get screen height for popup positioning
|
|
41
|
+
* Web: Returns window.screen.height
|
|
42
|
+
* React Native: Returns Dimensions.get('window').height
|
|
43
|
+
*
|
|
44
|
+
* @returns Screen height in pixels
|
|
45
|
+
*/
|
|
46
|
+
getScreenHeight(): number;
|
|
47
|
+
/**
|
|
48
|
+
* Load external JavaScript SDK
|
|
49
|
+
* Web: Injects <script> tag into document
|
|
50
|
+
* React Native: No-op (SDKs are bundled or not used)
|
|
51
|
+
*
|
|
52
|
+
* @param id Unique script ID
|
|
53
|
+
* @param src Script URL
|
|
54
|
+
* @param onLoad Callback when script loads
|
|
55
|
+
* @returns Script element if loaded, null otherwise
|
|
56
|
+
*/
|
|
57
|
+
loadScript(id: string, src: string, onLoad: () => void): HTMLScriptElement | null;
|
|
58
|
+
/**
|
|
59
|
+
* Check if script is already loaded
|
|
60
|
+
* Web: Checks document.getElementById(id)
|
|
61
|
+
* React Native: Returns false (no script loading)
|
|
62
|
+
*
|
|
63
|
+
* @param id Script ID
|
|
64
|
+
* @returns True if script exists
|
|
65
|
+
*/
|
|
66
|
+
scriptExists(id: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Remove loaded script
|
|
69
|
+
* Web: Removes script tag from DOM
|
|
70
|
+
* React Native: No-op
|
|
71
|
+
*
|
|
72
|
+
* @param scriptElement Script element to remove
|
|
73
|
+
*/
|
|
74
|
+
removeScript(scriptElement: HTMLScriptElement): void;
|
|
75
|
+
/**
|
|
76
|
+
* Check if current window is a popup window
|
|
77
|
+
* Web: Checks window.opener !== null
|
|
78
|
+
* React Native: Returns false (no popup concept)
|
|
79
|
+
*
|
|
80
|
+
* @returns True if running in a popup window
|
|
81
|
+
*/
|
|
82
|
+
isPopup(): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Close current window (used after popup OAuth callback)
|
|
85
|
+
* Web: Calls window.close()
|
|
86
|
+
* React Native: No-op or custom navigation
|
|
87
|
+
*/
|
|
88
|
+
closeWindow(): void;
|
|
89
|
+
}
|
|
@@ -1,2 +1,29 @@
|
|
|
1
1
|
import { SocialLoginSettings, SocialLoginManager } from "..";
|
|
2
|
+
/**
|
|
3
|
+
* Setup social login authentication
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ **Important**: You MUST configure the following services:
|
|
6
|
+
* - `storageService` (IStorageService)
|
|
7
|
+
* - `routingService` (IRoutingService)
|
|
8
|
+
* - `platformService` (IPlatformService)
|
|
9
|
+
*
|
|
10
|
+
* For **web applications**, use the helper method:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* setupSocialLogin(x => {
|
|
13
|
+
* x.useBrowserDefaults(); // ✅ Sets up localStorage, window routing, and browser platform
|
|
14
|
+
* x.apiUri = "https://api.example.com";
|
|
15
|
+
* x.microsoft.clientId = "your-client-id";
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* For **React Native**, provide custom implementations:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* setupSocialLogin(x => {
|
|
22
|
+
* x.storageService = new ReactNativeStorageService();
|
|
23
|
+
* x.routingService = new ReactNativeRoutingService();
|
|
24
|
+
* x.platformService = new ReactNativePlatformService();
|
|
25
|
+
* // ... rest of config
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
2
29
|
export declare const setupSocialLogin: (settings: (settings: SocialLoginSettings) => void) => SocialLoginManager;
|
|
@@ -7,6 +7,34 @@ const PlatformType_1 = require("../models/setup/PlatformType");
|
|
|
7
7
|
const platform_1 = require("../utils/platform");
|
|
8
8
|
const LocalStorageService_1 = require("../services/LocalStorageService");
|
|
9
9
|
const WindowRoutingService_1 = require("../services/WindowRoutingService");
|
|
10
|
+
const BrowserPlatformService_1 = require("../services/BrowserPlatformService");
|
|
11
|
+
/**
|
|
12
|
+
* Setup social login authentication
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ **Important**: You MUST configure the following services:
|
|
15
|
+
* - `storageService` (IStorageService)
|
|
16
|
+
* - `routingService` (IRoutingService)
|
|
17
|
+
* - `platformService` (IPlatformService)
|
|
18
|
+
*
|
|
19
|
+
* For **web applications**, use the helper method:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* setupSocialLogin(x => {
|
|
22
|
+
* x.useBrowserDefaults(); // ✅ Sets up localStorage, window routing, and browser platform
|
|
23
|
+
* x.apiUri = "https://api.example.com";
|
|
24
|
+
* x.microsoft.clientId = "your-client-id";
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* For **React Native**, provide custom implementations:
|
|
29
|
+
* ```typescript
|
|
30
|
+
* setupSocialLogin(x => {
|
|
31
|
+
* x.storageService = new ReactNativeStorageService();
|
|
32
|
+
* x.routingService = new ReactNativeRoutingService();
|
|
33
|
+
* x.platformService = new ReactNativePlatformService();
|
|
34
|
+
* // ... rest of config
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
10
38
|
const setupSocialLogin = function (settings) {
|
|
11
39
|
// ✅ Lazy evaluation: only access window when actually needed
|
|
12
40
|
const getDefaultApiUri = () => {
|
|
@@ -17,15 +45,17 @@ const setupSocialLogin = function (settings) {
|
|
|
17
45
|
return '';
|
|
18
46
|
};
|
|
19
47
|
const parameters = {
|
|
20
|
-
apiUri: getDefaultApiUri(),
|
|
48
|
+
apiUri: getDefaultApiUri(),
|
|
21
49
|
title: null,
|
|
22
50
|
onLoginFailure: (data) => { console.log(data.code); },
|
|
23
51
|
automaticRefresh: false,
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
// ❌ Temporarily undefined - MUST be configured before validation
|
|
53
|
+
storageService: undefined,
|
|
54
|
+
routingService: undefined,
|
|
55
|
+
platformService: undefined,
|
|
26
56
|
platform: {
|
|
27
57
|
type: PlatformType_1.PlatformType.Auto,
|
|
28
|
-
redirectPath: undefined,
|
|
58
|
+
redirectPath: undefined,
|
|
29
59
|
loginMode: LoginMode_1.LoginMode.Popup
|
|
30
60
|
},
|
|
31
61
|
google: {},
|
|
@@ -38,8 +68,52 @@ const setupSocialLogin = function (settings) {
|
|
|
38
68
|
instagram: {},
|
|
39
69
|
pinterest: {},
|
|
40
70
|
tiktok: {},
|
|
71
|
+
// ✅ Helper method for web applications
|
|
72
|
+
useBrowserDefaults: function () {
|
|
73
|
+
this.storageService = new LocalStorageService_1.LocalStorageService();
|
|
74
|
+
this.routingService = new WindowRoutingService_1.WindowRoutingService();
|
|
75
|
+
this.platformService = new BrowserPlatformService_1.BrowserPlatformService();
|
|
76
|
+
}
|
|
41
77
|
};
|
|
42
78
|
settings(parameters);
|
|
79
|
+
// ✅ VALIDATION: Ensure required services are configured
|
|
80
|
+
const missingServices = [];
|
|
81
|
+
if (!parameters.storageService) {
|
|
82
|
+
missingServices.push('storageService (IStorageService)');
|
|
83
|
+
}
|
|
84
|
+
if (!parameters.routingService) {
|
|
85
|
+
missingServices.push('routingService (IRoutingService)');
|
|
86
|
+
}
|
|
87
|
+
if (!parameters.platformService) {
|
|
88
|
+
missingServices.push('platformService (IPlatformService)');
|
|
89
|
+
}
|
|
90
|
+
if (missingServices.length > 0) {
|
|
91
|
+
const errorMessage = `
|
|
92
|
+
🚨 Missing required services in setupSocialLogin():
|
|
93
|
+
|
|
94
|
+
${missingServices.map(s => ` ❌ ${s}`).join('\n')}
|
|
95
|
+
|
|
96
|
+
📖 Solutions:
|
|
97
|
+
|
|
98
|
+
1️⃣ For WEB applications, use the helper method:
|
|
99
|
+
setupSocialLogin(x => {
|
|
100
|
+
x.useBrowserDefaults(); // ✅ Auto-configures browser services
|
|
101
|
+
x.apiUri = "https://api.example.com";
|
|
102
|
+
x.microsoft.clientId = "your-client-id";
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
2️⃣ For REACT NATIVE, provide custom implementations:
|
|
106
|
+
setupSocialLogin(x => {
|
|
107
|
+
x.storageService = new ReactNativeStorageService();
|
|
108
|
+
x.routingService = new ReactNativeRoutingService();
|
|
109
|
+
x.platformService = new ReactNativePlatformService();
|
|
110
|
+
// ... rest of config
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
📚 Documentation: https://github.com/KeyserDSoze/Rystem/blob/master/src/Authentication/rystem.authentication.social.client/README.md
|
|
114
|
+
`.trim();
|
|
115
|
+
throw new Error(errorMessage);
|
|
116
|
+
}
|
|
43
117
|
// Auto-detect platform if set to 'auto'
|
|
44
118
|
if (!parameters.platform) {
|
|
45
119
|
parameters.platform = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rystem.authentication.social.client",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"author": "Alessandro Rapiti <alessandro.rapiti44@gmail.com>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Framework-agnostic social authentication library for web and mobile (React, React Native, Next.js, Expo, etc.)",
|