react-native-nitro-auth 0.5.8 → 0.5.9

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 (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +64 -50
  3. package/cpp/HybridAuth.cpp +135 -50
  4. package/cpp/HybridAuth.hpp +1 -0
  5. package/lib/commonjs/Auth.web.js +141 -73
  6. package/lib/commonjs/Auth.web.js.map +1 -1
  7. package/lib/commonjs/create-auth-service.js +71 -0
  8. package/lib/commonjs/create-auth-service.js.map +1 -0
  9. package/lib/commonjs/service.js +2 -79
  10. package/lib/commonjs/service.js.map +1 -1
  11. package/lib/commonjs/service.web.js +2 -79
  12. package/lib/commonjs/service.web.js.map +1 -1
  13. package/lib/commonjs/use-auth.js +6 -3
  14. package/lib/commonjs/use-auth.js.map +1 -1
  15. package/lib/module/Auth.web.js +141 -73
  16. package/lib/module/Auth.web.js.map +1 -1
  17. package/lib/module/create-auth-service.js +67 -0
  18. package/lib/module/create-auth-service.js.map +1 -0
  19. package/lib/module/service.js +2 -79
  20. package/lib/module/service.js.map +1 -1
  21. package/lib/module/service.web.js +2 -79
  22. package/lib/module/service.web.js.map +1 -1
  23. package/lib/module/use-auth.js +6 -3
  24. package/lib/module/use-auth.js.map +1 -1
  25. package/lib/typescript/commonjs/Auth.web.d.ts +4 -2
  26. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/create-auth-service.d.ts +5 -0
  28. package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  32. package/lib/typescript/module/Auth.web.d.ts +4 -2
  33. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  34. package/lib/typescript/module/create-auth-service.d.ts +5 -0
  35. package/lib/typescript/module/create-auth-service.d.ts.map +1 -0
  36. package/lib/typescript/module/service.d.ts.map +1 -1
  37. package/lib/typescript/module/service.web.d.ts.map +1 -1
  38. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  39. package/package.json +7 -5
  40. package/react-native-nitro-auth.podspec +1 -0
  41. package/src/Auth.web.ts +261 -102
  42. package/src/create-auth-service.ts +97 -0
  43. package/src/service.ts +3 -101
  44. package/src/service.web.ts +3 -101
  45. package/src/use-auth.ts +7 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 0.5.9 - 2026-04-24
4
+
5
+ ### Added
6
+
7
+ - Added shared JS service factory coverage and logger behavior tests.
8
+ - Added C++ coverage support with `test:cpp:coverage` and expanded native tests for session restore, token refresh, access-token fallback, scope updates, listener isolation, logout cancellation, and serializer behavior.
9
+ - Added example-app smoke checks for the public auth API, provider support, platform-gated behavior, and session-dependent methods.
10
+
11
+ ### Changed
12
+
13
+ - Updated Expo SDK 55 patch dependencies, React Native 0.83.6, Nitro Modules 0.35.5, and related build tooling.
14
+ - Refactored native/web `AuthService` creation so native and web error mapping stay consistent.
15
+ - Hardened web OAuth state, cache parsing, token refresh, and provider error handling.
16
+ - Improved example app handling for unsupported providers and unavailable session actions.
17
+ - Improved release validation to include JS and C++ coverage gates and a faster publish dry run path.
18
+
19
+ ### Fixed
20
+
21
+ - Fixed native runtime crashes in the example app when optional Nitro methods are not available on the installed native object.
22
+ - Fixed startup `silentRestore()` errors in the example app so restore failures surface in status instead of becoming unhandled promises.
23
+ - Excluded C++ test sources from the iOS pod target to avoid app-target duplicate `main` symbols.
24
+
25
+ ### Verified
26
+
27
+ - `bun run check:ci`
28
+ - `bun run --cwd packages/react-native-nitro-auth test:coverage -- --runInBand`
29
+ - `bun run --cwd packages/react-native-nitro-auth test:cpp:coverage`
30
+ - `bun run publish-package:dry-run`
31
+ - `bun run example:prebuild`
32
+ - `bun run example:android`
33
+ - `bun run example:ios`
package/README.md CHANGED
@@ -13,15 +13,16 @@ Fast React Native authentication for Google Sign-In, Apple Sign-In, and Microsof
13
13
  - Typed `useAuth()` hook, `AuthService`, `SocialButton`, and `AuthError`.
14
14
  - App-owned persistence model: tokens stay in memory unless your app stores a snapshot.
15
15
  - Built-in flows for silent restore, token refresh, account picker, login hints, and incremental Google scopes.
16
+ - Consistent `AuthError` mapping for async `AuthService` failures on native and web.
16
17
 
17
18
  ## Choose Your Path
18
19
 
19
- | Need | Use |
20
- | --- | --- |
21
- | Google, Apple, or Microsoft sign-in in an Expo or React Native app | `react-native-nitro-auth` |
22
- | Generic OAuth or OIDC provider not covered by this package | `expo-auth-session` or `react-native-app-auth` |
20
+ | Need | Use |
21
+ | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
22
+ | Google, Apple, or Microsoft sign-in in an Expo or React Native app | `react-native-nitro-auth` |
23
+ | Generic OAuth or OIDC provider not covered by this package | `expo-auth-session` or `react-native-app-auth` |
23
24
  | Firebase user management, password auth, MFA, and hosted auth platform | `@react-native-firebase/auth`, Auth0, Authgear, or your IDaaS SDK |
24
- | Server-side session validation | Your backend; client JWT decode is display-only |
25
+ | Server-side session validation | Your backend; client JWT decode is display-only |
25
26
 
26
27
  ## Install
27
28
 
@@ -43,12 +44,12 @@ cd ios && pod install
43
44
 
44
45
  ## Requirements
45
46
 
46
- | Runtime | Requirement |
47
- | --- | --- |
48
- | React Native | `>=0.75` |
49
- | Nitro Modules | `>=0.35` |
50
- | iOS | 15.1+ recommended |
51
- | Android | min SDK 24+ recommended |
47
+ | Runtime | Requirement |
48
+ | --------------------- | ---------------------------------------- |
49
+ | React Native | `>=0.75` |
50
+ | Nitro Modules | `>=0.35` |
51
+ | iOS | 15.1+ recommended |
52
+ | Android | min SDK 24+ recommended |
52
53
  | Expo example baseline | Expo SDK 55, React Native 0.83, React 19 |
53
54
 
54
55
  ## Expo Setup
@@ -102,19 +103,19 @@ export default {
102
103
 
103
104
  ### Plugin Options
104
105
 
105
- | Option | Platform | Purpose |
106
- | --- | --- | --- |
107
- | `ios.googleClientId` | iOS | Google iOS OAuth client ID |
108
- | `ios.googleServerClientId` | iOS | Google web/server client ID for server auth code flows |
109
- | `ios.googleUrlScheme` | iOS | Reversed iOS client ID URL scheme |
110
- | `ios.appleSignIn` | iOS | Adds Apple Sign-In entitlement when `true` |
111
- | `ios.microsoftClientId` | iOS | Microsoft app/client ID |
112
- | `ios.microsoftTenant` | iOS | Microsoft tenant, `common`, `organizations`, `consumers`, or tenant ID |
113
- | `ios.microsoftB2cDomain` | iOS | Azure AD B2C domain |
114
- | `android.googleClientId` | Android | Google web OAuth client ID |
115
- | `android.microsoftClientId` | Android | Microsoft app/client ID |
116
- | `android.microsoftTenant` | Android | Microsoft tenant |
117
- | `android.microsoftB2cDomain` | Android | Azure AD B2C domain |
106
+ | Option | Platform | Purpose |
107
+ | ---------------------------- | -------- | ---------------------------------------------------------------------- |
108
+ | `ios.googleClientId` | iOS | Google iOS OAuth client ID |
109
+ | `ios.googleServerClientId` | iOS | Google web/server client ID for server auth code flows |
110
+ | `ios.googleUrlScheme` | iOS | Reversed iOS client ID URL scheme |
111
+ | `ios.appleSignIn` | iOS | Adds Apple Sign-In entitlement when `true` |
112
+ | `ios.microsoftClientId` | iOS | Microsoft app/client ID |
113
+ | `ios.microsoftTenant` | iOS | Microsoft tenant, `common`, `organizations`, `consumers`, or tenant ID |
114
+ | `ios.microsoftB2cDomain` | iOS | Azure AD B2C domain |
115
+ | `android.googleClientId` | Android | Google web OAuth client ID |
116
+ | `android.microsoftClientId` | Android | Microsoft app/client ID |
117
+ | `android.microsoftTenant` | Android | Microsoft tenant |
118
+ | `android.microsoftB2cDomain` | Android | Azure AD B2C domain |
118
119
 
119
120
  ## Provider Setup
120
121
 
@@ -239,16 +240,16 @@ await login("microsoft", {
239
240
  });
240
241
  ```
241
242
 
242
- | Option | Applies to | Notes |
243
- | --- | --- | --- |
244
- | `scopes` | Google, Microsoft | Requested OAuth scopes |
245
- | `loginHint` | Google, Microsoft | Prefills account selection when supported |
246
- | `useOneTap` | Android Google | Enables Credential Manager auto-select |
247
- | `useSheet` | iOS Google | Uses native sign-in sheet behavior |
248
- | `forceAccountPicker` | Google | Forces account picker |
249
- | `useLegacyGoogleSignIn` | Android Google | Uses legacy Google Sign-In path for server auth code |
250
- | `tenant` | Microsoft | Overrides configured tenant |
251
- | `prompt` | Microsoft | `login`, `consent`, `select_account`, or `none` |
243
+ | Option | Applies to | Notes |
244
+ | ----------------------- | ----------------- | ---------------------------------------------------- |
245
+ | `scopes` | Google, Microsoft | Requested OAuth scopes |
246
+ | `loginHint` | Google, Microsoft | Prefills account selection when supported |
247
+ | `useOneTap` | Android Google | Enables Credential Manager auto-select |
248
+ | `useSheet` | iOS Google | Uses native sign-in sheet behavior |
249
+ | `forceAccountPicker` | Google | Forces account picker |
250
+ | `useLegacyGoogleSignIn` | Android Google | Uses legacy Google Sign-In path for server auth code |
251
+ | `tenant` | Microsoft | Overrides configured tenant |
252
+ | `prompt` | Microsoft | `login`, `consent`, `select_account`, or `none` |
252
253
 
253
254
  ## Incremental Scopes
254
255
 
@@ -288,7 +289,7 @@ extra: {
288
289
 
289
290
  ## Error Contract
290
291
 
291
- All public async APIs throw `AuthError`.
292
+ All public async APIs throw `AuthError`, including provider errors surfaced by native and web `AuthService` implementations.
292
293
 
293
294
  ```ts
294
295
  try {
@@ -411,16 +412,16 @@ The demo includes:
411
412
 
412
413
  ## Troubleshooting
413
414
 
414
- | Symptom | Check |
415
- | --- | --- |
416
- | `configuration_error` on Google | Client ID is missing or wrong for the current platform |
417
- | Google works in debug but not release | Add release SHA-1/SHA-256 fingerprints to Google Cloud Console |
418
- | Android `hasPlayServices` is false | Use an emulator image with Google Play Services |
419
- | Apple email/name missing | Apple only returns these fields on first authorization |
420
- | Microsoft `invalid_state` | Redirect URI or app resume path is wrong, or an old auth redirect completed late |
421
- | Microsoft `token_error` | Check tenant, client ID, redirect URI, and requested scopes |
422
- | Web popup blocked | Call `login()` from a user gesture such as a button press |
423
- | `operation_in_progress` | A provider flow is already active; wait for it to finish or sign out |
415
+ | Symptom | Check |
416
+ | ------------------------------------- | -------------------------------------------------------------------------------- |
417
+ | `configuration_error` on Google | Client ID is missing or wrong for the current platform |
418
+ | Google works in debug but not release | Add release SHA-1/SHA-256 fingerprints to Google Cloud Console |
419
+ | Android `hasPlayServices` is false | Use an emulator image with Google Play Services |
420
+ | Apple email/name missing | Apple only returns these fields on first authorization |
421
+ | Microsoft `invalid_state` | Redirect URI or app resume path is wrong, or an old auth redirect completed late |
422
+ | Microsoft `token_error` | Check tenant, client ID, redirect URI, and requested scopes |
423
+ | Web popup blocked | Call `login()` from a user gesture such as a button press |
424
+ | `operation_in_progress` | A provider flow is already active; wait for it to finish or sign out |
424
425
 
425
426
  ## Production Notes
426
427
 
@@ -433,13 +434,26 @@ The demo includes:
433
434
  ## Release Checks
434
435
 
435
436
  ```sh
436
- bun run codegen
437
- bun run build
437
+ bun run publish-package:dry-run
438
+ ```
439
+
440
+ The publish script runs frozen install, core-version verification, codegen, build, lint, typecheck, Jest, JS coverage, C++ tests, C++ coverage, Expo Doctor, package docs sync, pack dry run, and `bun publish --dry-run --ignore-scripts`.
441
+
442
+ For faster local iteration before the full release dry run:
443
+
444
+ ```sh
438
445
  bun run check
439
446
  bun run test:cpp
440
- bun example:prebuild:clean
441
- bun example:ios
442
- bun example:android
447
+ bun run --cwd packages/react-native-nitro-auth test:coverage -- --runInBand
448
+ bun run --cwd packages/react-native-nitro-auth test:cpp:coverage
449
+ ```
450
+
451
+ Before shipping provider or native config changes, also verify the example app:
452
+
453
+ ```sh
454
+ bun run example:prebuild
455
+ bun run example:android
456
+ bun run example:ios
443
457
  ```
444
458
 
445
459
  ## License
@@ -2,10 +2,63 @@
2
2
  #include "PlatformAuth.hpp"
3
3
  #include <algorithm>
4
4
  #include <chrono>
5
+ #include <exception>
5
6
  #include <stdexcept>
7
+ #include <unordered_set>
6
8
 
7
9
  namespace margelo::nitro::NitroAuth {
8
10
 
11
+ namespace {
12
+
13
+ std::exception_ptr makeAuthError(const char* message) {
14
+ return std::make_exception_ptr(std::runtime_error(message));
15
+ }
16
+
17
+ void rejectIfPending(const std::shared_ptr<Promise<AuthTokens>>& promise, const char* message) {
18
+ if (promise && promise->isPending()) {
19
+ promise->reject(makeAuthError(message));
20
+ }
21
+ }
22
+
23
+ void mergeGrantedScopes(std::vector<std::string>& grantedScopes, const std::vector<std::string>& scopes) {
24
+ std::unordered_set<std::string> knownScopes(grantedScopes.begin(), grantedScopes.end());
25
+ grantedScopes.reserve(grantedScopes.size() + scopes.size());
26
+
27
+ for (const auto& scope : scopes) {
28
+ if (knownScopes.insert(scope).second) {
29
+ grantedScopes.push_back(scope);
30
+ }
31
+ }
32
+ }
33
+
34
+ void removeGrantedScopes(std::vector<std::string>& grantedScopes, const std::vector<std::string>& scopes) {
35
+ if (scopes.empty() || grantedScopes.empty()) {
36
+ return;
37
+ }
38
+
39
+ const std::unordered_set<std::string> scopesToRemove(scopes.begin(), scopes.end());
40
+ grantedScopes.erase(
41
+ std::remove_if(grantedScopes.begin(), grantedScopes.end(),
42
+ [&scopesToRemove](const std::string& scope) {
43
+ return scopesToRemove.find(scope) != scopesToRemove.end();
44
+ }),
45
+ grantedScopes.end()
46
+ );
47
+ }
48
+
49
+ template <typename TCallback, typename TValue>
50
+ void invokeListenersSafely(const std::vector<TCallback>& listeners, const TValue& value) {
51
+ for (const auto& listener : listeners) {
52
+ try {
53
+ listener(value);
54
+ } catch (...) {
55
+ // Callback failures are isolated so one listener cannot block core state updates.
56
+ }
57
+ }
58
+ }
59
+
60
+ } // namespace
61
+
9
62
  HybridAuth::HybridAuth() : HybridObject(TAG) {
10
63
  // In-memory only - no internal persistence.
11
64
  }
@@ -30,13 +83,12 @@ void HybridAuth::notifyAuthStateChanged() {
30
83
  {
31
84
  std::lock_guard<std::recursive_mutex> lock(_mutex);
32
85
  user = _currentUser;
86
+ listeners.reserve(_listeners.size());
33
87
  for (auto const& [id, listener] : _listeners) {
34
88
  listeners.push_back(listener);
35
89
  }
36
90
  }
37
- for (const auto& listener : listeners) {
38
- listener(user);
39
- }
91
+ invokeListenersSafely(listeners, user);
40
92
  }
41
93
 
42
94
  std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(const std::optional<AuthUser>&)>& callback) {
@@ -49,6 +101,7 @@ std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(co
49
101
  auto self = weak.lock();
50
102
  if (!self) return;
51
103
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
104
+ if (!auth) return;
52
105
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
53
106
  auth->_listeners.erase(id);
54
107
  };
@@ -64,26 +117,28 @@ std::function<void()> HybridAuth::onTokensRefreshed(const std::function<void(con
64
117
  auto self = weak.lock();
65
118
  if (!self) return;
66
119
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
120
+ if (!auth) return;
67
121
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
68
122
  auth->_tokenListeners.erase(id);
69
123
  };
70
124
  }
71
125
 
126
+ std::shared_ptr<Promise<AuthTokens>> HybridAuth::advanceSessionGenerationLocked() {
127
+ _sessionGeneration++;
128
+ auto refreshInFlight = _refreshInFlight;
129
+ _refreshInFlight = nullptr;
130
+ return refreshInFlight;
131
+ }
132
+
72
133
  void HybridAuth::logout() {
73
134
  std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
74
135
  {
75
136
  std::lock_guard<std::recursive_mutex> lock(_mutex);
76
- _sessionGeneration++;
137
+ refreshInFlight = advanceSessionGenerationLocked();
77
138
  _currentUser = std::nullopt;
78
139
  _grantedScopes.clear();
79
- refreshInFlight = _refreshInFlight;
80
- _refreshInFlight = nullptr;
81
- }
82
- if (refreshInFlight) {
83
- refreshInFlight->reject(
84
- std::make_exception_ptr(std::runtime_error("not_signed_in"))
85
- );
86
140
  }
141
+ rejectIfPending(refreshInFlight, "not_signed_in");
87
142
  PlatformAuth::logout();
88
143
  notifyAuthStateChanged();
89
144
  }
@@ -99,12 +154,18 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
99
154
  auto self = shared_from_this();
100
155
  silentPromise->addOnResolvedListener([self, promise, generation](const std::optional<AuthUser>& user) {
101
156
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
157
+ if (!auth) {
158
+ promise->reject(makeAuthError("internal_error"));
159
+ return;
160
+ }
161
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
102
162
  {
103
163
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
104
164
  if (auth->_sessionGeneration != generation) {
105
165
  promise->resolve();
106
166
  return;
107
167
  }
168
+ refreshInFlight = auth->advanceSessionGenerationLocked();
108
169
  auth->_currentUser = user;
109
170
  if (user) {
110
171
  if (user->scopes) {
@@ -116,6 +177,7 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
116
177
  auth->_grantedScopes.clear();
117
178
  }
118
179
  }
180
+ rejectIfPending(refreshInFlight, "cancelled");
119
181
  // Always resolve - no session is not an error, just means user is logged out
120
182
  auth->notifyAuthStateChanged();
121
183
  promise->resolve();
@@ -131,23 +193,30 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
131
193
  std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
132
194
  auto promise = Promise<void>::create();
133
195
  uint64_t generation;
196
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
134
197
  {
135
198
  std::lock_guard<std::recursive_mutex> lock(_mutex);
199
+ refreshInFlight = advanceSessionGenerationLocked();
136
200
  generation = _sessionGeneration;
137
201
  }
202
+ rejectIfPending(refreshInFlight, "cancelled");
138
203
 
139
204
  auto self = shared_from_this();
140
205
  auto loginPromise = PlatformAuth::login(provider, options);
141
206
  loginPromise->addOnResolvedListener([self, promise, options, generation](const AuthUser& user) {
142
207
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
208
+ if (!auth) {
209
+ promise->reject(makeAuthError("internal_error"));
210
+ return;
211
+ }
212
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
143
213
  {
144
214
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
145
215
  if (auth->_sessionGeneration != generation) {
146
- promise->reject(
147
- std::make_exception_ptr(std::runtime_error("cancelled"))
148
- );
216
+ promise->reject(makeAuthError("cancelled"));
149
217
  return;
150
218
  }
219
+ refreshInFlight = auth->advanceSessionGenerationLocked();
151
220
  auth->_currentUser = user;
152
221
  if (user.scopes && !user.scopes->empty()) {
153
222
  auth->_grantedScopes = *user.scopes;
@@ -162,6 +231,7 @@ std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const st
162
231
  : std::make_optional(auth->_grantedScopes);
163
232
  }
164
233
  }
234
+ rejectIfPending(refreshInFlight, "cancelled");
165
235
  auth->notifyAuthStateChanged();
166
236
  promise->resolve();
167
237
  });
@@ -183,20 +253,18 @@ std::shared_ptr<Promise<void>> HybridAuth::requestScopes(const std::vector<std::
183
253
  auto requestPromise = PlatformAuth::requestScopes(scopes);
184
254
  requestPromise->addOnResolvedListener([self, promise, scopes, generation](const AuthUser& user) {
185
255
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
256
+ if (!auth) {
257
+ promise->reject(makeAuthError("internal_error"));
258
+ return;
259
+ }
186
260
  {
187
261
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
188
262
  if (auth->_sessionGeneration != generation) {
189
- promise->reject(
190
- std::make_exception_ptr(std::runtime_error("cancelled"))
191
- );
263
+ promise->reject(makeAuthError("cancelled"));
192
264
  return;
193
265
  }
194
266
  auth->_currentUser = user;
195
- for (const auto& scope : scopes) {
196
- if (std::find(auth->_grantedScopes.begin(), auth->_grantedScopes.end(), scope) == auth->_grantedScopes.end()) {
197
- auth->_grantedScopes.push_back(scope);
198
- }
199
- }
267
+ mergeGrantedScopes(auth->_grantedScopes, scopes);
200
268
  if (auth->_currentUser) auth->_currentUser->scopes = auth->_grantedScopes;
201
269
  }
202
270
  auth->notifyAuthStateChanged();
@@ -213,13 +281,7 @@ std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::s
213
281
  auto promise = Promise<void>::create();
214
282
  {
215
283
  std::lock_guard<std::recursive_mutex> lock(_mutex);
216
- _grantedScopes.erase(
217
- std::remove_if(_grantedScopes.begin(), _grantedScopes.end(),
218
- [&scopes](const std::string& s) {
219
- return std::find(scopes.begin(), scopes.end(), s) != scopes.end();
220
- }),
221
- _grantedScopes.end()
222
- );
284
+ removeGrantedScopes(_grantedScopes, scopes);
223
285
  if (_currentUser) {
224
286
  _currentUser->scopes = _grantedScopes;
225
287
  }
@@ -280,28 +342,41 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
280
342
  auto refreshPromise = PlatformAuth::refreshToken();
281
343
  refreshPromise->addOnResolvedListener([self, promise, generation](const AuthTokens& tokens) {
282
344
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
345
+ if (!auth) {
346
+ promise->reject(makeAuthError("internal_error"));
347
+ return;
348
+ }
349
+ bool isStale = false;
283
350
  {
284
351
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
285
352
  if (auth->_sessionGeneration != generation) {
286
- return;
287
- }
288
- if (auth->_currentUser) {
289
- if (tokens.accessToken.has_value()) {
290
- auth->_currentUser->accessToken = tokens.accessToken;
353
+ if (auth->_refreshInFlight == promise) {
354
+ auth->_refreshInFlight = nullptr;
291
355
  }
292
- if (tokens.idToken.has_value()) {
293
- auth->_currentUser->idToken = tokens.idToken;
294
- }
295
- if (tokens.refreshToken.has_value()) {
296
- auth->_currentUser->refreshToken = tokens.refreshToken;
356
+ isStale = true;
357
+ } else {
358
+ if (auth->_currentUser) {
359
+ if (tokens.accessToken.has_value()) {
360
+ auth->_currentUser->accessToken = tokens.accessToken;
361
+ }
362
+ if (tokens.idToken.has_value()) {
363
+ auth->_currentUser->idToken = tokens.idToken;
364
+ }
365
+ if (tokens.refreshToken.has_value()) {
366
+ auth->_currentUser->refreshToken = tokens.refreshToken;
367
+ }
368
+ if (tokens.expirationTime.has_value()) {
369
+ auth->_currentUser->expirationTime = tokens.expirationTime;
370
+ }
297
371
  }
298
- if (tokens.expirationTime.has_value()) {
299
- auth->_currentUser->expirationTime = tokens.expirationTime;
372
+ if (auth->_refreshInFlight == promise) {
373
+ auth->_refreshInFlight = nullptr;
300
374
  }
301
375
  }
302
- if (auth->_refreshInFlight == promise) {
303
- auth->_refreshInFlight = nullptr;
304
- }
376
+ }
377
+ if (isStale) {
378
+ rejectIfPending(promise, "cancelled");
379
+ return;
305
380
  }
306
381
  auth->notifyTokensRefreshed(tokens);
307
382
  auth->notifyAuthStateChanged();
@@ -310,15 +385,26 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
310
385
 
311
386
  refreshPromise->addOnRejectedListener([self, promise, generation](const std::exception_ptr& error) {
312
387
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
388
+ if (!auth) {
389
+ promise->reject(makeAuthError("internal_error"));
390
+ return;
391
+ }
392
+ bool isStale = false;
313
393
  {
314
394
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
315
395
  if (auth->_sessionGeneration != generation) {
316
- return;
317
- }
318
- if (auth->_refreshInFlight == promise) {
396
+ if (auth->_refreshInFlight == promise) {
397
+ auth->_refreshInFlight = nullptr;
398
+ }
399
+ isStale = true;
400
+ } else if (auth->_refreshInFlight == promise) {
319
401
  auth->_refreshInFlight = nullptr;
320
402
  }
321
403
  }
404
+ if (isStale) {
405
+ rejectIfPending(promise, "cancelled");
406
+ return;
407
+ }
322
408
  promise->reject(error);
323
409
  });
324
410
  return promise;
@@ -332,13 +418,12 @@ void HybridAuth::notifyTokensRefreshed(const AuthTokens& tokens) {
332
418
  std::vector<std::function<void(const AuthTokens&)>> listeners;
333
419
  {
334
420
  std::lock_guard<std::recursive_mutex> lock(_mutex);
421
+ listeners.reserve(_tokenListeners.size());
335
422
  for (auto const& [id, listener] : _tokenListeners) {
336
423
  listeners.push_back(listener);
337
424
  }
338
425
  }
339
- for (const auto& listener : listeners) {
340
- listener(tokens);
341
- }
426
+ invokeListenersSafely(listeners, tokens);
342
427
  }
343
428
 
344
429
  } // namespace margelo::nitro::NitroAuth
@@ -38,6 +38,7 @@ public:
38
38
  private:
39
39
  void notifyAuthStateChanged();
40
40
  void notifyTokensRefreshed(const AuthTokens& tokens);
41
+ std::shared_ptr<Promise<AuthTokens>> advanceSessionGenerationLocked();
41
42
 
42
43
  private:
43
44
  std::optional<AuthUser> _currentUser;