react-native-nitro-auth 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +28 -11
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/cpp/PlatformAuth+Android.cpp +22 -7
  4. package/android/src/main/java/com/auth/AuthAdapter.kt +15 -15
  5. package/app.plugin.js +7 -5
  6. package/ios/AuthAdapter.swift +37 -48
  7. package/ios/PlatformAuth+iOS.mm +32 -24
  8. package/lib/commonjs/Auth.web.js +14 -21
  9. package/lib/commonjs/Auth.web.js.map +1 -1
  10. package/lib/commonjs/package.json +1 -0
  11. package/lib/commonjs/use-auth.js +5 -3
  12. package/lib/commonjs/use-auth.js.map +1 -1
  13. package/lib/module/Auth.web.js +14 -21
  14. package/lib/module/Auth.web.js.map +1 -1
  15. package/lib/module/package.json +1 -0
  16. package/lib/module/use-auth.js +5 -3
  17. package/lib/module/use-auth.js.map +1 -1
  18. package/lib/typescript/{Auth.nitro.d.ts → commonjs/Auth.nitro.d.ts} +4 -0
  19. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -0
  20. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -0
  21. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +1 -0
  22. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/index.web.d.ts.map +1 -0
  24. package/lib/typescript/commonjs/package.json +1 -0
  25. package/lib/typescript/commonjs/service.d.ts.map +1 -0
  26. package/lib/typescript/commonjs/service.web.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -0
  28. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -0
  30. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -0
  31. package/lib/typescript/module/Auth.nitro.d.ts +48 -0
  32. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -0
  33. package/lib/typescript/module/Auth.web.d.ts +40 -0
  34. package/lib/typescript/module/Auth.web.d.ts.map +1 -0
  35. package/lib/typescript/module/AuthStorage.nitro.d.ts +19 -0
  36. package/lib/typescript/module/AuthStorage.nitro.d.ts.map +1 -0
  37. package/lib/typescript/module/index.d.ts +6 -0
  38. package/lib/typescript/module/index.d.ts.map +1 -0
  39. package/lib/typescript/module/index.web.d.ts +6 -0
  40. package/lib/typescript/module/index.web.d.ts.map +1 -0
  41. package/lib/typescript/module/package.json +1 -0
  42. package/lib/typescript/module/service.d.ts +3 -0
  43. package/lib/typescript/module/service.d.ts.map +1 -0
  44. package/lib/typescript/module/service.web.d.ts +2 -0
  45. package/lib/typescript/module/service.web.d.ts.map +1 -0
  46. package/lib/typescript/module/ui/social-button.d.ts +17 -0
  47. package/lib/typescript/module/ui/social-button.d.ts.map +1 -0
  48. package/lib/typescript/module/ui/social-button.web.d.ts +17 -0
  49. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -0
  50. package/lib/typescript/module/use-auth.d.ts +15 -0
  51. package/lib/typescript/module/use-auth.d.ts.map +1 -0
  52. package/lib/typescript/module/utils/logger.d.ts +8 -0
  53. package/lib/typescript/module/utils/logger.d.ts.map +1 -0
  54. package/nitrogen/generated/android/NitroAuth+autolinking.cmake +1 -1
  55. package/nitrogen/generated/android/NitroAuth+autolinking.gradle +1 -1
  56. package/nitrogen/generated/android/NitroAuthOnLoad.cpp +1 -1
  57. package/nitrogen/generated/android/NitroAuthOnLoad.hpp +1 -1
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/auth/NitroAuthOnLoad.kt +1 -1
  59. package/nitrogen/generated/ios/NitroAuth+autolinking.rb +2 -2
  60. package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.cpp +1 -1
  61. package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.hpp +1 -1
  62. package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Umbrella.hpp +1 -1
  63. package/nitrogen/generated/ios/NitroAuthAutolinking.mm +1 -1
  64. package/nitrogen/generated/ios/NitroAuthAutolinking.swift +5 -1
  65. package/nitrogen/generated/shared/c++/AuthProvider.hpp +1 -1
  66. package/nitrogen/generated/shared/c++/AuthTokens.hpp +19 -11
  67. package/nitrogen/generated/shared/c++/AuthUser.hpp +42 -30
  68. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +1 -1
  69. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +1 -1
  70. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +1 -1
  71. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +1 -1
  72. package/nitrogen/generated/shared/c++/LoginOptions.hpp +24 -12
  73. package/package.json +5 -4
  74. package/react-native-nitro-auth.podspec +1 -1
  75. package/src/Auth.nitro.ts +4 -0
  76. package/src/Auth.web.ts +17 -20
  77. package/src/use-auth.ts +5 -3
  78. package/lib/typescript/Auth.nitro.d.ts.map +0 -1
  79. package/lib/typescript/Auth.web.d.ts.map +0 -1
  80. package/lib/typescript/AuthStorage.nitro.d.ts.map +0 -1
  81. package/lib/typescript/index.d.ts.map +0 -1
  82. package/lib/typescript/index.web.d.ts.map +0 -1
  83. package/lib/typescript/service.d.ts.map +0 -1
  84. package/lib/typescript/service.web.d.ts.map +0 -1
  85. package/lib/typescript/ui/social-button.d.ts.map +0 -1
  86. package/lib/typescript/ui/social-button.web.d.ts.map +0 -1
  87. package/lib/typescript/use-auth.d.ts.map +0 -1
  88. package/lib/typescript/utils/logger.d.ts.map +0 -1
  89. /package/lib/typescript/{Auth.web.d.ts → commonjs/Auth.web.d.ts} +0 -0
  90. /package/lib/typescript/{AuthStorage.nitro.d.ts → commonjs/AuthStorage.nitro.d.ts} +0 -0
  91. /package/lib/typescript/{index.d.ts → commonjs/index.d.ts} +0 -0
  92. /package/lib/typescript/{index.web.d.ts → commonjs/index.web.d.ts} +0 -0
  93. /package/lib/typescript/{service.d.ts → commonjs/service.d.ts} +0 -0
  94. /package/lib/typescript/{service.web.d.ts → commonjs/service.web.d.ts} +0 -0
  95. /package/lib/typescript/{ui → commonjs/ui}/social-button.d.ts +0 -0
  96. /package/lib/typescript/{ui → commonjs/ui}/social-button.web.d.ts +0 -0
  97. /package/lib/typescript/{use-auth.d.ts → commonjs/use-auth.d.ts} +0 -0
  98. /package/lib/typescript/{utils → commonjs/utils}/logger.d.ts +0 -0
package/README.md CHANGED
@@ -27,7 +27,8 @@ Nitro Auth is designed to replace legacy modules like `@react-native-google-sign
27
27
  - **Expo Ready**: Comes with a powerful Config Plugin for zero-config setup.
28
28
  - **Cross-Platform**: Unified API for iOS, Android, and Web.
29
29
  - **Auto-Refresh**: Synchronous access to tokens with automatic silent refresh.
30
- - **Google One-Tap**: Modern login experience on Android using Credential Manager.
30
+ - **Google One-Tap / Sheet**: Modern login experience on Android (Credential Manager) and iOS (Sign-In Sheet).
31
+ - **Error Metadata**: Detailed native error messages for easier debugging.
31
32
  - **Custom Storage**: Pluggable storage adapters for secure persistence (e.g., Keychain).
32
33
  - **Refresh Interceptors**: Listen to token updates globally.
33
34
 
@@ -52,7 +53,8 @@ Add the plugin to `app.json`:
52
53
  "ios": {
53
54
  "googleClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
54
55
  "googleServerClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
55
- "googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID"
56
+ "googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID",
57
+ "appleSignIn": true
56
58
  },
57
59
  "android": {
58
60
  "googleClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com"
@@ -64,7 +66,8 @@ Add the plugin to `app.json`:
64
66
  }
65
67
  ```
66
68
 
67
- > [!NOTE] > `googleServerClientId` is only required if you need a `serverAuthCode` for backend integration.
69
+ > [!NOTE] > `appleSignIn` on iOS is `false` by default to avoid unnecessary entitlements. Set it to `true` to enable Apple Sign-In.
70
+ > `googleServerClientId` is only required if you need a `serverAuthCode` for backend integration.
68
71
 
69
72
  ### Bare React Native
70
73
 
@@ -144,13 +147,24 @@ try {
144
147
  }
145
148
  ```
146
149
 
147
- | Code | Description |
148
- | ---------------------- | ---------------------------------------------- |
149
- | `cancelled` | The user cancelled the sign-in flow |
150
- | `network_error` | A network error occurred |
151
- | `configuration_error` | Missing client IDs or invalid setup |
150
+ | `cancelled` | The user cancelled the sign-in flow |
151
+ | `network_error` | A network error occurred |
152
+ | `configuration_error` | Missing client IDs or invalid setup |
152
153
  | `unsupported_provider` | The provider is not supported on this platform |
153
154
 
155
+ ### Native Error Metadata
156
+
157
+ For more detailed debugging, Nitro Auth captures the raw native error message in the `underlyingError` property of the `AuthUser` (on success) or surfaces it in the caught `Error` object:
158
+
159
+ ```ts
160
+ try {
161
+ await login("google");
162
+ } catch (e) {
163
+ // Access raw native error message
164
+ console.log(e.underlyingError);
165
+ }
166
+ ```
167
+
154
168
  ### Automatic Token Refresh
155
169
 
156
170
  The `getAccessToken()` method automatically checks if the current token is expired (or about to expire) and triggers a silent refresh if possible:
@@ -224,12 +238,15 @@ AuthService.onTokensRefreshed((tokens) => {
224
238
  });
225
239
  ```
226
240
 
227
- ### Google One-Tap (Android)
241
+ ### Google One-Tap & Sheet
228
242
 
229
- Explicitly enable the modern One-Tap flow on Android:
243
+ Explicitly enable the modern One-Tap flow on Android or the Sign-In Sheet on iOS:
230
244
 
231
245
  ```ts
232
- await login("google", { useOneTap: true });
246
+ await login("google", {
247
+ useOneTap: true, // Android
248
+ useSheet: true, // iOS
249
+ });
233
250
  ```
234
251
 
235
252
  ## API Reference
@@ -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.0"
97
97
 
98
98
  // Activity result APIs
99
99
  implementation "androidx.activity:activity-ktx:1.9.3"
@@ -225,13 +225,14 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess
225
225
  jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");
226
226
  user.expirationTime = (double)env->CallLongMethod(expirationTime, longValueMethod);
227
227
  }
228
+
228
229
  if (loginPromise) loginPromise->resolve(user);
229
230
  if (scopesPromise) scopesPromise->resolve(user);
230
231
  if (silentPromise) silentPromise->resolve(user);
231
232
  }
232
233
 
233
234
  extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginError(
234
- JNIEnv* env, jclass, jstring error) {
235
+ JNIEnv* env, jclass, jstring error, jstring underlyingError) {
235
236
 
236
237
  std::shared_ptr<Promise<AuthUser>> loginPromise;
237
238
  std::shared_ptr<Promise<AuthUser>> scopesPromise;
@@ -249,12 +250,19 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginError(
249
250
  const char* errorCStr = env->GetStringUTFChars(error, nullptr);
250
251
  std::string errorStr(errorCStr);
251
252
  env->ReleaseStringUTFChars(error, errorCStr);
253
+
254
+ std::string finalError = errorStr;
255
+ if (underlyingError) {
256
+ const char* uCStr = env->GetStringUTFChars(underlyingError, nullptr);
257
+ finalError = std::string(uCStr);
258
+ env->ReleaseStringUTFChars(underlyingError, uCStr);
259
+ }
252
260
 
253
- if (loginPromise) loginPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
254
- if (scopesPromise) scopesPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
261
+ if (loginPromise) loginPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
262
+ if (scopesPromise) scopesPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
255
263
  if (silentPromise) {
256
264
  if (errorStr == "No session") silentPromise->resolve(std::nullopt);
257
- else silentPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
265
+ else silentPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
258
266
  }
259
267
  }
260
268
 
@@ -290,7 +298,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshSucce
290
298
  }
291
299
 
292
300
  extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshError(
293
- JNIEnv* env, jclass, jstring error) {
301
+ JNIEnv* env, jclass, jstring error, jstring underlyingError) {
294
302
 
295
303
  std::shared_ptr<Promise<AuthTokens>> refreshPromise;
296
304
  {
@@ -299,10 +307,17 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshError
299
307
  gRefreshPromise = nullptr;
300
308
  }
301
309
  if (refreshPromise) {
310
+ std::string finalError;
302
311
  const char* errorCStr = env->GetStringUTFChars(error, nullptr);
303
- std::string errorStr(errorCStr);
312
+ finalError = std::string(errorCStr);
304
313
  env->ReleaseStringUTFChars(error, errorCStr);
305
- refreshPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
314
+
315
+ if (underlyingError) {
316
+ const char* uCStr = env->GetStringUTFChars(underlyingError, nullptr);
317
+ finalError = std::string(uCStr);
318
+ env->ReleaseStringUTFChars(underlyingError, uCStr);
319
+ }
320
+ refreshPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
306
321
  }
307
322
  }
308
323
 
@@ -47,13 +47,13 @@ object AuthAdapter {
47
47
  )
48
48
 
49
49
  @JvmStatic
50
- private external fun nativeOnLoginError(error: String)
50
+ private external fun nativeOnLoginError(error: String, underlyingError: String?)
51
51
 
52
52
  @JvmStatic
53
53
  private external fun nativeOnRefreshSuccess(idToken: String?, accessToken: String?, expirationTime: Long?)
54
54
 
55
55
  @JvmStatic
56
- private external fun nativeOnRefreshError(error: String)
56
+ private external fun nativeOnRefreshError(error: String, underlyingError: String?)
57
57
 
58
58
  fun initialize(context: Context) {
59
59
  val app = context.applicationContext as? Application
@@ -90,27 +90,27 @@ object AuthAdapter {
90
90
  12501 -> "cancelled"
91
91
  7 -> "network_error"
92
92
  8, 10 -> "configuration_error"
93
- else -> message ?: "unknown"
93
+ else -> "unknown"
94
94
  }
95
- nativeOnLoginError(mappedError)
95
+ nativeOnLoginError(mappedError, message)
96
96
  }
97
97
 
98
98
  @JvmStatic
99
99
  fun loginSync(context: Context, provider: String, googleClientId: String?, scopes: Array<String>?, loginHint: String?, useOneTap: Boolean) {
100
100
  if (provider == "apple") {
101
- nativeOnLoginError("Apple Sign-In is not supported on Android.")
101
+ nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
102
102
  return
103
103
  }
104
104
 
105
105
  if (provider != "google") {
106
- nativeOnLoginError("Unsupported provider: $provider")
106
+ nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
107
107
  return
108
108
  }
109
109
 
110
110
  val ctx = appContext ?: context.applicationContext
111
111
  val clientId = googleClientId ?: getClientIdFromResources(ctx)
112
112
  if (clientId == null) {
113
- nativeOnLoginError("Google Client ID is required. Set it in app.json plugins.")
113
+ nativeOnLoginError("configuration_error", "Google Client ID is required. Set it in app.json plugins.")
114
114
  return
115
115
  }
116
116
 
@@ -137,7 +137,7 @@ object AuthAdapter {
137
137
  val googleIdOption = GetGoogleIdOption.Builder()
138
138
  .setFilterByAuthorizedAccounts(false)
139
139
  .setServerClientId(clientId)
140
- .setAutoSelectEnabled(false) // Disable auto-select for testing so the sheet always shows
140
+ .setAutoSelectEnabled(false)
141
141
  .build()
142
142
 
143
143
  val request = GetCredentialRequest.Builder()
@@ -191,7 +191,7 @@ object AuthAdapter {
191
191
  )
192
192
  } else {
193
193
  Log.w(TAG, "Unsupported credential type: ${credential.type}")
194
- nativeOnLoginError("Unsupported credential type: ${credential.type}")
194
+ nativeOnLoginError("unknown", "Unsupported credential type: ${credential.type}")
195
195
  }
196
196
  }
197
197
 
@@ -200,7 +200,7 @@ object AuthAdapter {
200
200
  val ctx = appContext ?: context.applicationContext
201
201
  val account = GoogleSignIn.getLastSignedInAccount(ctx)
202
202
  if (account == null) {
203
- nativeOnLoginError("No user logged in")
203
+ nativeOnLoginError("unknown", "No user logged in")
204
204
  return
205
205
  }
206
206
 
@@ -212,7 +212,7 @@ object AuthAdapter {
212
212
 
213
213
  val clientId = getClientIdFromResources(ctx)
214
214
  if (clientId == null) {
215
- nativeOnLoginError("Google Client ID not configured")
215
+ nativeOnLoginError("configuration_error", "Google Client ID not configured")
216
216
  return
217
217
  }
218
218
 
@@ -227,12 +227,12 @@ object AuthAdapter {
227
227
  if (googleSignInClient == null) {
228
228
  val account = GoogleSignIn.getLastSignedInAccount(ctx)
229
229
  if (account == null) {
230
- nativeOnRefreshError("No user logged in")
230
+ nativeOnRefreshError("unknown", "No user logged in")
231
231
  return
232
232
  }
233
233
  val clientId = getClientIdFromResources(ctx)
234
234
  if (clientId == null) {
235
- nativeOnRefreshError("Google Client ID not configured")
235
+ nativeOnRefreshError("configuration_error", "Google Client ID not configured")
236
236
  return
237
237
  }
238
238
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
@@ -248,7 +248,7 @@ object AuthAdapter {
248
248
  val account = task.result
249
249
  nativeOnRefreshSuccess(account?.idToken, null, null)
250
250
  } else {
251
- nativeOnRefreshError(task.exception?.message ?: "Silent sign-in failed")
251
+ nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
252
252
  }
253
253
  }
254
254
  }
@@ -333,7 +333,7 @@ object AuthAdapter {
333
333
  val serverAuthCode = extractJsonValue(json, "serverAuthCode")
334
334
  nativeOnLoginSuccess(provider, email, name, photo, idToken, null, serverAuthCode, null, null)
335
335
  } else {
336
- nativeOnLoginError("No session")
336
+ nativeOnLoginError("unknown", "No session")
337
337
  }
338
338
  }
339
339
  }
package/app.plugin.js CHANGED
@@ -36,10 +36,12 @@ const withNitroAuth = (config, props = {}) => {
36
36
  });
37
37
 
38
38
  // 2. iOS Entitlements
39
- config = withEntitlementsPlist(config, (config) => {
40
- config.modResults["com.apple.developer.applesignin"] = ["Default"];
41
- return config;
42
- });
39
+ if (ios.appleSignIn === true) {
40
+ config = withEntitlementsPlist(config, (config) => {
41
+ config.modResults["com.apple.developer.applesignin"] = ["Default"];
42
+ return config;
43
+ });
44
+ }
43
45
 
44
46
  // 3. Android Strings (for Google Client ID)
45
47
  config = withStringsXml(config, (config) => {
@@ -63,5 +65,5 @@ const withNitroAuth = (config, props = {}) => {
63
65
  module.exports = createRunOncePlugin(
64
66
  withNitroAuth,
65
67
  "react-native-nitro-auth",
66
- "0.1.3"
68
+ "0.1.6"
67
69
  );
@@ -7,7 +7,7 @@ import ObjectiveC
7
7
  @objc
8
8
  public class AuthAdapter: NSObject {
9
9
  @objc
10
- public static func login(provider: String, scopes: [String], loginHint: String?, completion: @escaping (NSDictionary?, String?) -> Void) {
10
+ public static func login(provider: String, scopes: [String], loginHint: String?, useSheet: Bool, completion: @escaping (NSDictionary?, String?) -> Void) {
11
11
  if provider == "google" {
12
12
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "GIDClientID") as? String, !clientId.isEmpty else {
13
13
  completion(nil, "configuration_error")
@@ -27,28 +27,10 @@ public class AuthAdapter: NSObject {
27
27
  GIDSignIn.sharedInstance.configuration = config
28
28
 
29
29
  let additionalScopes = scopes.isEmpty ? nil : scopes
30
+
31
+ // Modern sheet-like API
30
32
  GIDSignIn.sharedInstance.signIn(withPresenting: rootVC, hint: loginHint, additionalScopes: additionalScopes) { result, error in
31
- if let error = error {
32
- completion(nil, AuthAdapter.mapError(error))
33
- return
34
- }
35
-
36
- guard let user = result?.user else {
37
- completion(nil, "unknown")
38
- return
39
- }
40
-
41
- let data: [String: Any] = [
42
- "provider": "google",
43
- "email": user.profile?.email ?? "",
44
- "name": user.profile?.name ?? "",
45
- "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
46
- "idToken": user.idToken?.tokenString ?? "",
47
- "accessToken": user.accessToken.tokenString,
48
- "serverAuthCode": result?.serverAuthCode ?? "",
49
- "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
50
- ]
51
- completion(data as NSDictionary, nil)
33
+ self.handleGoogleResult(result, error: error, completion: completion)
52
34
  }
53
35
  }
54
36
  } else if provider == "apple" {
@@ -66,6 +48,31 @@ public class AuthAdapter: NSObject {
66
48
  }
67
49
  }
68
50
 
51
+ private static func handleGoogleResult(_ result: GIDSignInResult?, error: Error?, completion: @escaping (NSDictionary?, String?) -> Void) {
52
+ if let error = error {
53
+ completion(nil, error.localizedDescription)
54
+ return
55
+ }
56
+
57
+ guard let user = result?.user else {
58
+ completion(nil, "unknown")
59
+ return
60
+ }
61
+
62
+ let data: [String: Any] = [
63
+ "provider": "google",
64
+ "email": user.profile?.email ?? "",
65
+ "name": user.profile?.name ?? "",
66
+ "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
67
+ "idToken": user.idToken?.tokenString ?? "",
68
+ "accessToken": user.accessToken.tokenString,
69
+ "serverAuthCode": result?.serverAuthCode ?? "",
70
+ "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000,
71
+ "underlyingError": ""
72
+ ]
73
+ completion(data as NSDictionary, nil)
74
+ }
75
+
69
76
  static func mapError(_ error: Error) -> String {
70
77
  let nsError = error as NSError
71
78
  if nsError.domain == "com.google.GIDSignIn" {
@@ -92,27 +99,7 @@ public class AuthAdapter: NSObject {
92
99
  }
93
100
 
94
101
  currentUser.addScopes(scopes, presenting: rootVC) { result, error in
95
- if let error = error {
96
- completion(nil, AuthAdapter.mapError(error))
97
- return
98
- }
99
-
100
- guard let user = result?.user else {
101
- completion(nil, "unknown")
102
- return
103
- }
104
-
105
- let data: [String: Any] = [
106
- "provider": "google",
107
- "email": user.profile?.email ?? "",
108
- "name": user.profile?.name ?? "",
109
- "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
110
- "idToken": user.idToken?.tokenString ?? "",
111
- "accessToken": user.accessToken.tokenString,
112
- "serverAuthCode": result?.serverAuthCode ?? "",
113
- "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
114
- ]
115
- completion(data as NSDictionary, nil)
102
+ self.handleGoogleResult(result, error: error, completion: completion)
116
103
  }
117
104
  }
118
105
  }
@@ -126,7 +113,7 @@ public class AuthAdapter: NSObject {
126
113
 
127
114
  currentUser.refreshTokensIfNeeded { user, error in
128
115
  if let error = error {
129
- completion(nil, AuthAdapter.mapError(error))
116
+ completion(nil, error.localizedDescription)
130
117
  return
131
118
  }
132
119
 
@@ -138,9 +125,10 @@ public class AuthAdapter: NSObject {
138
125
  let data: [String: Any] = [
139
126
  "accessToken": user.accessToken.tokenString,
140
127
  "idToken": user.idToken?.tokenString ?? "",
141
- "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
128
+ "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000,
129
+ "underlyingError": error?.localizedDescription ?? ""
142
130
  ]
143
- completion(data as NSDictionary, nil)
131
+ completion(data as NSDictionary, error?.localizedDescription)
144
132
  }
145
133
  }
146
134
 
@@ -196,13 +184,14 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
196
184
  "provider": "apple",
197
185
  "email": email ?? "",
198
186
  "name": name,
199
- "idToken": idToken ?? ""
187
+ "idToken": idToken ?? "",
188
+ "underlyingError": ""
200
189
  ]
201
190
  completion(data as NSDictionary, nil)
202
191
  }
203
192
  }
204
193
 
205
194
  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
206
- completion(nil, AuthAdapter.mapError(error))
195
+ completion(nil, error.localizedDescription)
207
196
  }
208
197
  }
@@ -14,6 +14,11 @@
14
14
  #include "LoginOptions.hpp"
15
15
 
16
16
  namespace margelo::nitro::NitroAuth {
17
+
18
+ inline std::string nsToStd(NSString* _Nullable ns) {
19
+ if (ns == nil) return "";
20
+ return std::string([ns UTF8String]);
21
+ }
17
22
 
18
23
  std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
19
24
  auto promise = Promise<AuthUser>::create();
@@ -33,12 +38,12 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
33
38
  }
34
39
  }
35
40
 
36
- // Default scopes if none provided
37
- if (scopesArray.count == 0) {
38
- [scopesArray addObjectsFromArray:@[@"openid", @"email", @"profile"]];
41
+ BOOL useSheet = NO;
42
+ if (options.has_value() && options->useSheet.has_value()) {
43
+ useSheet = options->useSheet.value();
39
44
  }
40
45
 
41
- [AuthAdapter loginWithProvider:providerStr scopes:scopesArray loginHint:hintStr completion:^(NSDictionary* _Nullable data, NSString* _Nullable error) {
46
+ [AuthAdapter loginWithProvider:providerStr scopes:scopesArray loginHint:hintStr useSheet:useSheet completion:^(NSDictionary* _Nullable data, NSString* _Nullable error) {
42
47
  if (error != nil) {
43
48
  promise->reject(std::make_exception_ptr(std::runtime_error([error UTF8String])));
44
49
  return;
@@ -50,13 +55,14 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
50
55
 
51
56
  AuthUser user;
52
57
  user.provider = provider;
53
- user.email = std::string([[data objectForKey:@"email"] UTF8String]);
54
- user.name = std::string([[data objectForKey:@"name"] UTF8String]);
55
- user.photo = std::string([[data objectForKey:@"photo"] UTF8String]);
56
- user.idToken = std::string([[data objectForKey:@"idToken"] UTF8String]);
57
- if ([data objectForKey:@"accessToken"]) user.accessToken = std::string([[data objectForKey:@"accessToken"] UTF8String]);
58
- if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = std::string([[data objectForKey:@"serverAuthCode"] UTF8String]);
58
+ user.email = nsToStd([data objectForKey:@"email"]);
59
+ user.name = nsToStd([data objectForKey:@"name"]);
60
+ user.photo = nsToStd([data objectForKey:@"photo"]);
61
+ user.idToken = nsToStd([data objectForKey:@"idToken"]);
62
+ if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
63
+ if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
59
64
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
65
+ if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
60
66
 
61
67
  promise->resolve(user);
62
68
  }];
@@ -80,13 +86,14 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::requestScopes(const std::vector
80
86
 
81
87
  AuthUser user;
82
88
  user.provider = AuthProvider::GOOGLE;
83
- user.email = std::string([[data objectForKey:@"email"] UTF8String]);
84
- user.name = std::string([[data objectForKey:@"name"] UTF8String]);
85
- user.photo = std::string([[data objectForKey:@"photo"] UTF8String]);
86
- user.idToken = std::string([[data objectForKey:@"idToken"] UTF8String]);
87
- if ([data objectForKey:@"accessToken"]) user.accessToken = std::string([[data objectForKey:@"accessToken"] UTF8String]);
88
- if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = std::string([[data objectForKey:@"serverAuthCode"] UTF8String]);
89
+ user.email = nsToStd([data objectForKey:@"email"]);
90
+ user.name = nsToStd([data objectForKey:@"name"]);
91
+ user.photo = nsToStd([data objectForKey:@"photo"]);
92
+ user.idToken = nsToStd([data objectForKey:@"idToken"]);
93
+ if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
94
+ if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
89
95
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
96
+ if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
90
97
  promise->resolve(user);
91
98
  }];
92
99
  return promise;
@@ -100,8 +107,8 @@ std::shared_ptr<Promise<AuthTokens>> PlatformAuth::refreshToken() {
100
107
  return;
101
108
  }
102
109
  AuthTokens tokens;
103
- if ([data objectForKey:@"accessToken"]) tokens.accessToken = std::string([[data objectForKey:@"accessToken"] UTF8String]);
104
- if ([data objectForKey:@"idToken"]) tokens.idToken = std::string([[data objectForKey:@"idToken"] UTF8String]);
110
+ if ([data objectForKey:@"accessToken"]) tokens.accessToken = nsToStd([data objectForKey:@"accessToken"]);
111
+ if ([data objectForKey:@"idToken"]) tokens.idToken = nsToStd([data objectForKey:@"idToken"]);
105
112
  if ([data objectForKey:@"expirationTime"]) tokens.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
106
113
  promise->resolve(tokens);
107
114
  }];
@@ -117,13 +124,14 @@ std::shared_ptr<Promise<std::optional<AuthUser>>> PlatformAuth::silentRestore()
117
124
  }
118
125
  AuthUser user;
119
126
  user.provider = [[data objectForKey:@"provider"] isEqualToString:@"google"] ? AuthProvider::GOOGLE : AuthProvider::APPLE;
120
- user.email = std::string([[data objectForKey:@"email"] UTF8String]);
121
- user.name = std::string([[data objectForKey:@"name"] UTF8String]);
122
- user.photo = std::string([[data objectForKey:@"photo"] UTF8String]);
123
- user.idToken = std::string([[data objectForKey:@"idToken"] UTF8String]);
124
- if ([data objectForKey:@"accessToken"]) user.accessToken = std::string([[data objectForKey:@"accessToken"] UTF8String]);
125
- if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = std::string([[data objectForKey:@"serverAuthCode"] UTF8String]);
127
+ user.email = nsToStd([data objectForKey:@"email"]);
128
+ user.name = nsToStd([data objectForKey:@"name"]);
129
+ user.photo = nsToStd([data objectForKey:@"photo"]);
130
+ user.idToken = nsToStd([data objectForKey:@"idToken"]);
131
+ if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
132
+ if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
126
133
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
134
+ if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
127
135
  promise->resolve(user);
128
136
  }];
129
137
  return promise;
@@ -147,24 +147,19 @@ class AuthWeb {
147
147
  return tokens;
148
148
  }
149
149
  mapError(error) {
150
- if (error instanceof Error) {
151
- const msg = error.message.toLowerCase();
152
- if (msg.includes("cancel") || msg.includes("popup_closed")) {
153
- return new Error("cancelled");
154
- }
155
- if (msg.includes("network")) {
156
- return new Error("network_error");
157
- }
158
- if (msg.includes("client id") || msg.includes("config")) {
159
- return new Error("configuration_error");
160
- }
161
- return error;
162
- }
163
- const msg = String(error).toLowerCase();
150
+ const rawMessage = error instanceof Error ? error.message : String(error);
151
+ const msg = rawMessage.toLowerCase();
152
+ let mappedMsg = rawMessage;
164
153
  if (msg.includes("cancel") || msg.includes("popup_closed")) {
165
- return new Error("cancelled");
154
+ mappedMsg = "cancelled";
155
+ } else if (msg.includes("network")) {
156
+ mappedMsg = "network_error";
157
+ } else if (msg.includes("client id") || msg.includes("config")) {
158
+ mappedMsg = "configuration_error";
166
159
  }
167
- return new Error(String(error));
160
+ const authError = new Error(mappedMsg);
161
+ authError.underlyingError = rawMessage;
162
+ return authError;
168
163
  }
169
164
  async loginGoogle(scopes, loginHint) {
170
165
  const config = getConfig();
@@ -177,13 +172,11 @@ class AuthWeb {
177
172
  const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
178
173
  authUrl.searchParams.set("client_id", clientId);
179
174
  authUrl.searchParams.set("redirect_uri", redirectUri);
180
- // Requesting code alongside tokens for server-side verification if needed
181
175
  authUrl.searchParams.set("response_type", "id_token token code");
182
176
  authUrl.searchParams.set("scope", scopes.join(" "));
183
177
  authUrl.searchParams.set("nonce", Math.random().toString(36).slice(2));
184
- authUrl.searchParams.set("access_type", "offline"); // Needed for server auth code flow
185
- authUrl.searchParams.set("prompt", "consent"); // Force consent to get refresh token/code sometimes
186
-
178
+ authUrl.searchParams.set("access_type", "offline");
179
+ authUrl.searchParams.set("prompt", "consent");
187
180
  if (loginHint) {
188
181
  authUrl.searchParams.set("login_hint", loginHint);
189
182
  }
@@ -282,7 +275,7 @@ class AuthWeb {
282
275
  };
283
276
  this.updateUser(user);
284
277
  resolve();
285
- }).catch(e => reject(this.mapError(e)));
278
+ }).catch(err => reject(this.mapError(err)));
286
279
  };
287
280
  script.onerror = () => reject(new Error("Failed to load Apple SDK"));
288
281
  document.head.appendChild(script);