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.
Files changed (59) hide show
  1. package/README.md +60 -30
  2. package/android/build.gradle +2 -5
  3. package/android/src/main/cpp/JniOnLoad.cpp +3 -1
  4. package/android/src/main/cpp/PlatformAuth+Android.cpp +95 -29
  5. package/android/src/main/java/com/auth/AuthAdapter.kt +124 -126
  6. package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
  7. package/cpp/AuthCache.cpp +0 -44
  8. package/cpp/AuthCache.hpp +0 -7
  9. package/cpp/HybridAuth.cpp +20 -2
  10. package/cpp/HybridAuth.hpp +1 -0
  11. package/ios/AuthAdapter.swift +64 -28
  12. package/lib/commonjs/Auth.web.js +96 -43
  13. package/lib/commonjs/Auth.web.js.map +1 -1
  14. package/lib/commonjs/index.js +23 -1
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/service.js +33 -8
  17. package/lib/commonjs/service.js.map +1 -1
  18. package/lib/commonjs/use-auth.js +51 -54
  19. package/lib/commonjs/use-auth.js.map +1 -1
  20. package/lib/commonjs/utils/auth-error.js +37 -0
  21. package/lib/commonjs/utils/auth-error.js.map +1 -0
  22. package/lib/module/Auth.web.js +96 -43
  23. package/lib/module/Auth.web.js.map +1 -1
  24. package/lib/module/index.js +1 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/service.js +33 -8
  27. package/lib/module/service.js.map +1 -1
  28. package/lib/module/use-auth.js +51 -54
  29. package/lib/module/use-auth.js.map +1 -1
  30. package/lib/module/utils/auth-error.js +30 -0
  31. package/lib/module/utils/auth-error.js.map +1 -0
  32. package/lib/typescript/commonjs/Auth.web.d.ts +7 -0
  33. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/index.d.ts +1 -0
  35. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/use-auth.d.ts +2 -1
  38. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
  40. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
  41. package/lib/typescript/module/Auth.web.d.ts +7 -0
  42. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  43. package/lib/typescript/module/index.d.ts +1 -0
  44. package/lib/typescript/module/index.d.ts.map +1 -1
  45. package/lib/typescript/module/service.d.ts.map +1 -1
  46. package/lib/typescript/module/use-auth.d.ts +2 -1
  47. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  48. package/lib/typescript/module/utils/auth-error.d.ts +16 -0
  49. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
  50. package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
  51. package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
  52. package/package.json +7 -7
  53. package/react-native-nitro-auth.podspec +1 -1
  54. package/src/Auth.web.ts +124 -50
  55. package/src/index.ts +1 -0
  56. package/src/service.ts +34 -8
  57. package/src/use-auth.ts +81 -114
  58. package/src/utils/auth-error.ts +49 -0
  59. package/ios/KeychainStore.swift +0 -43
package/cpp/AuthCache.cpp CHANGED
@@ -1,10 +1,5 @@
1
1
  #include "AuthCache.hpp"
2
2
 
3
- #ifdef __APPLE__
4
- #include <CoreFoundation/CoreFoundation.h>
5
- #include <Security/Security.h>
6
- #endif
7
-
8
3
  #ifdef __ANDROID__
9
4
  #include <jni.h>
10
5
  #include <fbjni/fbjni.h>
@@ -12,34 +7,10 @@
12
7
 
13
8
  namespace margelo::nitro::NitroAuth {
14
9
 
15
- #ifdef __APPLE__
16
- static std::string sInMemoryUserJson;
17
-
18
- void AuthCache::setUserJson(const std::string& json) {
19
- sInMemoryUserJson = json;
20
- }
21
-
22
- std::optional<std::string> AuthCache::getUserJson() {
23
- if (sInMemoryUserJson.empty()) {
24
- return std::nullopt;
25
- }
26
- return sInMemoryUserJson;
27
- }
28
-
29
- void AuthCache::clear() {
30
- sInMemoryUserJson.clear();
31
- }
32
- #endif
33
-
34
10
  #ifdef __ANDROID__
35
11
  using namespace facebook::jni;
36
12
 
37
- struct JContext : JavaClass<JContext> {
38
- static constexpr auto kJavaDescriptor = "Landroid/content/Context;";
39
- };
40
-
41
13
  static facebook::jni::global_ref<jobject> gContext;
42
- static std::string sInMemoryUserJson;
43
14
 
44
15
  void AuthCache::setAndroidContext(void* context) {
45
16
  gContext = facebook::jni::make_global(static_cast<jobject>(context));
@@ -48,21 +19,6 @@ void AuthCache::setAndroidContext(void* context) {
48
19
  void* AuthCache::getAndroidContext() {
49
20
  return gContext.get();
50
21
  }
51
-
52
- void AuthCache::setUserJson(const std::string& json) {
53
- sInMemoryUserJson = json;
54
- }
55
-
56
- std::optional<std::string> AuthCache::getUserJson() {
57
- if (sInMemoryUserJson.empty()) {
58
- return std::nullopt;
59
- }
60
- return sInMemoryUserJson;
61
- }
62
-
63
- void AuthCache::clear() {
64
- sInMemoryUserJson.clear();
65
- }
66
22
  #endif
67
23
 
68
24
  } // namespace margelo::nitro::NitroAuth
package/cpp/AuthCache.hpp CHANGED
@@ -1,16 +1,9 @@
1
1
  #pragma once
2
2
 
3
- #include <string>
4
- #include <optional>
5
-
6
3
  namespace margelo::nitro::NitroAuth {
7
4
 
8
5
  class AuthCache {
9
6
  public:
10
- static void setUserJson(const std::string& json);
11
- static std::optional<std::string> getUserJson();
12
- static void clear();
13
-
14
7
  #ifdef __ANDROID__
15
8
  static void setAndroidContext(void* context);
16
9
  static void* getAndroidContext();
@@ -209,7 +209,16 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
209
209
  }
210
210
 
211
211
  std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
212
- auto promise = Promise<AuthTokens>::create();
212
+ std::shared_ptr<Promise<AuthTokens>> promise;
213
+ {
214
+ std::lock_guard<std::mutex> lock(_mutex);
215
+ if (_refreshInFlight) {
216
+ return _refreshInFlight;
217
+ }
218
+ promise = Promise<AuthTokens>::create();
219
+ _refreshInFlight = promise;
220
+ }
221
+
213
222
  auto refreshPromise = PlatformAuth::refreshToken();
214
223
  refreshPromise->addOnResolvedListener([this, promise](const AuthTokens& tokens) {
215
224
  {
@@ -228,13 +237,22 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
228
237
  _currentUser->expirationTime = tokens.expirationTime;
229
238
  }
230
239
  }
240
+ if (_refreshInFlight == promise) {
241
+ _refreshInFlight = nullptr;
242
+ }
231
243
  }
232
244
  notifyTokensRefreshed(tokens);
233
245
  notifyAuthStateChanged();
234
246
  promise->resolve(tokens);
235
247
  });
236
248
 
237
- refreshPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
249
+ refreshPromise->addOnRejectedListener([this, promise](const std::exception_ptr& error) {
250
+ {
251
+ std::lock_guard<std::mutex> lock(_mutex);
252
+ if (_refreshInFlight == promise) {
253
+ _refreshInFlight = nullptr;
254
+ }
255
+ }
238
256
  promise->reject(error);
239
257
  });
240
258
  return promise;
@@ -46,6 +46,7 @@ private:
46
46
 
47
47
  std::map<int, std::function<void(const AuthTokens&)>> _tokenListeners;
48
48
  int _nextTokenListenerId = 0;
49
+ std::shared_ptr<Promise<AuthTokens>> _refreshInFlight;
49
50
 
50
51
  std::mutex _mutex;
51
52
 
@@ -114,37 +114,43 @@ public class AuthAdapter: NSObject {
114
114
  DispatchQueue.main.async {
115
115
  let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
116
116
  if let error = error {
117
- if (error as NSError).code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
117
+ let nsError = error as NSError
118
+ if nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
118
119
  completion(nil, "cancelled")
120
+ } else if nsError.domain.lowercased().contains("network") || nsError.code == NSURLErrorNotConnectedToInternet {
121
+ completion(nil, "network_error")
119
122
  } else {
120
- completion(nil, error.localizedDescription)
123
+ completion(nil, "unknown")
121
124
  }
122
125
  return
123
126
  }
124
-
127
+
125
128
  guard let callbackURL = callbackURL,
126
129
  let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else {
127
- completion(nil, "No response from Microsoft")
130
+ completion(nil, "unknown")
128
131
  return
129
132
  }
130
-
133
+
131
134
  var params: [String: String] = [:]
132
135
  for item in components.queryItems ?? [] {
133
136
  params[item.name] = item.value
134
137
  }
135
-
138
+
136
139
  if let errorCode = params["error"] {
137
- completion(nil, params["error_description"] ?? errorCode)
140
+ // OAuth error codes are already structured (e.g. "access_denied").
141
+ // Map well-known ones; fall back to "unknown".
142
+ let mapped = mapOAuthError(errorCode)
143
+ completion(nil, mapped)
138
144
  return
139
145
  }
140
-
146
+
141
147
  guard let returnedState = params["state"], returnedState == state else {
142
- completion(nil, "State mismatch - possible CSRF attack")
148
+ completion(nil, "invalid_state")
143
149
  return
144
150
  }
145
-
151
+
146
152
  guard let code = params["code"] else {
147
- completion(nil, "No authorization code in response")
153
+ completion(nil, "unknown")
148
154
  return
149
155
  }
150
156
 
@@ -230,30 +236,34 @@ public class AuthAdapter: NSObject {
230
236
  URLSession.shared.dataTask(with: request) { data, response, error in
231
237
  DispatchQueue.main.async {
232
238
  if let error = error {
233
- completion(nil, error.localizedDescription)
239
+ let nsError = error as NSError
240
+ if nsError.code == NSURLErrorNotConnectedToInternet || nsError.code == NSURLErrorTimedOut {
241
+ completion(nil, "network_error")
242
+ } else {
243
+ completion(nil, "network_error")
244
+ }
234
245
  return
235
246
  }
236
-
247
+
237
248
  guard let data = data,
238
249
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
239
- completion(nil, "Failed to parse token response")
250
+ completion(nil, "parse_error")
240
251
  return
241
252
  }
242
-
253
+
243
254
  if let errorCode = json["error"] as? String {
244
- let desc = json["error_description"] as? String ?? errorCode
245
- completion(nil, desc)
255
+ completion(nil, mapOAuthError(errorCode))
246
256
  return
247
257
  }
248
-
258
+
249
259
  guard let idToken = json["id_token"] as? String else {
250
- completion(nil, "No id_token in token response")
260
+ completion(nil, "no_id_token")
251
261
  return
252
262
  }
253
-
263
+
254
264
  let claims = decodeJwt(idToken)
255
265
  guard claims["nonce"] == expectedNonce else {
256
- completion(nil, "Nonce mismatch - token may be replayed")
266
+ completion(nil, "invalid_nonce")
257
267
  return
258
268
  }
259
269
 
@@ -288,6 +298,8 @@ public class AuthAdapter: NSObject {
288
298
  guard parts.count >= 2 else { return [:] }
289
299
 
290
300
  var base64 = parts[1]
301
+ .replacingOccurrences(of: "-", with: "+")
302
+ .replacingOccurrences(of: "_", with: "/")
291
303
  let remainder = base64.count % 4
292
304
  if remainder > 0 {
293
305
  base64 += String(repeating: "=", count: 4 - remainder)
@@ -309,7 +321,7 @@ public class AuthAdapter: NSObject {
309
321
 
310
322
  private static func handleGoogleResult(_ result: GIDSignInResult?, error: Error?, completion: @escaping (NSDictionary?, String?) -> Void) {
311
323
  if let error = error {
312
- completion(nil, error.localizedDescription)
324
+ completion(nil, mapError(error))
313
325
  return
314
326
  }
315
327
 
@@ -337,15 +349,39 @@ public class AuthAdapter: NSObject {
337
349
 
338
350
  static func mapError(_ error: Error) -> String {
339
351
  let nsError = error as NSError
352
+ // GIDSignIn error codes
340
353
  if nsError.domain == "com.google.GIDSignIn" {
341
- if nsError.code == -5 { return "cancelled" }
354
+ switch nsError.code {
355
+ case -5: return "cancelled" // GIDSignInErrorCodeCanceled
356
+ case -4: return "network_error" // GIDSignInErrorCodeNoCurrentUser (used for network issues)
357
+ default: break
358
+ }
359
+ }
360
+ // ASAuthorizationError codes (Apple Sign-In / ASWebAuthenticationSession)
361
+ if nsError.domain == ASAuthorizationError.errorDomain {
362
+ switch nsError.code {
363
+ case ASAuthorizationError.canceled.rawValue: return "cancelled"
364
+ case ASAuthorizationError.invalidResponse.rawValue: return "configuration_error"
365
+ default: return "unknown"
366
+ }
342
367
  }
343
368
  let msg = error.localizedDescription.lowercased()
344
369
  if msg.contains("cancel") { return "cancelled" }
345
- if msg.contains("network") { return "network_error" }
370
+ if msg.contains("network") || msg.contains("internet") || msg.contains("offline") { return "network_error" }
346
371
  return "unknown"
347
372
  }
348
373
 
374
+ /// Maps OAuth 2.0 error codes (returned in query params or JSON) to AuthErrorCode values.
375
+ private static func mapOAuthError(_ oauthCode: String) -> String {
376
+ switch oauthCode {
377
+ case "access_denied": return "cancelled"
378
+ case "invalid_client", "unauthorized_client", "invalid_scope": return "configuration_error"
379
+ case "invalid_grant", "invalid_request": return "token_error"
380
+ case "temporarily_unavailable", "server_error": return "network_error"
381
+ default: return "unknown"
382
+ }
383
+ }
384
+
349
385
  @objc
350
386
  public static func addScopes(scopes: [String], completion: @escaping (NSDictionary?, String?) -> Void) {
351
387
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
@@ -512,16 +548,16 @@ public class AuthAdapter: NSObject {
512
548
  URLSession.shared.dataTask(with: request) { data, response, error in
513
549
  DispatchQueue.main.async {
514
550
  if let error = error {
515
- completion(nil, error.localizedDescription)
551
+ completion(nil, "network_error")
516
552
  return
517
553
  }
518
554
  guard let data = data,
519
555
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
520
- completion(nil, "Token refresh failed")
556
+ completion(nil, "parse_error")
521
557
  return
522
558
  }
523
559
  if let errorCode = json["error"] as? String {
524
- completion(nil, (json["error_description"] as? String) ?? errorCode)
560
+ completion(nil, AuthAdapter.mapOAuthError(errorCode))
525
561
  return
526
562
  }
527
563
  let idToken = json["id_token"] as? String ?? ""
@@ -593,7 +629,7 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
593
629
  }
594
630
 
595
631
  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
596
- completion(nil, error.localizedDescription)
632
+ completion(nil, AuthAdapter.mapError(error))
597
633
  }
598
634
  }
599
635
 
@@ -91,7 +91,9 @@ class AuthWeb {
91
91
  _grantedScopes = [];
92
92
  _listeners = [];
93
93
  _tokenListeners = [];
94
+ _browserStorageResolved = false;
94
95
  constructor() {
96
+ this._config = getConfig();
95
97
  this.loadFromCache();
96
98
  }
97
99
  isPromiseLike(value) {
@@ -127,21 +129,27 @@ class AuthWeb {
127
129
  if (this._storageAdapter) {
128
130
  return true;
129
131
  }
130
- return getConfig().nitroAuthPersistTokensOnWeb === true;
132
+ return this._config.nitroAuthPersistTokensOnWeb === true;
131
133
  }
132
134
  getWebStorageMode() {
133
- const configuredMode = getConfig().nitroAuthWebStorage;
135
+ const configuredMode = this._config.nitroAuthWebStorage;
134
136
  if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
135
137
  return configuredMode;
136
138
  }
137
139
  return STORAGE_MODE_SESSION;
138
140
  }
139
141
  getBrowserStorage() {
142
+ if (this._browserStorageResolved) {
143
+ return this._browserStorageCache;
144
+ }
145
+ this._browserStorageResolved = true;
140
146
  if (typeof window === "undefined") {
147
+ this._browserStorageCache = undefined;
141
148
  return undefined;
142
149
  }
143
150
  const mode = this.getWebStorageMode();
144
151
  if (mode === STORAGE_MODE_MEMORY) {
152
+ this._browserStorageCache = undefined;
145
153
  return undefined;
146
154
  }
147
155
  const storage = mode === STORAGE_MODE_LOCAL ? window.localStorage : window.sessionStorage;
@@ -149,12 +157,14 @@ class AuthWeb {
149
157
  const testKey = "__nitro_auth_storage_probe__";
150
158
  storage.setItem(testKey, "1");
151
159
  storage.removeItem(testKey);
160
+ this._browserStorageCache = storage;
152
161
  return storage;
153
162
  } catch (error) {
154
163
  _logger.logger.warn("Configured web storage is unavailable; using in-memory fallback", {
155
164
  mode,
156
165
  error: String(error)
157
166
  });
167
+ this._browserStorageCache = undefined;
158
168
  return undefined;
159
169
  }
160
170
  }
@@ -371,6 +381,20 @@ class AuthWeb {
371
381
  return this._currentUser?.accessToken;
372
382
  }
373
383
  async refreshToken() {
384
+ if (this._refreshPromise) {
385
+ return this._refreshPromise;
386
+ }
387
+ const refreshPromise = this.performRefreshToken();
388
+ this._refreshPromise = refreshPromise;
389
+ try {
390
+ return await refreshPromise;
391
+ } finally {
392
+ if (this._refreshPromise === refreshPromise) {
393
+ this._refreshPromise = undefined;
394
+ }
395
+ }
396
+ }
397
+ async performRefreshToken() {
374
398
  if (!this._currentUser) {
375
399
  throw new Error("No user logged in");
376
400
  }
@@ -380,13 +404,12 @@ class AuthWeb {
380
404
  if (!refreshToken) {
381
405
  throw new Error("No refresh token available");
382
406
  }
383
- const config = getConfig();
384
- const clientId = config.microsoftClientId;
407
+ const clientId = this._config.microsoftClientId;
385
408
  if (!clientId) {
386
409
  throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
387
410
  }
388
- const tenant = config.microsoftTenant ?? "common";
389
- const b2cDomain = config.microsoftB2cDomain;
411
+ const tenant = this._config.microsoftTenant ?? "common";
412
+ const b2cDomain = this._config.microsoftB2cDomain;
390
413
  const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, b2cDomain);
391
414
  const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
392
415
  const body = new URLSearchParams({
@@ -480,7 +503,9 @@ class AuthWeb {
480
503
  if (!payload) {
481
504
  throw new Error("Invalid JWT payload");
482
505
  }
483
- const decoded = JSON.parse(atob(payload));
506
+ const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
507
+ const padding = "=".repeat((4 - normalizedPayload.length % 4) % 4);
508
+ const decoded = JSON.parse(atob(`${normalizedPayload}${padding}`));
484
509
  if (!isJsonObject(decoded)) {
485
510
  throw new Error("Expected JWT payload to be an object");
486
511
  }
@@ -531,8 +556,7 @@ class AuthWeb {
531
556
  });
532
557
  }
533
558
  async loginGoogle(scopes, loginHint) {
534
- const config = getConfig();
535
- const clientId = config.googleWebClientId;
559
+ const clientId = this._config.googleWebClientId;
536
560
  if (!clientId) {
537
561
  throw new Error("Google Web Client ID not configured. Add 'GOOGLE_WEB_CLIENT_ID' to your .env file.");
538
562
  }
@@ -603,13 +627,12 @@ class AuthWeb {
603
627
  }
604
628
  }
605
629
  async loginMicrosoft(scopes, loginHint, tenant, prompt) {
606
- const config = getConfig();
607
- const clientId = config.microsoftClientId;
630
+ const clientId = this._config.microsoftClientId;
608
631
  if (!clientId) {
609
632
  throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
610
633
  }
611
- const effectiveTenant = tenant ?? config.microsoftTenant ?? "common";
612
- const b2cDomain = config.microsoftB2cDomain;
634
+ const effectiveTenant = tenant ?? this._config.microsoftTenant ?? "common";
635
+ const b2cDomain = this._config.microsoftB2cDomain;
613
636
  const authBaseUrl = this.getMicrosoftAuthBaseUrl(effectiveTenant, b2cDomain);
614
637
  const effectiveScopes = scopes.length ? scopes : ["openid", "email", "profile", "offline_access", "User.Read"];
615
638
  const codeVerifier = this.generateCodeVerifier();
@@ -680,8 +703,7 @@ class AuthWeb {
680
703
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
681
704
  }
682
705
  async exchangeMicrosoftCodeForTokens(code, codeVerifier, clientId, redirectUri, tenant, expectedNonce, scopes) {
683
- const config = getConfig();
684
- const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, config.microsoftB2cDomain);
706
+ const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, this._config.microsoftB2cDomain);
685
707
  const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
686
708
  const body = new URLSearchParams({
687
709
  client_id: clientId,
@@ -753,45 +775,76 @@ class AuthWeb {
753
775
  return {};
754
776
  }
755
777
  }
756
- async loginApple() {
757
- const config = getConfig();
758
- const clientId = config.appleWebClientId;
759
- if (!clientId) {
760
- throw new Error("Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.");
778
+ async ensureAppleSdkLoaded() {
779
+ if (window.AppleID) {
780
+ return;
761
781
  }
762
- return new Promise((resolve, reject) => {
763
- const script = document.createElement("script");
764
- script.src = "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
765
- script.async = true;
766
- script.onload = () => {
767
- if (!window.AppleID) {
768
- reject(new Error("Apple SDK not loaded"));
782
+ if (this._appleSdkLoadPromise) {
783
+ return this._appleSdkLoadPromise;
784
+ }
785
+ this._appleSdkLoadPromise = new Promise((resolve, reject) => {
786
+ const scriptId = "nitro-auth-apple-sdk";
787
+ const existingScript = document.getElementById(scriptId);
788
+ if (existingScript) {
789
+ if (window.AppleID) {
790
+ resolve();
769
791
  return;
770
792
  }
771
- window.AppleID.auth.init({
772
- clientId,
773
- scope: "name email",
774
- redirectURI: window.location.origin,
775
- usePopup: true
776
- });
777
- window.AppleID.auth.signIn().then(response => {
778
- const user = {
779
- provider: "apple",
780
- idToken: response.authorization.id_token,
781
- email: response.user?.email,
782
- name: response.user?.name ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim() : undefined
783
- };
784
- this.updateUser(user);
793
+ existingScript.addEventListener("load", () => {
785
794
  resolve();
786
- }).catch(err => {
787
- reject(this.mapError(err));
795
+ }, {
796
+ once: true
797
+ });
798
+ existingScript.addEventListener("error", () => {
799
+ this._appleSdkLoadPromise = undefined;
800
+ reject(new Error("Failed to load Apple SDK"));
801
+ }, {
802
+ once: true
788
803
  });
804
+ return;
805
+ }
806
+ const script = document.createElement("script");
807
+ script.id = scriptId;
808
+ script.src = "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
809
+ script.async = true;
810
+ script.onload = () => {
811
+ resolve();
789
812
  };
790
813
  script.onerror = () => {
814
+ this._appleSdkLoadPromise = undefined;
791
815
  reject(new Error("Failed to load Apple SDK"));
792
816
  };
793
817
  document.head.appendChild(script);
794
818
  });
819
+ return this._appleSdkLoadPromise;
820
+ }
821
+ async loginApple() {
822
+ const clientId = this._config.appleWebClientId;
823
+ if (!clientId) {
824
+ throw new Error("Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.");
825
+ }
826
+ await this.ensureAppleSdkLoaded();
827
+ if (!window.AppleID) {
828
+ throw new Error("Apple SDK not loaded");
829
+ }
830
+ window.AppleID.auth.init({
831
+ clientId,
832
+ scope: "name email",
833
+ redirectURI: window.location.origin,
834
+ usePopup: true
835
+ });
836
+ try {
837
+ const response = await window.AppleID.auth.signIn();
838
+ const user = {
839
+ provider: "apple",
840
+ idToken: response.authorization.id_token,
841
+ email: response.user?.email,
842
+ name: response.user?.name ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim() : undefined
843
+ };
844
+ this.updateUser(user);
845
+ } catch (error) {
846
+ throw this.mapError(error);
847
+ }
795
848
  }
796
849
  async silentRestore() {
797
850
  _logger.logger.log("Attempting silent restore...");