@capgo/capacitor-social-login 7.16.0 → 7.18.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 +55 -18
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/AppleProvider.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor/social/login/SocialLoginPlugin.java +32 -1
- package/android/src/main/java/ee/forgr/capacitor/social/login/TwitterLoginActivity.java +93 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/TwitterProvider.java +510 -0
- package/dist/docs.json +188 -8
- package/dist/esm/definitions.d.ts +84 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/google-provider.d.ts +3 -1
- package/dist/esm/google-provider.js +25 -3
- package/dist/esm/google-provider.js.map +1 -1
- package/dist/esm/twitter-provider.d.ts +36 -0
- package/dist/esm/twitter-provider.js +346 -0
- package/dist/esm/twitter-provider.js.map +1 -0
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +59 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +428 -11
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +428 -11
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/SocialLoginPlugin/GoogleProvider.swift +5 -1
- package/ios/Sources/SocialLoginPlugin/SocialLoginPlugin.swift +93 -1
- package/ios/Sources/SocialLoginPlugin/TwitterProvider.swift +381 -0
- package/package.json +1 -1
package/dist/plugin.cjs.js
CHANGED
|
@@ -351,6 +351,13 @@ class GoogleSocialLogin extends BaseSocialLogin {
|
|
|
351
351
|
}
|
|
352
352
|
handleOAuthRedirect(url) {
|
|
353
353
|
const paramsRaw = url.searchParams;
|
|
354
|
+
// Check for errors in search params first (for offline mode)
|
|
355
|
+
const errorInParams = paramsRaw.get('error');
|
|
356
|
+
if (errorInParams) {
|
|
357
|
+
localStorage.removeItem(BaseSocialLogin.OAUTH_STATE_KEY);
|
|
358
|
+
const errorDescription = paramsRaw.get('error_description') || errorInParams;
|
|
359
|
+
return { error: errorDescription };
|
|
360
|
+
}
|
|
354
361
|
const code = paramsRaw.get('code');
|
|
355
362
|
if (code && paramsRaw.has('scope')) {
|
|
356
363
|
return {
|
|
@@ -365,8 +372,15 @@ class GoogleSocialLogin extends BaseSocialLogin {
|
|
|
365
372
|
console.log('handleOAuthRedirect', url.hash);
|
|
366
373
|
if (!hash)
|
|
367
374
|
return null;
|
|
368
|
-
console.log('handleOAuthRedirect ok');
|
|
369
375
|
const params = new URLSearchParams(hash);
|
|
376
|
+
// Check for error cases in hash (e.g., user cancelled)
|
|
377
|
+
const error = params.get('error');
|
|
378
|
+
if (error) {
|
|
379
|
+
localStorage.removeItem(BaseSocialLogin.OAUTH_STATE_KEY);
|
|
380
|
+
const errorDescription = params.get('error_description') || error;
|
|
381
|
+
return { error: errorDescription };
|
|
382
|
+
}
|
|
383
|
+
console.log('handleOAuthRedirect ok');
|
|
370
384
|
const accessToken = params.get('access_token');
|
|
371
385
|
const idToken = params.get('id_token');
|
|
372
386
|
if (accessToken && idToken) {
|
|
@@ -517,7 +531,7 @@ class GoogleSocialLogin extends BaseSocialLogin {
|
|
|
517
531
|
const height = 600;
|
|
518
532
|
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
519
533
|
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
520
|
-
localStorage.setItem(BaseSocialLogin.OAUTH_STATE_KEY, '
|
|
534
|
+
localStorage.setItem(BaseSocialLogin.OAUTH_STATE_KEY, JSON.stringify({ provider: 'google', loginType: this.loginType }));
|
|
521
535
|
const popup = window.open(url, 'Google Sign In', `width=${width},height=${height},left=${left},top=${top},popup=1`);
|
|
522
536
|
let popupClosedInterval;
|
|
523
537
|
let timeoutHandle;
|
|
@@ -528,12 +542,13 @@ class GoogleSocialLogin extends BaseSocialLogin {
|
|
|
528
542
|
return;
|
|
529
543
|
}
|
|
530
544
|
const handleMessage = (event) => {
|
|
531
|
-
var _a, _b, _c;
|
|
545
|
+
var _a, _b, _c, _d;
|
|
532
546
|
if (event.origin !== window.location.origin || ((_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.source) === null || _b === void 0 ? void 0 : _b.startsWith('angular')))
|
|
533
547
|
return;
|
|
534
548
|
if (((_c = event.data) === null || _c === void 0 ? void 0 : _c.type) === 'oauth-response') {
|
|
535
549
|
window.removeEventListener('message', handleMessage);
|
|
536
550
|
clearInterval(popupClosedInterval);
|
|
551
|
+
clearTimeout(timeoutHandle);
|
|
537
552
|
if (this.loginType === 'online') {
|
|
538
553
|
const { accessToken, idToken } = event.data;
|
|
539
554
|
if (accessToken && idToken) {
|
|
@@ -570,6 +585,13 @@ class GoogleSocialLogin extends BaseSocialLogin {
|
|
|
570
585
|
});
|
|
571
586
|
}
|
|
572
587
|
}
|
|
588
|
+
else if (((_d = event.data) === null || _d === void 0 ? void 0 : _d.type) === 'oauth-error') {
|
|
589
|
+
window.removeEventListener('message', handleMessage);
|
|
590
|
+
clearInterval(popupClosedInterval);
|
|
591
|
+
clearTimeout(timeoutHandle);
|
|
592
|
+
const errorMessage = event.data.error || 'User cancelled the OAuth flow';
|
|
593
|
+
reject(new Error(errorMessage));
|
|
594
|
+
}
|
|
573
595
|
// Don't reject for non-OAuth messages, just ignore them
|
|
574
596
|
};
|
|
575
597
|
window.addEventListener('message', handleMessage);
|
|
@@ -590,29 +612,411 @@ class GoogleSocialLogin extends BaseSocialLogin {
|
|
|
590
612
|
}
|
|
591
613
|
}
|
|
592
614
|
|
|
593
|
-
|
|
615
|
+
var __rest = (undefined && undefined.__rest) || function (s, e) {
|
|
616
|
+
var t = {};
|
|
617
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
618
|
+
t[p] = s[p];
|
|
619
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
620
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
621
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
622
|
+
t[p[i]] = s[p[i]];
|
|
623
|
+
}
|
|
624
|
+
return t;
|
|
625
|
+
};
|
|
626
|
+
class TwitterSocialLogin extends BaseSocialLogin {
|
|
594
627
|
constructor() {
|
|
628
|
+
super(...arguments);
|
|
629
|
+
this.clientId = null;
|
|
630
|
+
this.redirectUrl = null;
|
|
631
|
+
this.defaultScopes = ['tweet.read', 'users.read'];
|
|
632
|
+
this.forceLogin = false;
|
|
633
|
+
this.TOKENS_KEY = 'capgo_social_login_twitter_tokens_v1';
|
|
634
|
+
this.STATE_PREFIX = 'capgo_social_login_twitter_state_';
|
|
635
|
+
}
|
|
636
|
+
async initialize(clientId, redirectUrl, defaultScopes, forceLogin, audience) {
|
|
637
|
+
this.clientId = clientId;
|
|
638
|
+
this.redirectUrl = redirectUrl !== null && redirectUrl !== void 0 ? redirectUrl : null;
|
|
639
|
+
if (defaultScopes === null || defaultScopes === void 0 ? void 0 : defaultScopes.length) {
|
|
640
|
+
this.defaultScopes = defaultScopes;
|
|
641
|
+
}
|
|
642
|
+
this.forceLogin = forceLogin !== null && forceLogin !== void 0 ? forceLogin : false;
|
|
643
|
+
this.audience = audience !== null && audience !== void 0 ? audience : undefined;
|
|
644
|
+
}
|
|
645
|
+
async login(options) {
|
|
646
|
+
var _a, _b, _c, _d, _e, _f;
|
|
647
|
+
if (!this.clientId) {
|
|
648
|
+
throw new Error('Twitter Client ID not configured. Call initialize() first.');
|
|
649
|
+
}
|
|
650
|
+
const redirectUri = (_b = (_a = options.redirectUrl) !== null && _a !== void 0 ? _a : this.redirectUrl) !== null && _b !== void 0 ? _b : window.location.origin + window.location.pathname;
|
|
651
|
+
const scopes = ((_c = options.scopes) === null || _c === void 0 ? void 0 : _c.length) ? options.scopes : this.defaultScopes;
|
|
652
|
+
const state = (_d = options.state) !== null && _d !== void 0 ? _d : this.generateState();
|
|
653
|
+
const codeVerifier = (_e = options.codeVerifier) !== null && _e !== void 0 ? _e : this.generateCodeVerifier();
|
|
654
|
+
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
|
655
|
+
this.persistPendingLogin(state, {
|
|
656
|
+
codeVerifier,
|
|
657
|
+
redirectUri,
|
|
658
|
+
scopes,
|
|
659
|
+
});
|
|
660
|
+
localStorage.setItem(BaseSocialLogin.OAUTH_STATE_KEY, JSON.stringify({ provider: 'twitter', state }));
|
|
661
|
+
const params = new URLSearchParams({
|
|
662
|
+
response_type: 'code',
|
|
663
|
+
client_id: this.clientId,
|
|
664
|
+
redirect_uri: redirectUri,
|
|
665
|
+
scope: scopes.join(' '),
|
|
666
|
+
state,
|
|
667
|
+
code_challenge: codeChallenge,
|
|
668
|
+
code_challenge_method: 'S256',
|
|
669
|
+
});
|
|
670
|
+
if (((_f = options.forceLogin) !== null && _f !== void 0 ? _f : this.forceLogin) === true) {
|
|
671
|
+
params.set('force_login', 'true');
|
|
672
|
+
}
|
|
673
|
+
if (this.audience) {
|
|
674
|
+
params.set('audience', this.audience);
|
|
675
|
+
}
|
|
676
|
+
const authUrl = `https://x.com/i/oauth2/authorize?${params.toString()}`;
|
|
677
|
+
const width = 500;
|
|
678
|
+
const height = 650;
|
|
679
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
680
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
681
|
+
const popup = window.open(authUrl, 'XLogin', `width=${width},height=${height},left=${left},top=${top},popup=1`);
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
if (!popup) {
|
|
684
|
+
reject(new Error('Unable to open login window. Please allow popups.'));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const cleanup = (messageHandler, timeoutHandle, intervalHandle) => {
|
|
688
|
+
window.removeEventListener('message', messageHandler);
|
|
689
|
+
clearTimeout(timeoutHandle);
|
|
690
|
+
clearInterval(intervalHandle);
|
|
691
|
+
};
|
|
692
|
+
const messageHandler = (event) => {
|
|
693
|
+
var _a, _b, _c, _d;
|
|
694
|
+
if (event.origin !== window.location.origin) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) === 'oauth-response') {
|
|
698
|
+
if (((_b = event.data) === null || _b === void 0 ? void 0 : _b.provider) && event.data.provider !== 'twitter') {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
cleanup(messageHandler, timeoutHandle, popupClosedInterval);
|
|
702
|
+
const _e = event.data, { provider: _ignoredProvider } = _e, payload = __rest(_e, ["provider"]);
|
|
703
|
+
resolve({
|
|
704
|
+
provider: 'twitter',
|
|
705
|
+
result: payload,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
else if (((_c = event.data) === null || _c === void 0 ? void 0 : _c.type) === 'oauth-error') {
|
|
709
|
+
if (((_d = event.data) === null || _d === void 0 ? void 0 : _d.provider) && event.data.provider !== 'twitter') {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
cleanup(messageHandler, timeoutHandle, popupClosedInterval);
|
|
713
|
+
reject(new Error(event.data.error || 'Twitter login was cancelled.'));
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
window.addEventListener('message', messageHandler);
|
|
717
|
+
const timeoutHandle = window.setTimeout(() => {
|
|
718
|
+
window.removeEventListener('message', messageHandler);
|
|
719
|
+
popup.close();
|
|
720
|
+
reject(new Error('Twitter login timed out.'));
|
|
721
|
+
}, 300000);
|
|
722
|
+
const popupClosedInterval = window.setInterval(() => {
|
|
723
|
+
if (popup.closed) {
|
|
724
|
+
window.removeEventListener('message', messageHandler);
|
|
725
|
+
clearInterval(popupClosedInterval);
|
|
726
|
+
clearTimeout(timeoutHandle);
|
|
727
|
+
reject(new Error('Twitter login window was closed.'));
|
|
728
|
+
}
|
|
729
|
+
}, 1000);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
async logout() {
|
|
733
|
+
localStorage.removeItem(this.TOKENS_KEY);
|
|
734
|
+
}
|
|
735
|
+
async isLoggedIn() {
|
|
736
|
+
const tokens = this.getStoredTokens();
|
|
737
|
+
if (!tokens) {
|
|
738
|
+
return { isLoggedIn: false };
|
|
739
|
+
}
|
|
740
|
+
const isValid = tokens.expiresAt > Date.now();
|
|
741
|
+
if (!isValid) {
|
|
742
|
+
localStorage.removeItem(this.TOKENS_KEY);
|
|
743
|
+
}
|
|
744
|
+
return { isLoggedIn: isValid };
|
|
745
|
+
}
|
|
746
|
+
async getAuthorizationCode() {
|
|
747
|
+
const tokens = this.getStoredTokens();
|
|
748
|
+
if (!tokens) {
|
|
749
|
+
throw new Error('Twitter access token is not available.');
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
accessToken: tokens.accessToken,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async refresh() {
|
|
756
|
+
const tokens = this.getStoredTokens();
|
|
757
|
+
if (!(tokens === null || tokens === void 0 ? void 0 : tokens.refreshToken)) {
|
|
758
|
+
throw new Error('No Twitter refresh token is available. Include offline.access scope to receive one.');
|
|
759
|
+
}
|
|
760
|
+
await this.refreshWithRefreshToken(tokens.refreshToken);
|
|
761
|
+
}
|
|
762
|
+
async handleOAuthRedirect(url, expectedState) {
|
|
763
|
+
const params = url.searchParams;
|
|
764
|
+
const stateFromUrl = expectedState !== null && expectedState !== void 0 ? expectedState : params.get('state');
|
|
765
|
+
if (!stateFromUrl) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
const pending = this.consumePendingLogin(stateFromUrl);
|
|
769
|
+
if (!pending) {
|
|
770
|
+
localStorage.removeItem(BaseSocialLogin.OAUTH_STATE_KEY);
|
|
771
|
+
return { error: 'Twitter login session expired or state mismatch.' };
|
|
772
|
+
}
|
|
773
|
+
const error = params.get('error');
|
|
774
|
+
if (error) {
|
|
775
|
+
localStorage.removeItem(BaseSocialLogin.OAUTH_STATE_KEY);
|
|
776
|
+
return { error: params.get('error_description') || error };
|
|
777
|
+
}
|
|
778
|
+
const code = params.get('code');
|
|
779
|
+
if (!code) {
|
|
780
|
+
localStorage.removeItem(BaseSocialLogin.OAUTH_STATE_KEY);
|
|
781
|
+
return { error: 'Twitter authorization code missing from redirect.' };
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const tokens = await this.exchangeAuthorizationCode(code, pending);
|
|
785
|
+
const profile = await this.fetchProfile(tokens.access_token);
|
|
786
|
+
const expiresAt = Date.now() + tokens.expires_in * 1000;
|
|
787
|
+
const scopeArray = tokens.scope.split(' ').filter(Boolean);
|
|
788
|
+
this.persistTokens({
|
|
789
|
+
accessToken: tokens.access_token,
|
|
790
|
+
refreshToken: tokens.refresh_token,
|
|
791
|
+
expiresAt,
|
|
792
|
+
scope: scopeArray,
|
|
793
|
+
tokenType: tokens.token_type,
|
|
794
|
+
userId: profile.id,
|
|
795
|
+
profile,
|
|
796
|
+
});
|
|
797
|
+
return {
|
|
798
|
+
provider: 'twitter',
|
|
799
|
+
result: {
|
|
800
|
+
accessToken: {
|
|
801
|
+
token: tokens.access_token,
|
|
802
|
+
tokenType: tokens.token_type,
|
|
803
|
+
expires: new Date(expiresAt).toISOString(),
|
|
804
|
+
userId: profile.id,
|
|
805
|
+
},
|
|
806
|
+
refreshToken: tokens.refresh_token,
|
|
807
|
+
scope: scopeArray,
|
|
808
|
+
tokenType: tokens.token_type,
|
|
809
|
+
expiresIn: tokens.expires_in,
|
|
810
|
+
profile,
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
if (err instanceof Error) {
|
|
816
|
+
return { error: err.message };
|
|
817
|
+
}
|
|
818
|
+
return { error: 'Twitter login failed unexpectedly.' };
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
localStorage.removeItem(BaseSocialLogin.OAUTH_STATE_KEY);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async exchangeAuthorizationCode(code, pending) {
|
|
595
825
|
var _a;
|
|
826
|
+
const params = new URLSearchParams({
|
|
827
|
+
grant_type: 'authorization_code',
|
|
828
|
+
client_id: (_a = this.clientId) !== null && _a !== void 0 ? _a : '',
|
|
829
|
+
code,
|
|
830
|
+
redirect_uri: pending.redirectUri,
|
|
831
|
+
code_verifier: pending.codeVerifier,
|
|
832
|
+
});
|
|
833
|
+
const response = await fetch('https://api.x.com/2/oauth2/token', {
|
|
834
|
+
method: 'POST',
|
|
835
|
+
headers: {
|
|
836
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
837
|
+
},
|
|
838
|
+
body: params.toString(),
|
|
839
|
+
});
|
|
840
|
+
if (!response.ok) {
|
|
841
|
+
const text = await response.text();
|
|
842
|
+
throw new Error(`Twitter token exchange failed (${response.status}): ${text}`);
|
|
843
|
+
}
|
|
844
|
+
return (await response.json());
|
|
845
|
+
}
|
|
846
|
+
async refreshWithRefreshToken(refreshToken) {
|
|
847
|
+
var _a, _b;
|
|
848
|
+
const params = new URLSearchParams({
|
|
849
|
+
grant_type: 'refresh_token',
|
|
850
|
+
refresh_token: refreshToken,
|
|
851
|
+
client_id: (_a = this.clientId) !== null && _a !== void 0 ? _a : '',
|
|
852
|
+
});
|
|
853
|
+
const response = await fetch('https://api.x.com/2/oauth2/token', {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
headers: {
|
|
856
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
857
|
+
},
|
|
858
|
+
body: params.toString(),
|
|
859
|
+
});
|
|
860
|
+
if (!response.ok) {
|
|
861
|
+
const text = await response.text();
|
|
862
|
+
throw new Error(`Twitter refresh failed (${response.status}): ${text}`);
|
|
863
|
+
}
|
|
864
|
+
const tokens = (await response.json());
|
|
865
|
+
const profile = await this.fetchProfile(tokens.access_token);
|
|
866
|
+
const expiresAt = Date.now() + tokens.expires_in * 1000;
|
|
867
|
+
const scopeArray = tokens.scope.split(' ').filter(Boolean);
|
|
868
|
+
this.persistTokens({
|
|
869
|
+
accessToken: tokens.access_token,
|
|
870
|
+
refreshToken: (_b = tokens.refresh_token) !== null && _b !== void 0 ? _b : refreshToken,
|
|
871
|
+
expiresAt,
|
|
872
|
+
scope: scopeArray,
|
|
873
|
+
tokenType: tokens.token_type,
|
|
874
|
+
userId: profile.id,
|
|
875
|
+
profile,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
async fetchProfile(accessToken) {
|
|
879
|
+
var _a, _b, _c, _d;
|
|
880
|
+
const fields = ['profile_image_url', 'verified', 'name', 'username'];
|
|
881
|
+
const response = await fetch(`https://api.x.com/2/users/me?user.fields=${fields.join(',')}`, {
|
|
882
|
+
headers: {
|
|
883
|
+
Authorization: `Bearer ${accessToken}`,
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
if (!response.ok) {
|
|
887
|
+
const text = await response.text();
|
|
888
|
+
throw new Error(`Unable to fetch Twitter profile (${response.status}): ${text}`);
|
|
889
|
+
}
|
|
890
|
+
const payload = (await response.json());
|
|
891
|
+
if (!payload.data) {
|
|
892
|
+
throw new Error('Twitter profile payload is missing data.');
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
id: payload.data.id,
|
|
896
|
+
username: payload.data.username,
|
|
897
|
+
name: (_a = payload.data.name) !== null && _a !== void 0 ? _a : null,
|
|
898
|
+
profileImageUrl: (_b = payload.data.profile_image_url) !== null && _b !== void 0 ? _b : null,
|
|
899
|
+
verified: (_c = payload.data.verified) !== null && _c !== void 0 ? _c : false,
|
|
900
|
+
email: (_d = payload.data.email) !== null && _d !== void 0 ? _d : null,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
persistTokens(tokens) {
|
|
904
|
+
localStorage.setItem(this.TOKENS_KEY, JSON.stringify(tokens));
|
|
905
|
+
}
|
|
906
|
+
getStoredTokens() {
|
|
907
|
+
const raw = localStorage.getItem(this.TOKENS_KEY);
|
|
908
|
+
if (!raw) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
return JSON.parse(raw);
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
console.warn('Failed to parse stored Twitter tokens', err);
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
persistPendingLogin(state, payload) {
|
|
920
|
+
localStorage.setItem(`${this.STATE_PREFIX}${state}`, JSON.stringify(payload));
|
|
921
|
+
}
|
|
922
|
+
consumePendingLogin(state) {
|
|
923
|
+
const key = `${this.STATE_PREFIX}${state}`;
|
|
924
|
+
const raw = localStorage.getItem(key);
|
|
925
|
+
localStorage.removeItem(key);
|
|
926
|
+
if (!raw) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
return JSON.parse(raw);
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
console.warn('Failed to parse pending Twitter login payload', err);
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
generateState() {
|
|
938
|
+
return [...crypto.getRandomValues(new Uint8Array(16))].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
939
|
+
}
|
|
940
|
+
generateCodeVerifier() {
|
|
941
|
+
const array = new Uint8Array(64);
|
|
942
|
+
crypto.getRandomValues(array);
|
|
943
|
+
return Array.from(array)
|
|
944
|
+
.map((b) => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'[b % 66])
|
|
945
|
+
.join('');
|
|
946
|
+
}
|
|
947
|
+
async generateCodeChallenge(codeVerifier) {
|
|
948
|
+
const encoder = new TextEncoder();
|
|
949
|
+
const data = encoder.encode(codeVerifier);
|
|
950
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
951
|
+
return this.base64UrlEncode(new Uint8Array(digest));
|
|
952
|
+
}
|
|
953
|
+
base64UrlEncode(buffer) {
|
|
954
|
+
let binary = '';
|
|
955
|
+
buffer.forEach((b) => (binary += String.fromCharCode(b)));
|
|
956
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
class SocialLoginWeb extends core.WebPlugin {
|
|
961
|
+
constructor() {
|
|
596
962
|
super();
|
|
597
963
|
this.googleProvider = new GoogleSocialLogin();
|
|
598
964
|
this.appleProvider = new AppleSocialLogin();
|
|
599
965
|
this.facebookProvider = new FacebookSocialLogin();
|
|
966
|
+
this.twitterProvider = new TwitterSocialLogin();
|
|
600
967
|
// Set up listener for OAuth redirects if we have a pending OAuth flow
|
|
601
968
|
if (localStorage.getItem(SocialLoginWeb.OAUTH_STATE_KEY)) {
|
|
602
969
|
console.log('OAUTH_STATE_KEY found');
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
(_a = window.opener) === null || _a === void 0 ? void 0 : _a.postMessage(Object.assign({ type: 'oauth-response' }, result.result), window.location.origin);
|
|
970
|
+
this.handleOAuthRedirect().catch((error) => {
|
|
971
|
+
console.error('Failed to finish OAuth redirect', error);
|
|
606
972
|
window.close();
|
|
607
|
-
}
|
|
973
|
+
});
|
|
608
974
|
}
|
|
609
975
|
}
|
|
610
|
-
handleOAuthRedirect() {
|
|
976
|
+
async handleOAuthRedirect() {
|
|
977
|
+
var _a, _b, _c;
|
|
611
978
|
const url = new URL(window.location.href);
|
|
612
|
-
|
|
979
|
+
const stateRaw = localStorage.getItem(SocialLoginWeb.OAUTH_STATE_KEY);
|
|
980
|
+
let provider = null;
|
|
981
|
+
let state;
|
|
982
|
+
if (stateRaw) {
|
|
983
|
+
try {
|
|
984
|
+
const parsed = JSON.parse(stateRaw);
|
|
985
|
+
provider = (_a = parsed.provider) !== null && _a !== void 0 ? _a : null;
|
|
986
|
+
state = parsed.state;
|
|
987
|
+
}
|
|
988
|
+
catch (_d) {
|
|
989
|
+
provider = stateRaw === 'true' ? 'google' : null;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
let result = null;
|
|
993
|
+
switch (provider) {
|
|
994
|
+
case 'twitter':
|
|
995
|
+
result = await this.twitterProvider.handleOAuthRedirect(url, state);
|
|
996
|
+
break;
|
|
997
|
+
case 'google':
|
|
998
|
+
default:
|
|
999
|
+
result = this.googleProvider.handleOAuthRedirect(url);
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
if (!result) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if ('error' in result) {
|
|
1006
|
+
const resolvedProvider = provider !== null && provider !== void 0 ? provider : null;
|
|
1007
|
+
(_b = window.opener) === null || _b === void 0 ? void 0 : _b.postMessage({
|
|
1008
|
+
type: 'oauth-error',
|
|
1009
|
+
provider: resolvedProvider,
|
|
1010
|
+
error: result.error,
|
|
1011
|
+
}, window.location.origin);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
(_c = window.opener) === null || _c === void 0 ? void 0 : _c.postMessage(Object.assign({ type: 'oauth-response', provider: result.provider }, result.result), window.location.origin);
|
|
1015
|
+
}
|
|
1016
|
+
window.close();
|
|
613
1017
|
}
|
|
614
1018
|
async initialize(options) {
|
|
615
|
-
var _a, _b, _c;
|
|
1019
|
+
var _a, _b, _c, _d;
|
|
616
1020
|
const initPromises = [];
|
|
617
1021
|
if ((_a = options.google) === null || _a === void 0 ? void 0 : _a.webClientId) {
|
|
618
1022
|
initPromises.push(this.googleProvider.initialize(options.google.webClientId, options.google.mode, options.google.hostedDomain, options.google.redirectUrl));
|
|
@@ -623,6 +1027,9 @@ class SocialLoginWeb extends core.WebPlugin {
|
|
|
623
1027
|
if ((_c = options.facebook) === null || _c === void 0 ? void 0 : _c.appId) {
|
|
624
1028
|
initPromises.push(this.facebookProvider.initialize(options.facebook.appId, options.facebook.locale));
|
|
625
1029
|
}
|
|
1030
|
+
if ((_d = options.twitter) === null || _d === void 0 ? void 0 : _d.clientId) {
|
|
1031
|
+
initPromises.push(this.twitterProvider.initialize(options.twitter.clientId, options.twitter.redirectUrl, options.twitter.defaultScopes, options.twitter.forceLogin, options.twitter.audience));
|
|
1032
|
+
}
|
|
626
1033
|
await Promise.all(initPromises);
|
|
627
1034
|
}
|
|
628
1035
|
async login(options) {
|
|
@@ -633,6 +1040,8 @@ class SocialLoginWeb extends core.WebPlugin {
|
|
|
633
1040
|
return this.appleProvider.login(options.options);
|
|
634
1041
|
case 'facebook':
|
|
635
1042
|
return this.facebookProvider.login(options.options);
|
|
1043
|
+
case 'twitter':
|
|
1044
|
+
return this.twitterProvider.login(options.options);
|
|
636
1045
|
default:
|
|
637
1046
|
throw new Error(`Login for ${options.provider} is not implemented on web`);
|
|
638
1047
|
}
|
|
@@ -645,6 +1054,8 @@ class SocialLoginWeb extends core.WebPlugin {
|
|
|
645
1054
|
return this.appleProvider.logout();
|
|
646
1055
|
case 'facebook':
|
|
647
1056
|
return this.facebookProvider.logout();
|
|
1057
|
+
case 'twitter':
|
|
1058
|
+
return this.twitterProvider.logout();
|
|
648
1059
|
default:
|
|
649
1060
|
throw new Error(`Logout for ${options.provider} is not implemented`);
|
|
650
1061
|
}
|
|
@@ -657,6 +1068,8 @@ class SocialLoginWeb extends core.WebPlugin {
|
|
|
657
1068
|
return this.appleProvider.isLoggedIn();
|
|
658
1069
|
case 'facebook':
|
|
659
1070
|
return this.facebookProvider.isLoggedIn();
|
|
1071
|
+
case 'twitter':
|
|
1072
|
+
return this.twitterProvider.isLoggedIn();
|
|
660
1073
|
default:
|
|
661
1074
|
throw new Error(`isLoggedIn for ${options.provider} is not implemented`);
|
|
662
1075
|
}
|
|
@@ -669,6 +1082,8 @@ class SocialLoginWeb extends core.WebPlugin {
|
|
|
669
1082
|
return this.appleProvider.getAuthorizationCode();
|
|
670
1083
|
case 'facebook':
|
|
671
1084
|
return this.facebookProvider.getAuthorizationCode();
|
|
1085
|
+
case 'twitter':
|
|
1086
|
+
return this.twitterProvider.getAuthorizationCode();
|
|
672
1087
|
default:
|
|
673
1088
|
throw new Error(`getAuthorizationCode for ${options.provider} is not implemented`);
|
|
674
1089
|
}
|
|
@@ -681,6 +1096,8 @@ class SocialLoginWeb extends core.WebPlugin {
|
|
|
681
1096
|
return this.appleProvider.refresh();
|
|
682
1097
|
case 'facebook':
|
|
683
1098
|
return this.facebookProvider.refresh(options.options);
|
|
1099
|
+
case 'twitter':
|
|
1100
|
+
return this.twitterProvider.refresh();
|
|
684
1101
|
default:
|
|
685
1102
|
throw new Error(`Refresh for ${options.provider} is not implemented`);
|
|
686
1103
|
}
|