react-native-nitro-auth 0.5.3 → 0.5.5
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 +60 -30
- package/android/build.gradle +2 -5
- package/android/src/main/cpp/JniOnLoad.cpp +3 -1
- package/android/src/main/cpp/PlatformAuth+Android.cpp +95 -29
- package/android/src/main/java/com/auth/AuthAdapter.kt +124 -126
- package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
- package/cpp/AuthCache.cpp +0 -44
- package/cpp/AuthCache.hpp +0 -7
- package/cpp/HybridAuth.cpp +20 -2
- package/cpp/HybridAuth.hpp +1 -0
- package/ios/AuthAdapter.swift +64 -28
- package/lib/commonjs/Auth.web.js +96 -43
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.js +23 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/service.js +33 -8
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/use-auth.js +51 -54
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/auth-error.js +37 -0
- package/lib/commonjs/utils/auth-error.js.map +1 -0
- package/lib/module/Auth.web.js +96 -43
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/service.js +33 -8
- package/lib/module/service.js.map +1 -1
- package/lib/module/use-auth.js +51 -54
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/auth-error.js +30 -0
- package/lib/module/utils/auth-error.js.map +1 -0
- package/lib/typescript/commonjs/Auth.web.d.ts +7 -0
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts +2 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
- package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
- package/lib/typescript/module/Auth.web.d.ts +7 -0
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts +2 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/auth-error.d.ts +16 -0
- package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
- package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
- package/package.json +7 -7
- package/react-native-nitro-auth.podspec +1 -1
- package/src/Auth.web.ts +124 -50
- package/src/index.ts +1 -0
- package/src/service.ts +34 -8
- package/src/use-auth.ts +81 -114
- package/src/utils/auth-error.ts +49 -0
- package/ios/KeychainStore.swift +0 -43
package/src/Auth.web.ts
CHANGED
|
@@ -166,13 +166,19 @@ const getConfig = (): AuthWebExtraConfig => {
|
|
|
166
166
|
};
|
|
167
167
|
|
|
168
168
|
class AuthWeb implements Auth {
|
|
169
|
+
private readonly _config: AuthWebExtraConfig;
|
|
169
170
|
private _currentUser: AuthUser | undefined;
|
|
170
171
|
private _grantedScopes: string[] = [];
|
|
171
172
|
private _listeners: ((user: AuthUser | undefined) => void)[] = [];
|
|
172
173
|
private _tokenListeners: ((tokens: AuthTokens) => void)[] = [];
|
|
173
174
|
private _storageAdapter: WebStorageDriver | undefined;
|
|
175
|
+
private _browserStorageResolved = false;
|
|
176
|
+
private _browserStorageCache: Storage | undefined;
|
|
177
|
+
private _refreshPromise: Promise<AuthTokens> | undefined;
|
|
178
|
+
private _appleSdkLoadPromise: Promise<void> | undefined;
|
|
174
179
|
|
|
175
180
|
constructor() {
|
|
181
|
+
this._config = getConfig();
|
|
176
182
|
this.loadFromCache();
|
|
177
183
|
}
|
|
178
184
|
|
|
@@ -213,11 +219,11 @@ class AuthWeb implements Auth {
|
|
|
213
219
|
if (this._storageAdapter) {
|
|
214
220
|
return true;
|
|
215
221
|
}
|
|
216
|
-
return
|
|
222
|
+
return this._config.nitroAuthPersistTokensOnWeb === true;
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
private getWebStorageMode(): "session" | "local" | "memory" {
|
|
220
|
-
const configuredMode =
|
|
226
|
+
const configuredMode = this._config.nitroAuthWebStorage;
|
|
221
227
|
if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
|
|
222
228
|
return configuredMode;
|
|
223
229
|
}
|
|
@@ -225,12 +231,19 @@ class AuthWeb implements Auth {
|
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
private getBrowserStorage(): Storage | undefined {
|
|
234
|
+
if (this._browserStorageResolved) {
|
|
235
|
+
return this._browserStorageCache;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this._browserStorageResolved = true;
|
|
228
239
|
if (typeof window === "undefined") {
|
|
240
|
+
this._browserStorageCache = undefined;
|
|
229
241
|
return undefined;
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
const mode = this.getWebStorageMode();
|
|
233
245
|
if (mode === STORAGE_MODE_MEMORY) {
|
|
246
|
+
this._browserStorageCache = undefined;
|
|
234
247
|
return undefined;
|
|
235
248
|
}
|
|
236
249
|
|
|
@@ -240,6 +253,7 @@ class AuthWeb implements Auth {
|
|
|
240
253
|
const testKey = "__nitro_auth_storage_probe__";
|
|
241
254
|
storage.setItem(testKey, "1");
|
|
242
255
|
storage.removeItem(testKey);
|
|
256
|
+
this._browserStorageCache = storage;
|
|
243
257
|
return storage;
|
|
244
258
|
} catch (error) {
|
|
245
259
|
logger.warn(
|
|
@@ -249,6 +263,7 @@ class AuthWeb implements Auth {
|
|
|
249
263
|
error: String(error),
|
|
250
264
|
},
|
|
251
265
|
);
|
|
266
|
+
this._browserStorageCache = undefined;
|
|
252
267
|
return undefined;
|
|
253
268
|
}
|
|
254
269
|
}
|
|
@@ -499,6 +514,22 @@ class AuthWeb implements Auth {
|
|
|
499
514
|
}
|
|
500
515
|
|
|
501
516
|
async refreshToken(): Promise<AuthTokens> {
|
|
517
|
+
if (this._refreshPromise) {
|
|
518
|
+
return this._refreshPromise;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const refreshPromise = this.performRefreshToken();
|
|
522
|
+
this._refreshPromise = refreshPromise;
|
|
523
|
+
try {
|
|
524
|
+
return await refreshPromise;
|
|
525
|
+
} finally {
|
|
526
|
+
if (this._refreshPromise === refreshPromise) {
|
|
527
|
+
this._refreshPromise = undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private async performRefreshToken(): Promise<AuthTokens> {
|
|
502
533
|
if (!this._currentUser) {
|
|
503
534
|
throw new Error("No user logged in");
|
|
504
535
|
}
|
|
@@ -511,15 +542,14 @@ class AuthWeb implements Auth {
|
|
|
511
542
|
throw new Error("No refresh token available");
|
|
512
543
|
}
|
|
513
544
|
|
|
514
|
-
const
|
|
515
|
-
const clientId = config.microsoftClientId;
|
|
545
|
+
const clientId = this._config.microsoftClientId;
|
|
516
546
|
if (!clientId) {
|
|
517
547
|
throw new Error(
|
|
518
548
|
"Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js",
|
|
519
549
|
);
|
|
520
550
|
}
|
|
521
|
-
const tenant =
|
|
522
|
-
const b2cDomain =
|
|
551
|
+
const tenant = this._config.microsoftTenant ?? "common";
|
|
552
|
+
const b2cDomain = this._config.microsoftB2cDomain;
|
|
523
553
|
|
|
524
554
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, b2cDomain);
|
|
525
555
|
const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
|
|
@@ -642,7 +672,9 @@ class AuthWeb implements Auth {
|
|
|
642
672
|
throw new Error("Invalid JWT payload");
|
|
643
673
|
}
|
|
644
674
|
|
|
645
|
-
const
|
|
675
|
+
const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
676
|
+
const padding = "=".repeat((4 - (normalizedPayload.length % 4)) % 4);
|
|
677
|
+
const decoded: unknown = JSON.parse(atob(`${normalizedPayload}${padding}`));
|
|
646
678
|
if (!isJsonObject(decoded)) {
|
|
647
679
|
throw new Error("Expected JWT payload to be an object");
|
|
648
680
|
}
|
|
@@ -715,8 +747,7 @@ class AuthWeb implements Auth {
|
|
|
715
747
|
scopes: string[],
|
|
716
748
|
loginHint?: string,
|
|
717
749
|
): Promise<void> {
|
|
718
|
-
const
|
|
719
|
-
const clientId = config.googleWebClientId;
|
|
750
|
+
const clientId = this._config.googleWebClientId;
|
|
720
751
|
|
|
721
752
|
if (!clientId) {
|
|
722
753
|
throw new Error(
|
|
@@ -812,8 +843,7 @@ class AuthWeb implements Auth {
|
|
|
812
843
|
tenant?: string,
|
|
813
844
|
prompt?: string,
|
|
814
845
|
): Promise<void> {
|
|
815
|
-
const
|
|
816
|
-
const clientId = config.microsoftClientId;
|
|
846
|
+
const clientId = this._config.microsoftClientId;
|
|
817
847
|
|
|
818
848
|
if (!clientId) {
|
|
819
849
|
throw new Error(
|
|
@@ -821,8 +851,8 @@ class AuthWeb implements Auth {
|
|
|
821
851
|
);
|
|
822
852
|
}
|
|
823
853
|
|
|
824
|
-
const effectiveTenant = tenant ??
|
|
825
|
-
const b2cDomain =
|
|
854
|
+
const effectiveTenant = tenant ?? this._config.microsoftTenant ?? "common";
|
|
855
|
+
const b2cDomain = this._config.microsoftB2cDomain;
|
|
826
856
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(
|
|
827
857
|
effectiveTenant,
|
|
828
858
|
b2cDomain,
|
|
@@ -941,10 +971,9 @@ class AuthWeb implements Auth {
|
|
|
941
971
|
expectedNonce: string,
|
|
942
972
|
scopes: string[],
|
|
943
973
|
): Promise<void> {
|
|
944
|
-
const config = getConfig();
|
|
945
974
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(
|
|
946
975
|
tenant,
|
|
947
|
-
|
|
976
|
+
this._config.microsoftB2cDomain,
|
|
948
977
|
);
|
|
949
978
|
const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
|
|
950
979
|
|
|
@@ -1040,55 +1069,100 @@ class AuthWeb implements Auth {
|
|
|
1040
1069
|
}
|
|
1041
1070
|
}
|
|
1042
1071
|
|
|
1043
|
-
private async
|
|
1044
|
-
|
|
1045
|
-
|
|
1072
|
+
private async ensureAppleSdkLoaded(): Promise<void> {
|
|
1073
|
+
if (window.AppleID) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1046
1076
|
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1049
|
-
"Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.",
|
|
1050
|
-
);
|
|
1077
|
+
if (this._appleSdkLoadPromise) {
|
|
1078
|
+
return this._appleSdkLoadPromise;
|
|
1051
1079
|
}
|
|
1052
1080
|
|
|
1053
|
-
|
|
1081
|
+
this._appleSdkLoadPromise = new Promise<void>((resolve, reject) => {
|
|
1082
|
+
const scriptId = "nitro-auth-apple-sdk";
|
|
1083
|
+
const existingScript = document.getElementById(
|
|
1084
|
+
scriptId,
|
|
1085
|
+
) as HTMLScriptElement | null;
|
|
1086
|
+
|
|
1087
|
+
if (existingScript) {
|
|
1088
|
+
if (window.AppleID) {
|
|
1089
|
+
resolve();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
existingScript.addEventListener(
|
|
1094
|
+
"load",
|
|
1095
|
+
() => {
|
|
1096
|
+
resolve();
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
once: true,
|
|
1100
|
+
},
|
|
1101
|
+
);
|
|
1102
|
+
existingScript.addEventListener(
|
|
1103
|
+
"error",
|
|
1104
|
+
() => {
|
|
1105
|
+
this._appleSdkLoadPromise = undefined;
|
|
1106
|
+
reject(new Error("Failed to load Apple SDK"));
|
|
1107
|
+
},
|
|
1108
|
+
{ once: true },
|
|
1109
|
+
);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1054
1113
|
const script = document.createElement("script");
|
|
1114
|
+
script.id = scriptId;
|
|
1055
1115
|
script.src =
|
|
1056
1116
|
"https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
|
|
1057
1117
|
script.async = true;
|
|
1058
1118
|
script.onload = () => {
|
|
1059
|
-
|
|
1060
|
-
reject(new Error("Apple SDK not loaded"));
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
window.AppleID.auth.init({
|
|
1064
|
-
clientId,
|
|
1065
|
-
scope: "name email",
|
|
1066
|
-
redirectURI: window.location.origin,
|
|
1067
|
-
usePopup: true,
|
|
1068
|
-
});
|
|
1069
|
-
window.AppleID.auth
|
|
1070
|
-
.signIn()
|
|
1071
|
-
.then((response: AppleAuthResponse) => {
|
|
1072
|
-
const user: AuthUser = {
|
|
1073
|
-
provider: "apple",
|
|
1074
|
-
idToken: response.authorization.id_token,
|
|
1075
|
-
email: response.user?.email,
|
|
1076
|
-
name: response.user?.name
|
|
1077
|
-
? `${response.user.name.firstName} ${response.user.name.lastName}`.trim()
|
|
1078
|
-
: undefined,
|
|
1079
|
-
};
|
|
1080
|
-
this.updateUser(user);
|
|
1081
|
-
resolve();
|
|
1082
|
-
})
|
|
1083
|
-
.catch((err: unknown) => {
|
|
1084
|
-
reject(this.mapError(err));
|
|
1085
|
-
});
|
|
1119
|
+
resolve();
|
|
1086
1120
|
};
|
|
1087
1121
|
script.onerror = () => {
|
|
1122
|
+
this._appleSdkLoadPromise = undefined;
|
|
1088
1123
|
reject(new Error("Failed to load Apple SDK"));
|
|
1089
1124
|
};
|
|
1090
1125
|
document.head.appendChild(script);
|
|
1091
1126
|
});
|
|
1127
|
+
|
|
1128
|
+
return this._appleSdkLoadPromise;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private async loginApple(): Promise<void> {
|
|
1132
|
+
const clientId = this._config.appleWebClientId;
|
|
1133
|
+
|
|
1134
|
+
if (!clientId) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
"Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.",
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
await this.ensureAppleSdkLoaded();
|
|
1141
|
+
if (!window.AppleID) {
|
|
1142
|
+
throw new Error("Apple SDK not loaded");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
window.AppleID.auth.init({
|
|
1146
|
+
clientId,
|
|
1147
|
+
scope: "name email",
|
|
1148
|
+
redirectURI: window.location.origin,
|
|
1149
|
+
usePopup: true,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
try {
|
|
1153
|
+
const response: AppleAuthResponse = await window.AppleID.auth.signIn();
|
|
1154
|
+
const user: AuthUser = {
|
|
1155
|
+
provider: "apple",
|
|
1156
|
+
idToken: response.authorization.id_token,
|
|
1157
|
+
email: response.user?.email,
|
|
1158
|
+
name: response.user?.name
|
|
1159
|
+
? `${response.user.name.firstName} ${response.user.name.lastName}`.trim()
|
|
1160
|
+
: undefined,
|
|
1161
|
+
};
|
|
1162
|
+
this.updateUser(user);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
throw this.mapError(error);
|
|
1165
|
+
}
|
|
1092
1166
|
}
|
|
1093
1167
|
|
|
1094
1168
|
async silentRestore(): Promise<void> {
|
package/src/index.ts
CHANGED
package/src/service.ts
CHANGED
|
@@ -6,8 +6,10 @@ import type {
|
|
|
6
6
|
LoginOptions,
|
|
7
7
|
AuthUser,
|
|
8
8
|
} from "./Auth.nitro";
|
|
9
|
+
import { AuthError } from "./utils/auth-error";
|
|
9
10
|
|
|
10
11
|
const nitroAuth = NitroModules.createHybridObject<Auth>("Auth");
|
|
12
|
+
|
|
11
13
|
export const AuthService: Auth = {
|
|
12
14
|
get name() {
|
|
13
15
|
return nitroAuth.name;
|
|
@@ -26,23 +28,43 @@ export const AuthService: Auth = {
|
|
|
26
28
|
},
|
|
27
29
|
|
|
28
30
|
async login(provider: AuthProvider, options?: LoginOptions) {
|
|
29
|
-
|
|
31
|
+
try {
|
|
32
|
+
return await nitroAuth.login(provider, options);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw AuthError.from(e);
|
|
35
|
+
}
|
|
30
36
|
},
|
|
31
37
|
|
|
32
38
|
async requestScopes(scopes: string[]) {
|
|
33
|
-
|
|
39
|
+
try {
|
|
40
|
+
return await nitroAuth.requestScopes(scopes);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
throw AuthError.from(e);
|
|
43
|
+
}
|
|
34
44
|
},
|
|
35
45
|
|
|
36
46
|
async revokeScopes(scopes: string[]) {
|
|
37
|
-
|
|
47
|
+
try {
|
|
48
|
+
return await nitroAuth.revokeScopes(scopes);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw AuthError.from(e);
|
|
51
|
+
}
|
|
38
52
|
},
|
|
39
53
|
|
|
40
54
|
async getAccessToken() {
|
|
41
|
-
|
|
55
|
+
try {
|
|
56
|
+
return await nitroAuth.getAccessToken();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
throw AuthError.from(e);
|
|
59
|
+
}
|
|
42
60
|
},
|
|
43
61
|
|
|
44
62
|
async refreshToken() {
|
|
45
|
-
|
|
63
|
+
try {
|
|
64
|
+
return await nitroAuth.refreshToken();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw AuthError.from(e);
|
|
67
|
+
}
|
|
46
68
|
},
|
|
47
69
|
|
|
48
70
|
logout() {
|
|
@@ -50,12 +72,16 @@ export const AuthService: Auth = {
|
|
|
50
72
|
},
|
|
51
73
|
|
|
52
74
|
async silentRestore() {
|
|
53
|
-
|
|
75
|
+
try {
|
|
76
|
+
return await nitroAuth.silentRestore();
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw AuthError.from(e);
|
|
79
|
+
}
|
|
54
80
|
},
|
|
55
81
|
|
|
56
82
|
onAuthStateChanged(callback: (user: AuthUser | undefined) => void) {
|
|
57
|
-
return nitroAuth.onAuthStateChanged(() => {
|
|
58
|
-
callback(
|
|
83
|
+
return nitroAuth.onAuthStateChanged((user) => {
|
|
84
|
+
callback(user);
|
|
59
85
|
});
|
|
60
86
|
},
|
|
61
87
|
|
package/src/use-auth.ts
CHANGED
|
@@ -6,23 +6,22 @@ import type {
|
|
|
6
6
|
AuthTokens,
|
|
7
7
|
} from "./Auth.nitro";
|
|
8
8
|
import { AuthService } from "./service";
|
|
9
|
+
import { AuthError } from "./utils/auth-error";
|
|
9
10
|
|
|
10
11
|
type AuthState = {
|
|
11
12
|
user: AuthUser | undefined;
|
|
12
13
|
scopes: string[];
|
|
13
14
|
loading: boolean;
|
|
14
|
-
error:
|
|
15
|
+
error: AuthError | undefined;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
super(message);
|
|
22
|
-
this.name = "AuthHookError";
|
|
23
|
-
this.underlyingError = underlyingError;
|
|
18
|
+
const areScopesEqual = (left: string[], right: string[]): boolean => {
|
|
19
|
+
if (left.length !== right.length) return false;
|
|
20
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
21
|
+
if (left[i] !== right[i]) return false;
|
|
24
22
|
}
|
|
25
|
-
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
26
25
|
|
|
27
26
|
export type UseAuthReturn = AuthState & {
|
|
28
27
|
hasPlayServices: boolean;
|
|
@@ -43,81 +42,74 @@ export function useAuth(): UseAuthReturn {
|
|
|
43
42
|
error: undefined,
|
|
44
43
|
});
|
|
45
44
|
|
|
45
|
+
const syncStateFromService = useCallback(
|
|
46
|
+
(nextLoading: boolean, nextError: AuthError | undefined) => {
|
|
47
|
+
const nextUser = AuthService.currentUser;
|
|
48
|
+
const nextScopes = AuthService.grantedScopes;
|
|
49
|
+
setState((prev) => {
|
|
50
|
+
if (
|
|
51
|
+
prev.loading === nextLoading &&
|
|
52
|
+
prev.error === nextError &&
|
|
53
|
+
prev.user === nextUser &&
|
|
54
|
+
areScopesEqual(prev.scopes, nextScopes)
|
|
55
|
+
) {
|
|
56
|
+
return prev;
|
|
57
|
+
}
|
|
58
|
+
return { user: nextUser, scopes: nextScopes, loading: nextLoading, error: nextError };
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
63
|
+
|
|
46
64
|
const login = useCallback(
|
|
47
65
|
async (provider: AuthProvider, options?: LoginOptions) => {
|
|
48
66
|
setState((prev) => ({ ...prev, loading: true, error: undefined }));
|
|
49
67
|
try {
|
|
50
68
|
await AuthService.login(provider, options);
|
|
51
|
-
|
|
52
|
-
user: AuthService.currentUser,
|
|
53
|
-
scopes: AuthService.grantedScopes,
|
|
54
|
-
loading: false,
|
|
55
|
-
error: undefined,
|
|
56
|
-
});
|
|
69
|
+
syncStateFromService(false, undefined);
|
|
57
70
|
} catch (e) {
|
|
58
|
-
const error =
|
|
59
|
-
setState((prev) => ({
|
|
60
|
-
...prev,
|
|
61
|
-
loading: false,
|
|
62
|
-
error,
|
|
63
|
-
}));
|
|
71
|
+
const error = AuthError.from(e);
|
|
72
|
+
setState((prev) => ({ ...prev, loading: false, error }));
|
|
64
73
|
throw error;
|
|
65
74
|
}
|
|
66
75
|
},
|
|
67
|
-
[],
|
|
76
|
+
[syncStateFromService],
|
|
68
77
|
);
|
|
69
78
|
|
|
70
79
|
const logout = useCallback(() => {
|
|
71
80
|
AuthService.logout();
|
|
72
|
-
setState({
|
|
73
|
-
user: undefined,
|
|
74
|
-
scopes: [],
|
|
75
|
-
loading: false,
|
|
76
|
-
error: undefined,
|
|
77
|
-
});
|
|
81
|
+
setState({ user: undefined, scopes: [], loading: false, error: undefined });
|
|
78
82
|
}, []);
|
|
79
83
|
|
|
80
|
-
const requestScopes = useCallback(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
loading: false,
|
|
95
|
-
error,
|
|
96
|
-
}));
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}, []);
|
|
84
|
+
const requestScopes = useCallback(
|
|
85
|
+
async (newScopes: string[]) => {
|
|
86
|
+
setState((prev) => ({ ...prev, loading: true, error: undefined }));
|
|
87
|
+
try {
|
|
88
|
+
await AuthService.requestScopes(newScopes);
|
|
89
|
+
syncStateFromService(false, undefined);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const error = AuthError.from(e);
|
|
92
|
+
setState((prev) => ({ ...prev, loading: false, error }));
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
[syncStateFromService],
|
|
97
|
+
);
|
|
100
98
|
|
|
101
|
-
const revokeScopes = useCallback(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
loading: false,
|
|
116
|
-
error,
|
|
117
|
-
}));
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}, []);
|
|
99
|
+
const revokeScopes = useCallback(
|
|
100
|
+
async (scopesToRevoke: string[]) => {
|
|
101
|
+
setState((prev) => ({ ...prev, loading: true, error: undefined }));
|
|
102
|
+
try {
|
|
103
|
+
await AuthService.revokeScopes(scopesToRevoke);
|
|
104
|
+
syncStateFromService(false, undefined);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const error = AuthError.from(e);
|
|
107
|
+
setState((prev) => ({ ...prev, loading: false, error }));
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[syncStateFromService],
|
|
112
|
+
);
|
|
121
113
|
|
|
122
114
|
const getAccessToken = useCallback(() => AuthService.getAccessToken(), []);
|
|
123
115
|
|
|
@@ -125,56 +117,40 @@ export function useAuth(): UseAuthReturn {
|
|
|
125
117
|
setState((prev) => ({ ...prev, loading: true, error: undefined }));
|
|
126
118
|
try {
|
|
127
119
|
const tokens = await AuthService.refreshToken();
|
|
128
|
-
|
|
129
|
-
user: AuthService.currentUser,
|
|
130
|
-
scopes: AuthService.grantedScopes,
|
|
131
|
-
loading: false,
|
|
132
|
-
error: undefined,
|
|
133
|
-
});
|
|
120
|
+
syncStateFromService(false, undefined);
|
|
134
121
|
return tokens;
|
|
135
122
|
} catch (e) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
AuthService.currentUser?.underlyingError,
|
|
140
|
-
);
|
|
141
|
-
setState((prev) => ({
|
|
142
|
-
...prev,
|
|
143
|
-
loading: false,
|
|
144
|
-
error: authError,
|
|
145
|
-
}));
|
|
146
|
-
throw authError;
|
|
123
|
+
const error = AuthError.from(e);
|
|
124
|
+
setState((prev) => ({ ...prev, loading: false, error }));
|
|
125
|
+
throw error;
|
|
147
126
|
}
|
|
148
|
-
}, []);
|
|
127
|
+
}, [syncStateFromService]);
|
|
149
128
|
|
|
150
129
|
const silentRestore = useCallback(async () => {
|
|
151
130
|
setState((prev) => ({ ...prev, loading: true, error: undefined }));
|
|
152
131
|
try {
|
|
153
132
|
await AuthService.silentRestore();
|
|
154
|
-
|
|
155
|
-
user: AuthService.currentUser,
|
|
156
|
-
scopes: AuthService.grantedScopes,
|
|
157
|
-
loading: false,
|
|
158
|
-
error: undefined,
|
|
159
|
-
});
|
|
133
|
+
syncStateFromService(false, undefined);
|
|
160
134
|
} catch (e) {
|
|
161
|
-
const error =
|
|
162
|
-
setState((prev) => ({
|
|
163
|
-
...prev,
|
|
164
|
-
loading: false,
|
|
165
|
-
error,
|
|
166
|
-
}));
|
|
135
|
+
const error = AuthError.from(e);
|
|
136
|
+
setState((prev) => ({ ...prev, loading: false, error }));
|
|
167
137
|
throw error;
|
|
168
138
|
}
|
|
169
|
-
}, []);
|
|
139
|
+
}, [syncStateFromService]);
|
|
170
140
|
|
|
171
141
|
useEffect(() => {
|
|
172
142
|
const unsubscribe = AuthService.onAuthStateChanged((currentUser) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
143
|
+
const nextScopes = AuthService.grantedScopes;
|
|
144
|
+
setState((prev) => {
|
|
145
|
+
if (
|
|
146
|
+
prev.user === currentUser &&
|
|
147
|
+
areScopesEqual(prev.scopes, nextScopes) &&
|
|
148
|
+
prev.loading === false
|
|
149
|
+
) {
|
|
150
|
+
return prev;
|
|
151
|
+
}
|
|
152
|
+
return { ...prev, user: currentUser, scopes: nextScopes, loading: false };
|
|
153
|
+
});
|
|
178
154
|
});
|
|
179
155
|
return unsubscribe;
|
|
180
156
|
}, []);
|
|
@@ -191,15 +167,6 @@ export function useAuth(): UseAuthReturn {
|
|
|
191
167
|
refreshToken,
|
|
192
168
|
silentRestore,
|
|
193
169
|
}),
|
|
194
|
-
[
|
|
195
|
-
state,
|
|
196
|
-
login,
|
|
197
|
-
logout,
|
|
198
|
-
requestScopes,
|
|
199
|
-
revokeScopes,
|
|
200
|
-
getAccessToken,
|
|
201
|
-
refreshToken,
|
|
202
|
-
silentRestore,
|
|
203
|
-
],
|
|
170
|
+
[state, login, logout, requestScopes, revokeScopes, getAccessToken, refreshToken, silentRestore],
|
|
204
171
|
);
|
|
205
172
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AuthErrorCode } from "../Auth.nitro";
|
|
2
|
+
|
|
3
|
+
const AUTH_ERROR_CODES: ReadonlySet<string> = new Set<AuthErrorCode>([
|
|
4
|
+
"cancelled",
|
|
5
|
+
"timeout",
|
|
6
|
+
"popup_blocked",
|
|
7
|
+
"network_error",
|
|
8
|
+
"configuration_error",
|
|
9
|
+
"unsupported_provider",
|
|
10
|
+
"invalid_state",
|
|
11
|
+
"invalid_nonce",
|
|
12
|
+
"token_error",
|
|
13
|
+
"no_id_token",
|
|
14
|
+
"parse_error",
|
|
15
|
+
"refresh_failed",
|
|
16
|
+
"unknown",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function isAuthErrorCode(value: string): value is AuthErrorCode {
|
|
20
|
+
return AUTH_ERROR_CODES.has(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function toAuthErrorCode(raw: string): AuthErrorCode {
|
|
24
|
+
return isAuthErrorCode(raw) ? raw : "unknown";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Typed error thrown by all AuthService operations.
|
|
29
|
+
*
|
|
30
|
+
* - `code` — always a valid `AuthErrorCode`, safe to switch on
|
|
31
|
+
* - `underlyingMessage` — the raw platform message when it differs from `code`
|
|
32
|
+
*/
|
|
33
|
+
export class AuthError extends Error {
|
|
34
|
+
readonly code: AuthErrorCode;
|
|
35
|
+
readonly underlyingMessage: string | undefined;
|
|
36
|
+
|
|
37
|
+
constructor(raw: unknown) {
|
|
38
|
+
const message = raw instanceof Error ? raw.message : String(raw);
|
|
39
|
+
const code = toAuthErrorCode(message);
|
|
40
|
+
super(code);
|
|
41
|
+
this.name = "AuthError";
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.underlyingMessage = code !== message ? message : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static from(e: unknown): AuthError {
|
|
47
|
+
return e instanceof AuthError ? e : new AuthError(e);
|
|
48
|
+
}
|
|
49
|
+
}
|