react-native-nitro-auth 0.5.3 → 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.
@@ -93,7 +93,7 @@ dependencies {
93
93
  implementation project(":react-native-nitro-modules")
94
94
 
95
95
  // Google Sign-In SDK (full scope support)
96
- implementation "com.google.android.gms:play-services-auth:21.2.0"
96
+ implementation "com.google.android.gms:play-services-auth:21.5.1"
97
97
 
98
98
  // Activity result APIs
99
99
  implementation "androidx.activity:activity-ktx:1.9.3"
@@ -104,8 +104,5 @@ dependencies {
104
104
  // Google Credential Manager (One-Tap / Passkeys)
105
105
  implementation "androidx.credentials:credentials:1.5.0"
106
106
  implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
107
- implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
108
-
109
- // Secure storage (EncryptedSharedPreferences)
110
- implementation "androidx.security:security-crypto:1.0.0"
107
+ implementation "com.google.android.libraries.identity.googleid:googleid:1.2.0"
111
108
  }
@@ -6,6 +6,8 @@
6
6
  #include <fbjni/fbjni.h>
7
7
  #include <NitroModules/NitroLogger.hpp>
8
8
  #include <NitroModules/Promise.hpp>
9
+ #include <exception>
10
+ #include <stdexcept>
9
11
 
10
12
  namespace margelo::nitro::NitroAuth {
11
13
 
@@ -24,6 +26,33 @@ static std::shared_ptr<Promise<AuthUser>> gScopesPromise;
24
26
  static std::shared_ptr<Promise<AuthTokens>> gRefreshPromise;
25
27
  static std::shared_ptr<Promise<std::optional<AuthUser>>> gSilentPromise;
26
28
  static std::mutex gMutex;
29
+ static jclass gAuthAdapterClass = nullptr;
30
+ static jmethodID gLoginMethod = nullptr;
31
+ static jmethodID gRequestScopesMethod = nullptr;
32
+
33
+ static void ensureAuthAdapterMethods(JNIEnv* env) {
34
+ if (gAuthAdapterClass != nullptr && gLoginMethod != nullptr && gRequestScopesMethod != nullptr) {
35
+ return;
36
+ }
37
+
38
+ jclass localAdapterClass = env->FindClass("com/auth/AuthAdapter");
39
+ if (localAdapterClass == nullptr) {
40
+ throw std::runtime_error("Unable to resolve com/auth/AuthAdapter");
41
+ }
42
+ gAuthAdapterClass = static_cast<jclass>(env->NewGlobalRef(localAdapterClass));
43
+ env->DeleteLocalRef(localAdapterClass);
44
+
45
+ gLoginMethod = env->GetStaticMethodID(
46
+ gAuthAdapterClass,
47
+ "loginSync",
48
+ "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZZZLjava/lang/String;Ljava/lang/String;)V"
49
+ );
50
+ gRequestScopesMethod = env->GetStaticMethodID(
51
+ gAuthAdapterClass,
52
+ "requestScopesSync",
53
+ "(Landroid/content/Context;[Ljava/lang/String;)V"
54
+ );
55
+ }
27
56
 
28
57
  std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
29
58
  auto promise = Promise<AuthUser>::create();
@@ -35,6 +64,9 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
35
64
 
36
65
  {
37
66
  std::lock_guard<std::mutex> lock(gMutex);
67
+ if (gLoginPromise) {
68
+ gLoginPromise->reject(std::make_exception_ptr(std::runtime_error("Login request superseded by a newer request")));
69
+ }
38
70
  gLoginPromise = promise;
39
71
  }
40
72
 
@@ -71,30 +103,47 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
71
103
  }
72
104
 
73
105
  JNIEnv* env = Environment::current();
106
+ try {
107
+ ensureAuthAdapterMethods(env);
108
+ } catch (...) {
109
+ promise->reject(std::current_exception());
110
+ return promise;
111
+ }
74
112
  jclass stringClass = env->FindClass("java/lang/String");
75
113
  jobjectArray jScopes = env->NewObjectArray(scopes.size(), stringClass, nullptr);
76
114
  for (size_t i = 0; i < scopes.size(); i++) {
77
115
  env->SetObjectArrayElement(jScopes, i, make_jstring(scopes[i]).get());
78
116
  }
79
117
 
80
- jstring jLoginHint = loginHint.has_value() ? make_jstring(loginHint.value()).get() : nullptr;
81
- jstring jTenant = tenant.has_value() ? make_jstring(tenant.value()).get() : nullptr;
82
- jstring jPrompt = prompt.has_value() ? make_jstring(prompt.value()).get() : nullptr;
83
-
84
- jclass adapterClass = env->FindClass("com/auth/AuthAdapter");
85
- jmethodID loginMethod = env->GetStaticMethodID(adapterClass, "loginSync",
86
- "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZZZLjava/lang/String;Ljava/lang/String;)V");
87
- env->CallStaticVoidMethod(adapterClass, loginMethod,
118
+ local_ref<JString> providerRef = make_jstring(providerStr);
119
+ local_ref<JString> loginHintRef;
120
+ local_ref<JString> tenantRef;
121
+ local_ref<JString> promptRef;
122
+
123
+ if (loginHint.has_value()) {
124
+ loginHintRef = make_jstring(loginHint.value());
125
+ }
126
+ if (tenant.has_value()) {
127
+ tenantRef = make_jstring(tenant.value());
128
+ }
129
+ if (prompt.has_value()) {
130
+ promptRef = make_jstring(prompt.value());
131
+ }
132
+
133
+ env->CallStaticVoidMethod(gAuthAdapterClass, gLoginMethod,
88
134
  contextPtr,
89
- make_jstring(providerStr).get(),
135
+ providerRef.get(),
90
136
  nullptr,
91
137
  jScopes,
92
- jLoginHint,
138
+ loginHintRef.get(),
93
139
  (jboolean)useOneTap,
94
140
  (jboolean)forceAccountPicker,
95
141
  (jboolean)useLegacyGoogleSignIn,
96
- jTenant,
97
- jPrompt);
142
+ tenantRef.get(),
143
+ promptRef.get());
144
+
145
+ env->DeleteLocalRef(jScopes);
146
+ env->DeleteLocalRef(stringClass);
98
147
 
99
148
  return promise;
100
149
  }
@@ -109,20 +158,28 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::requestScopes(const std::vector
109
158
 
110
159
  {
111
160
  std::lock_guard<std::mutex> lock(gMutex);
161
+ if (gScopesPromise) {
162
+ gScopesPromise->reject(std::make_exception_ptr(std::runtime_error("Scope request superseded by a newer request")));
163
+ }
112
164
  gScopesPromise = promise;
113
165
  }
114
166
 
115
167
  JNIEnv* env = Environment::current();
168
+ try {
169
+ ensureAuthAdapterMethods(env);
170
+ } catch (...) {
171
+ promise->reject(std::current_exception());
172
+ return promise;
173
+ }
116
174
  jclass stringClass = env->FindClass("java/lang/String");
117
175
  jobjectArray jScopes = env->NewObjectArray(scopes.size(), stringClass, nullptr);
118
176
  for (size_t i = 0; i < scopes.size(); i++) {
119
177
  env->SetObjectArrayElement(jScopes, i, make_jstring(scopes[i]).get());
120
178
  }
121
179
 
122
- jclass adapterClass = env->FindClass("com/auth/AuthAdapter");
123
- jmethodID requestMethod = env->GetStaticMethodID(adapterClass, "requestScopesSync",
124
- "(Landroid/content/Context;[Ljava/lang/String;)V");
125
- env->CallStaticVoidMethod(adapterClass, requestMethod, contextPtr, jScopes);
180
+ env->CallStaticVoidMethod(gAuthAdapterClass, gRequestScopesMethod, contextPtr, jScopes);
181
+ env->DeleteLocalRef(jScopes);
182
+ env->DeleteLocalRef(stringClass);
126
183
  return promise;
127
184
  }
128
185
 
@@ -136,6 +193,9 @@ std::shared_ptr<Promise<AuthTokens>> PlatformAuth::refreshToken() {
136
193
 
137
194
  {
138
195
  std::lock_guard<std::mutex> lock(gMutex);
196
+ if (gRefreshPromise) {
197
+ gRefreshPromise->reject(std::make_exception_ptr(std::runtime_error("Refresh request superseded by a newer request")));
198
+ }
139
199
  gRefreshPromise = promise;
140
200
  }
141
201
 
@@ -155,6 +215,9 @@ std::shared_ptr<Promise<std::optional<AuthUser>>> PlatformAuth::silentRestore()
155
215
 
156
216
  {
157
217
  std::lock_guard<std::mutex> lock(gMutex);
218
+ if (gSilentPromise) {
219
+ gSilentPromise->reject(std::make_exception_ptr(std::runtime_error("Silent restore superseded by a newer request")));
220
+ }
158
221
  gSilentPromise = promise;
159
222
  }
160
223
 
@@ -182,8 +245,8 @@ void PlatformAuth::logout() {
182
245
  logoutMethod(JAuthAdapter::javaClassStatic(), static_ref_cast<JContext>(jContext));
183
246
  }
184
247
 
185
- extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeInitialize(JNIEnv* env, jclass, jobject context) {
186
- AuthCache::setAndroidContext(env->NewGlobalRef(context));
248
+ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeInitialize(JNIEnv*, jclass, jobject context) {
249
+ AuthCache::setAndroidContext(context);
187
250
  }
188
251
 
189
252
  extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess(
@@ -253,6 +316,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess
253
316
  const char* s = env->GetStringUTFChars(jstr, nullptr);
254
317
  scopeVec.push_back(std::string(s));
255
318
  env->ReleaseStringUTFChars(jstr, s);
319
+ env->DeleteLocalRef(jstr);
256
320
  }
257
321
  user.scopes = scopeVec;
258
322
  }
@@ -260,6 +324,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess
260
324
  jclass longClass = env->FindClass("java/lang/Long");
261
325
  jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");
262
326
  user.expirationTime = (double)env->CallLongMethod(expirationTime, longValueMethod);
327
+ env->DeleteLocalRef(longClass);
263
328
  }
264
329
 
265
330
  if (loginPromise) loginPromise->resolve(user);
@@ -328,6 +393,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshSucce
328
393
  jclass longClass = env->FindClass("java/lang/Long");
329
394
  jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");
330
395
  tokens.expirationTime = (double)env->CallLongMethod(expirationTime, longValueMethod);
396
+ env->DeleteLocalRef(longClass);
331
397
  }
332
398
  refreshPromise->resolve(tokens);
333
399
  }
@@ -32,9 +32,13 @@ import android.util.Base64
32
32
  object AuthAdapter {
33
33
  private const val TAG = "AuthAdapter"
34
34
 
35
+ @Volatile
36
+ private var isInitialized = false
37
+
35
38
  private var appContext: Context? = null
36
39
  private var currentActivity: Activity? = null
37
40
  private var googleSignInClient: GoogleSignInClient? = null
41
+ private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
38
42
  private var pendingScopes: List<String> = emptyList()
39
43
  private var pendingMicrosoftScopes: List<String> = emptyList()
40
44
 
@@ -74,23 +78,33 @@ object AuthAdapter {
74
78
  @JvmStatic
75
79
  private external fun nativeOnRefreshError(error: String, underlyingError: String?)
76
80
 
81
+ @Synchronized
77
82
  fun initialize(context: Context) {
78
- val app = context.applicationContext as? Application
79
- appContext = app
80
-
81
- app?.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
82
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
83
- override fun onActivityStarted(activity: Activity) { currentActivity = activity }
84
- override fun onActivityResumed(activity: Activity) { currentActivity = activity }
85
- override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
86
- override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
87
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
88
- override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
89
- })
83
+ if (isInitialized) {
84
+ return
85
+ }
86
+
87
+ val applicationContext = context.applicationContext
88
+ appContext = applicationContext
89
+
90
+ val app = applicationContext as? Application
91
+ if (app != null && lifecycleCallbacks == null) {
92
+ lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
93
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
94
+ override fun onActivityStarted(activity: Activity) { currentActivity = activity }
95
+ override fun onActivityResumed(activity: Activity) { currentActivity = activity }
96
+ override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
97
+ override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
98
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
99
+ override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
100
+ }
101
+ app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
102
+ }
90
103
 
91
104
  try {
92
105
  System.loadLibrary("NitroAuth")
93
- nativeInitialize(appContext!!)
106
+ nativeInitialize(applicationContext)
107
+ isInitialized = true
94
108
  } catch (e: Exception) {
95
109
  Log.e(TAG, "Failed to load NitroAuth library", e)
96
110
  }
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
 
@@ -288,6 +288,8 @@ public class AuthAdapter: NSObject {
288
288
  guard parts.count >= 2 else { return [:] }
289
289
 
290
290
  var base64 = parts[1]
291
+ .replacingOccurrences(of: "-", with: "+")
292
+ .replacingOccurrences(of: "_", with: "/")
291
293
  let remainder = base64.count % 4
292
294
  if remainder > 0 {
293
295
  base64 += String(repeating: "=", count: 4 - remainder)
@@ -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...");