react-native-nitro-auth 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -2
- package/android/build.gradle +7 -2
- package/android/src/main/cpp/PlatformAuth+Android.cpp +14 -3
- package/android/src/main/java/com/auth/AuthAdapter.kt +100 -3
- package/app.plugin.js +7 -5
- package/cpp/HybridAuth.cpp +58 -13
- package/cpp/HybridAuth.hpp +8 -0
- package/cpp/PlatformAuth.hpp +2 -1
- package/ios/PlatformAuth+iOS.mm +22 -4
- package/lib/commonjs/Auth.web.js +48 -10
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/AuthStorage.nitro.js +6 -0
- package/lib/commonjs/AuthStorage.nitro.js.map +1 -0
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +12 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/module/Auth.web.js +48 -10
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/AuthStorage.nitro.js +4 -0
- package/lib/module/AuthStorage.nitro.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/typescript/Auth.nitro.d.ts +4 -0
- package/lib/typescript/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/Auth.web.d.ts +8 -1
- package/lib/typescript/Auth.web.d.ts.map +1 -1
- package/lib/typescript/AuthStorage.nitro.d.ts +19 -0
- package/lib/typescript/AuthStorage.nitro.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +1 -0
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroAuth+autolinking.cmake +2 -1
- package/nitrogen/generated/android/NitroAuth+autolinking.gradle +1 -1
- package/nitrogen/generated/android/NitroAuthOnLoad.cpp +1 -1
- package/nitrogen/generated/android/NitroAuthOnLoad.hpp +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/auth/NitroAuthOnLoad.kt +1 -1
- package/nitrogen/generated/ios/NitroAuth+autolinking.rb +1 -1
- package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.cpp +1 -1
- package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.hpp +1 -1
- package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Umbrella.hpp +1 -1
- package/nitrogen/generated/ios/NitroAuthAutolinking.mm +1 -1
- package/nitrogen/generated/ios/NitroAuthAutolinking.swift +1 -1
- package/nitrogen/generated/shared/c++/AuthProvider.hpp +1 -1
- package/nitrogen/generated/shared/c++/AuthTokens.hpp +1 -1
- package/nitrogen/generated/shared/c++/AuthUser.hpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +3 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +7 -1
- package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/LoginOptions.hpp +7 -3
- package/package.json +1 -1
- package/react-native-nitro-auth.podspec +1 -1
- package/src/Auth.nitro.ts +5 -0
- package/src/Auth.web.ts +71 -11
- package/src/AuthStorage.nitro.ts +17 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# react-native-nitro-auth
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-nitro-auth)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nitro.margelo.com)
|
|
6
|
+
|
|
3
7
|
🚀 **High-performance, JSI-powered Authentication for React Native.**
|
|
4
8
|
|
|
5
9
|
Nitro Auth is a modern authentication library for React Native built on top of [Nitro Modules](https://github.com/mrousavy/nitro). It provides a unified, type-safe API for Google and Apple Sign-In with zero-bridge overhead.
|
|
@@ -23,6 +27,9 @@ Nitro Auth is designed to replace legacy modules like `@react-native-google-sign
|
|
|
23
27
|
- **Expo Ready**: Comes with a powerful Config Plugin for zero-config setup.
|
|
24
28
|
- **Cross-Platform**: Unified API for iOS, Android, and Web.
|
|
25
29
|
- **Auto-Refresh**: Synchronous access to tokens with automatic silent refresh.
|
|
30
|
+
- **Google One-Tap**: Modern login experience on Android using Credential Manager.
|
|
31
|
+
- **Custom Storage**: Pluggable storage adapters for secure persistence (e.g., Keychain).
|
|
32
|
+
- **Refresh Interceptors**: Listen to token updates globally.
|
|
26
33
|
|
|
27
34
|
## Installation
|
|
28
35
|
|
|
@@ -45,7 +52,8 @@ Add the plugin to `app.json`:
|
|
|
45
52
|
"ios": {
|
|
46
53
|
"googleClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
|
|
47
54
|
"googleServerClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
|
|
48
|
-
"googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID"
|
|
55
|
+
"googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID",
|
|
56
|
+
"appleSignIn": true
|
|
49
57
|
},
|
|
50
58
|
"android": {
|
|
51
59
|
"googleClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com"
|
|
@@ -57,7 +65,9 @@ Add the plugin to `app.json`:
|
|
|
57
65
|
}
|
|
58
66
|
```
|
|
59
67
|
|
|
60
|
-
> [!NOTE]
|
|
68
|
+
> [!NOTE]
|
|
69
|
+
> `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.
|
|
61
71
|
|
|
62
72
|
### Bare React Native
|
|
63
73
|
|
|
@@ -182,6 +192,49 @@ if (user?.serverAuthCode) {
|
|
|
182
192
|
}
|
|
183
193
|
```
|
|
184
194
|
|
|
195
|
+
### Custom Storage Adapter
|
|
196
|
+
|
|
197
|
+
By default, Nitro Auth uses standard local storage. You can provide a custom adapter for better security (e.g., using `react-native-keychain`):
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { AuthService, AuthStorageAdapter } from "react-native-nitro-auth";
|
|
201
|
+
|
|
202
|
+
const myStorage: AuthStorageAdapter = {
|
|
203
|
+
save: (key, value) => {
|
|
204
|
+
/* Save to Keychain */
|
|
205
|
+
},
|
|
206
|
+
load: (key) => {
|
|
207
|
+
/* Load from Keychain */
|
|
208
|
+
},
|
|
209
|
+
remove: (key) => {
|
|
210
|
+
/* Clear from Keychain */
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
AuthService.setStorageAdapter(myStorage);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Token Refresh Listeners
|
|
218
|
+
|
|
219
|
+
Perfect for updating your API client (e.g., Axios/Fetch) whenever tokens are refreshed in the background:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
AuthService.onTokensRefreshed((tokens) => {
|
|
223
|
+
console.log("Tokens were updated!", tokens.accessToken);
|
|
224
|
+
apiClient.defaults.headers.common[
|
|
225
|
+
"Authorization"
|
|
226
|
+
] = `Bearer ${tokens.accessToken}`;
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Google One-Tap (Android)
|
|
231
|
+
|
|
232
|
+
Explicitly enable the modern One-Tap flow on Android:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
await login("google", { useOneTap: true });
|
|
236
|
+
```
|
|
237
|
+
|
|
185
238
|
## API Reference
|
|
186
239
|
|
|
187
240
|
### useAuth Hook
|
package/android/build.gradle
CHANGED
|
@@ -89,12 +89,17 @@ repositories {
|
|
|
89
89
|
|
|
90
90
|
dependencies {
|
|
91
91
|
implementation "com.facebook.react:react-native:+"
|
|
92
|
-
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.
|
|
92
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.24"
|
|
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.
|
|
96
|
+
implementation "com.google.android.gms:play-services-auth:21.2.0"
|
|
97
97
|
|
|
98
98
|
// Activity result APIs
|
|
99
99
|
implementation "androidx.activity:activity-ktx:1.9.3"
|
|
100
|
+
|
|
101
|
+
// Google Credential Manager (One-Tap / Passkeys)
|
|
102
|
+
implementation "androidx.credentials:credentials:1.5.0"
|
|
103
|
+
implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
|
|
104
|
+
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
|
|
100
105
|
}
|
|
@@ -24,7 +24,7 @@ static std::shared_ptr<Promise<AuthTokens>> gRefreshPromise;
|
|
|
24
24
|
static std::shared_ptr<Promise<std::optional<AuthUser>>> gSilentPromise;
|
|
25
25
|
static std::mutex gMutex;
|
|
26
26
|
|
|
27
|
-
std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::
|
|
27
|
+
std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
|
|
28
28
|
auto promise = Promise<AuthUser>::create();
|
|
29
29
|
auto contextPtr = static_cast<jobject>(AuthCache::getAndroidContext());
|
|
30
30
|
if (!contextPtr) {
|
|
@@ -38,6 +38,16 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
std::string providerStr = (provider == AuthProvider::GOOGLE) ? "google" : "apple";
|
|
41
|
+
std::vector<std::string> scopes = {"email", "profile"};
|
|
42
|
+
std::optional<std::string> loginHint;
|
|
43
|
+
bool useOneTap = false;
|
|
44
|
+
|
|
45
|
+
if (options) {
|
|
46
|
+
if (options->scopes) scopes = *options->scopes;
|
|
47
|
+
loginHint = options->loginHint;
|
|
48
|
+
useOneTap = options->useOneTap.value_or(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
JNIEnv* env = Environment::current();
|
|
42
52
|
jclass stringClass = env->FindClass("java/lang/String");
|
|
43
53
|
jobjectArray jScopes = env->NewObjectArray(scopes.size(), stringClass, nullptr);
|
|
@@ -48,13 +58,14 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
|
|
|
48
58
|
jstring jLoginHint = loginHint.has_value() ? make_jstring(loginHint.value()).get() : nullptr;
|
|
49
59
|
jclass adapterClass = env->FindClass("com/auth/AuthAdapter");
|
|
50
60
|
jmethodID loginMethod = env->GetStaticMethodID(adapterClass, "loginSync",
|
|
51
|
-
"(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)V");
|
|
61
|
+
"(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Z)V");
|
|
52
62
|
env->CallStaticVoidMethod(adapterClass, loginMethod,
|
|
53
63
|
contextPtr,
|
|
54
64
|
make_jstring(providerStr).get(),
|
|
55
65
|
nullptr,
|
|
56
66
|
jScopes,
|
|
57
|
-
jLoginHint
|
|
67
|
+
jLoginHint,
|
|
68
|
+
(jboolean)useOneTap);
|
|
58
69
|
|
|
59
70
|
return promise;
|
|
60
71
|
}
|
|
@@ -10,12 +10,23 @@ import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
|
|
10
10
|
import com.google.android.gms.common.GoogleApiAvailability
|
|
11
11
|
import com.google.android.gms.common.ConnectionResult
|
|
12
12
|
import com.google.android.gms.common.api.Scope
|
|
13
|
+
import androidx.credentials.CredentialManager
|
|
14
|
+
import androidx.credentials.GetCredentialRequest
|
|
15
|
+
import androidx.credentials.GetCredentialResponse
|
|
16
|
+
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
|
17
|
+
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
|
18
|
+
import kotlinx.coroutines.CoroutineScope
|
|
19
|
+
import kotlinx.coroutines.Dispatchers
|
|
20
|
+
import kotlinx.coroutines.launch
|
|
21
|
+
import android.app.Activity
|
|
22
|
+
import android.app.Application
|
|
13
23
|
|
|
14
24
|
object AuthAdapter {
|
|
15
25
|
private const val TAG = "AuthAdapter"
|
|
16
26
|
private const val PREF_NAME = "nitro_auth"
|
|
17
27
|
|
|
18
28
|
private var appContext: Context? = null
|
|
29
|
+
private var currentActivity: Activity? = null
|
|
19
30
|
private var googleSignInClient: GoogleSignInClient? = null
|
|
20
31
|
private var pendingScopes: List<String> = emptyList()
|
|
21
32
|
|
|
@@ -45,7 +56,19 @@ object AuthAdapter {
|
|
|
45
56
|
private external fun nativeOnRefreshError(error: String)
|
|
46
57
|
|
|
47
58
|
fun initialize(context: Context) {
|
|
48
|
-
|
|
59
|
+
val app = context.applicationContext as? Application
|
|
60
|
+
appContext = app
|
|
61
|
+
|
|
62
|
+
app?.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
|
63
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
|
|
64
|
+
override fun onActivityStarted(activity: Activity) { currentActivity = activity }
|
|
65
|
+
override fun onActivityResumed(activity: Activity) { currentActivity = activity }
|
|
66
|
+
override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
67
|
+
override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
68
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
69
|
+
override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
70
|
+
})
|
|
71
|
+
|
|
49
72
|
try {
|
|
50
73
|
System.loadLibrary("NitroAuth")
|
|
51
74
|
nativeInitialize(appContext!!)
|
|
@@ -73,7 +96,7 @@ object AuthAdapter {
|
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
@JvmStatic
|
|
76
|
-
fun loginSync(context: Context, provider: String, googleClientId: String?, scopes: Array<String>?, loginHint: String
|
|
99
|
+
fun loginSync(context: Context, provider: String, googleClientId: String?, scopes: Array<String>?, loginHint: String?, useOneTap: Boolean) {
|
|
77
100
|
if (provider == "apple") {
|
|
78
101
|
nativeOnLoginError("Apple Sign-In is not supported on Android.")
|
|
79
102
|
return
|
|
@@ -94,10 +117,84 @@ object AuthAdapter {
|
|
|
94
117
|
val requestedScopes = scopes?.toList() ?: listOf("email", "profile")
|
|
95
118
|
pendingScopes = requestedScopes
|
|
96
119
|
|
|
97
|
-
|
|
120
|
+
if (useOneTap) {
|
|
121
|
+
loginOneTap(context, clientId, requestedScopes)
|
|
122
|
+
} else {
|
|
123
|
+
val intent = GoogleSignInActivity.createIntent(ctx, clientId, requestedScopes.toTypedArray(), loginHint)
|
|
124
|
+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
125
|
+
ctx.startActivity(intent)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun loginOneTap(context: Context, clientId: String, scopes: List<String>) {
|
|
130
|
+
val activity = currentActivity ?: context as? Activity
|
|
131
|
+
if (activity == null) {
|
|
132
|
+
Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
|
|
133
|
+
return loginLegacy(context, clientId, scopes)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
val credentialManager = CredentialManager.create(activity)
|
|
137
|
+
val googleIdOption = GetGoogleIdOption.Builder()
|
|
138
|
+
.setFilterByAuthorizedAccounts(false)
|
|
139
|
+
.setServerClientId(clientId)
|
|
140
|
+
.setAutoSelectEnabled(false) // Disable auto-select for testing so the sheet always shows
|
|
141
|
+
.build()
|
|
142
|
+
|
|
143
|
+
val request = GetCredentialRequest.Builder()
|
|
144
|
+
.addCredentialOption(googleIdOption)
|
|
145
|
+
.build()
|
|
146
|
+
|
|
147
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
148
|
+
try {
|
|
149
|
+
val result = credentialManager.getCredential(context = activity, request = request)
|
|
150
|
+
handleCredentialResponse(result, scopes)
|
|
151
|
+
} catch (e: Exception) {
|
|
152
|
+
Log.w(TAG, "One-Tap failed, falling back to legacy: ${e.message}")
|
|
153
|
+
loginLegacy(context, clientId, scopes)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun loginLegacy(context: Context, clientId: String, scopes: List<String>) {
|
|
159
|
+
val ctx = appContext ?: context.applicationContext
|
|
160
|
+
val intent = GoogleSignInActivity.createIntent(ctx, clientId, scopes.toTypedArray(), null)
|
|
161
|
+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
98
162
|
ctx.startActivity(intent)
|
|
99
163
|
}
|
|
100
164
|
|
|
165
|
+
private fun handleCredentialResponse(response: GetCredentialResponse, scopes: List<String>) {
|
|
166
|
+
val credential = response.credential
|
|
167
|
+
val googleIdTokenCredential = try {
|
|
168
|
+
if (credential is GoogleIdTokenCredential) {
|
|
169
|
+
credential
|
|
170
|
+
} else if (credential.type == "com.google.android.libraries.identity.googleid.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL") {
|
|
171
|
+
GoogleIdTokenCredential.createFrom(credential.data)
|
|
172
|
+
} else {
|
|
173
|
+
null
|
|
174
|
+
}
|
|
175
|
+
} catch (e: Exception) {
|
|
176
|
+
Log.e(TAG, "Failed to parse Google ID token credential: ${e.message}")
|
|
177
|
+
null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (googleIdTokenCredential != null) {
|
|
181
|
+
nativeOnLoginSuccess(
|
|
182
|
+
"google",
|
|
183
|
+
googleIdTokenCredential.id,
|
|
184
|
+
googleIdTokenCredential.displayName,
|
|
185
|
+
googleIdTokenCredential.profilePictureUri?.toString(),
|
|
186
|
+
googleIdTokenCredential.idToken,
|
|
187
|
+
null,
|
|
188
|
+
null,
|
|
189
|
+
scopes.toTypedArray(),
|
|
190
|
+
null
|
|
191
|
+
)
|
|
192
|
+
} else {
|
|
193
|
+
Log.w(TAG, "Unsupported credential type: ${credential.type}")
|
|
194
|
+
nativeOnLoginError("Unsupported credential type: ${credential.type}")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
101
198
|
@JvmStatic
|
|
102
199
|
fun requestScopesSync(context: Context, scopes: Array<String>) {
|
|
103
200
|
val ctx = appContext ?: context.applicationContext
|
package/app.plugin.js
CHANGED
|
@@ -36,10 +36,12 @@ const withNitroAuth = (config, props = {}) => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// 2. iOS Entitlements
|
|
39
|
-
|
|
40
|
-
config
|
|
41
|
-
|
|
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.
|
|
68
|
+
"0.1.6"
|
|
67
69
|
);
|
package/cpp/HybridAuth.cpp
CHANGED
|
@@ -29,7 +29,14 @@ bool HybridAuth::getHasPlayServices() {
|
|
|
29
29
|
|
|
30
30
|
void HybridAuth::loadFromCache() {
|
|
31
31
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
32
|
-
|
|
32
|
+
std::optional<std::string> json;
|
|
33
|
+
|
|
34
|
+
if (_storageAdapter) {
|
|
35
|
+
json = _storageAdapter->load("nitro_auth_user");
|
|
36
|
+
} else {
|
|
37
|
+
json = AuthCache::getUserJson();
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
if (json) {
|
|
34
41
|
_currentUser = JSONSerializer::deserialize(*json);
|
|
35
42
|
if (_currentUser && _currentUser->scopes) {
|
|
@@ -41,9 +48,17 @@ void HybridAuth::loadFromCache() {
|
|
|
41
48
|
void HybridAuth::saveToCache(const std::optional<AuthUser>& user) {
|
|
42
49
|
if (user) {
|
|
43
50
|
auto json = JSONSerializer::serialize(*user);
|
|
44
|
-
|
|
51
|
+
if (_storageAdapter) {
|
|
52
|
+
_storageAdapter->save("nitro_auth_user", json);
|
|
53
|
+
} else {
|
|
54
|
+
AuthCache::setUserJson(json);
|
|
55
|
+
}
|
|
45
56
|
} else {
|
|
46
|
-
|
|
57
|
+
if (_storageAdapter) {
|
|
58
|
+
_storageAdapter->remove("nitro_auth_user");
|
|
59
|
+
} else {
|
|
60
|
+
AuthCache::clear();
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
|
|
@@ -73,6 +88,17 @@ std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(co
|
|
|
73
88
|
};
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
std::function<void()> HybridAuth::onTokensRefreshed(const std::function<void(const AuthTokens&)>& callback) {
|
|
92
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
93
|
+
int id = _nextTokenListenerId++;
|
|
94
|
+
_tokenListeners[id] = callback;
|
|
95
|
+
|
|
96
|
+
return [this, id]() {
|
|
97
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
98
|
+
_tokenListeners.erase(id);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
void HybridAuth::logout() {
|
|
77
103
|
{
|
|
78
104
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
@@ -86,20 +112,16 @@ void HybridAuth::logout() {
|
|
|
86
112
|
|
|
87
113
|
std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
|
|
88
114
|
auto promise = Promise<void>::create();
|
|
89
|
-
std::vector<std::string> scopes;
|
|
90
|
-
std::optional<std::string> loginHint;
|
|
91
|
-
if (options) {
|
|
92
|
-
if (options->scopes) scopes = *options->scopes;
|
|
93
|
-
loginHint = options->loginHint;
|
|
94
|
-
}
|
|
95
115
|
|
|
96
|
-
auto loginPromise = PlatformAuth::login(provider,
|
|
97
|
-
loginPromise->addOnResolvedListener([this, promise,
|
|
116
|
+
auto loginPromise = PlatformAuth::login(provider, options);
|
|
117
|
+
loginPromise->addOnResolvedListener([this, promise, options](const AuthUser& user) {
|
|
98
118
|
{
|
|
99
119
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
100
120
|
_currentUser = user;
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
if (options && options->scopes) {
|
|
122
|
+
_grantedScopes = *options->scopes;
|
|
123
|
+
}
|
|
124
|
+
if (_currentUser) _currentUser->scopes = _grantedScopes;
|
|
103
125
|
saveToCache(_currentUser);
|
|
104
126
|
}
|
|
105
127
|
notifyAuthStateChanged();
|
|
@@ -202,6 +224,7 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
|
|
|
202
224
|
saveToCache(_currentUser);
|
|
203
225
|
}
|
|
204
226
|
}
|
|
227
|
+
notifyTokensRefreshed(tokens);
|
|
205
228
|
notifyAuthStateChanged();
|
|
206
229
|
promise->resolve(tokens);
|
|
207
230
|
});
|
|
@@ -216,4 +239,26 @@ void HybridAuth::setLoggingEnabled(bool enabled) {
|
|
|
216
239
|
sLoggingEnabled = enabled;
|
|
217
240
|
}
|
|
218
241
|
|
|
242
|
+
void HybridAuth::setStorageAdapter(const std::optional<std::shared_ptr<HybridAuthStorageAdapterSpec>>& adapter) {
|
|
243
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
244
|
+
_storageAdapter = adapter.value_or(nullptr);
|
|
245
|
+
if (_storageAdapter) {
|
|
246
|
+
loadFromCache();
|
|
247
|
+
notifyAuthStateChanged();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
void HybridAuth::notifyTokensRefreshed(const AuthTokens& tokens) {
|
|
252
|
+
std::vector<std::function<void(const AuthTokens&)>> listeners;
|
|
253
|
+
{
|
|
254
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
255
|
+
for (auto const& [id, listener] : _tokenListeners) {
|
|
256
|
+
listeners.push_back(listener);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const auto& listener : listeners) {
|
|
260
|
+
listener(tokens);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
219
264
|
} // namespace margelo::nitro::NitroAuth
|
package/cpp/HybridAuth.hpp
CHANGED
|
@@ -28,18 +28,26 @@ public:
|
|
|
28
28
|
|
|
29
29
|
void logout() override;
|
|
30
30
|
std::function<void()> onAuthStateChanged(const std::function<void(const std::optional<AuthUser>&)>& callback) override;
|
|
31
|
+
std::function<void()> onTokensRefreshed(const std::function<void(const AuthTokens&)>& callback) override;
|
|
31
32
|
void setLoggingEnabled(bool enabled) override;
|
|
33
|
+
void setStorageAdapter(const std::optional<std::shared_ptr<HybridAuthStorageAdapterSpec>>& adapter) override;
|
|
32
34
|
|
|
33
35
|
private:
|
|
34
36
|
void loadFromCache();
|
|
35
37
|
void saveToCache(const std::optional<AuthUser>& user);
|
|
36
38
|
void notifyAuthStateChanged();
|
|
39
|
+
void notifyTokensRefreshed(const AuthTokens& tokens);
|
|
37
40
|
|
|
38
41
|
private:
|
|
39
42
|
std::optional<AuthUser> _currentUser;
|
|
40
43
|
std::vector<std::string> _grantedScopes;
|
|
41
44
|
std::map<int, std::function<void(const std::optional<AuthUser>&)>> _listeners;
|
|
42
45
|
int _nextListenerId = 0;
|
|
46
|
+
|
|
47
|
+
std::shared_ptr<HybridAuthStorageAdapterSpec> _storageAdapter;
|
|
48
|
+
std::map<int, std::function<void(const AuthTokens&)>> _tokenListeners;
|
|
49
|
+
int _nextTokenListenerId = 0;
|
|
50
|
+
|
|
43
51
|
std::mutex _mutex;
|
|
44
52
|
|
|
45
53
|
static constexpr auto TAG = "Auth";
|
package/cpp/PlatformAuth.hpp
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "AuthProvider.hpp"
|
|
4
4
|
#include "AuthUser.hpp"
|
|
5
5
|
#include "AuthTokens.hpp"
|
|
6
|
+
#include "LoginOptions.hpp"
|
|
6
7
|
#include <NitroModules/Promise.hpp>
|
|
7
8
|
#include <memory>
|
|
8
9
|
#include <vector>
|
|
@@ -14,7 +15,7 @@ using namespace margelo::nitro;
|
|
|
14
15
|
|
|
15
16
|
class PlatformAuth {
|
|
16
17
|
public:
|
|
17
|
-
static std::shared_ptr<Promise<AuthUser>> login(AuthProvider provider, const std::
|
|
18
|
+
static std::shared_ptr<Promise<AuthUser>> login(AuthProvider provider, const std::optional<LoginOptions>& options = std::nullopt);
|
|
18
19
|
static std::shared_ptr<Promise<AuthUser>> requestScopes(const std::vector<std::string>& scopes);
|
|
19
20
|
static std::shared_ptr<Promise<AuthTokens>> refreshToken();
|
|
20
21
|
static std::shared_ptr<Promise<std::optional<AuthUser>>> silentRestore();
|
package/ios/PlatformAuth+iOS.mm
CHANGED
|
@@ -11,14 +11,32 @@
|
|
|
11
11
|
#import "react_native_nitro_auth-Swift.h"
|
|
12
12
|
#endif
|
|
13
13
|
|
|
14
|
+
#include "LoginOptions.hpp"
|
|
15
|
+
|
|
14
16
|
namespace margelo::nitro::NitroAuth {
|
|
15
17
|
|
|
16
|
-
std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::
|
|
18
|
+
std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
|
|
17
19
|
auto promise = Promise<AuthUser>::create();
|
|
18
20
|
NSString* providerStr = provider == AuthProvider::GOOGLE ? @"google" : @"apple";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
NSString* hintStr =
|
|
21
|
+
|
|
22
|
+
NSMutableArray* scopesArray = [NSMutableArray array];
|
|
23
|
+
NSString* hintStr = nil;
|
|
24
|
+
|
|
25
|
+
if (options.has_value()) {
|
|
26
|
+
if (options->scopes.has_value()) {
|
|
27
|
+
for (const auto& scope : *options->scopes) {
|
|
28
|
+
[scopesArray addObject:[NSString stringWithUTF8String:scope.c_str()]];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (options->loginHint.has_value()) {
|
|
32
|
+
hintStr = [NSString stringWithUTF8String:options->loginHint->c_str()];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Default scopes if none provided
|
|
37
|
+
if (scopesArray.count == 0) {
|
|
38
|
+
[scopesArray addObjectsFromArray:@[@"openid", @"email", @"profile"]];
|
|
39
|
+
}
|
|
22
40
|
|
|
23
41
|
[AuthAdapter loginWithProvider:providerStr scopes:scopesArray loginHint:hintStr completion:^(NSDictionary* _Nullable data, NSString* _Nullable error) {
|
|
24
42
|
if (error != nil) {
|
package/lib/commonjs/Auth.web.js
CHANGED
|
@@ -19,24 +19,35 @@ const getConfig = () => {
|
|
|
19
19
|
class AuthWeb {
|
|
20
20
|
_grantedScopes = [];
|
|
21
21
|
_listeners = [];
|
|
22
|
+
_tokenListeners = [];
|
|
22
23
|
constructor() {
|
|
23
|
-
|
|
24
|
+
this.loadFromCache();
|
|
25
|
+
}
|
|
26
|
+
loadFromCache() {
|
|
27
|
+
const cached = this._storageAdapter ? this._storageAdapter.load(CACHE_KEY) : localStorage.getItem(CACHE_KEY);
|
|
24
28
|
if (cached) {
|
|
25
29
|
try {
|
|
26
30
|
this._currentUser = JSON.parse(cached);
|
|
27
31
|
} catch {
|
|
28
|
-
|
|
32
|
+
this.removeFromCache(CACHE_KEY);
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
const scopes = localStorage.getItem(SCOPES_KEY);
|
|
35
|
+
const scopes = this._storageAdapter ? this._storageAdapter.load(SCOPES_KEY) : localStorage.getItem(SCOPES_KEY);
|
|
32
36
|
if (scopes) {
|
|
33
37
|
try {
|
|
34
38
|
this._grantedScopes = JSON.parse(scopes);
|
|
35
39
|
} catch {
|
|
36
|
-
|
|
40
|
+
this.removeFromCache(SCOPES_KEY);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
}
|
|
44
|
+
removeFromCache(key) {
|
|
45
|
+
if (this._storageAdapter) {
|
|
46
|
+
this._storageAdapter.remove(key);
|
|
47
|
+
} else {
|
|
48
|
+
localStorage.removeItem(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
get currentUser() {
|
|
41
52
|
return this._currentUser;
|
|
42
53
|
}
|
|
@@ -53,6 +64,12 @@ class AuthWeb {
|
|
|
53
64
|
this._listeners = this._listeners.filter(l => l !== callback);
|
|
54
65
|
};
|
|
55
66
|
}
|
|
67
|
+
onTokensRefreshed(callback) {
|
|
68
|
+
this._tokenListeners.push(callback);
|
|
69
|
+
return () => {
|
|
70
|
+
this._tokenListeners = this._tokenListeners.filter(l => l !== callback);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
56
73
|
notify() {
|
|
57
74
|
this._listeners.forEach(l => l(this._currentUser));
|
|
58
75
|
}
|
|
@@ -93,7 +110,11 @@ class AuthWeb {
|
|
|
93
110
|
async revokeScopes(scopes) {
|
|
94
111
|
_logger.logger.log("Revoking scopes:", scopes);
|
|
95
112
|
this._grantedScopes = this._grantedScopes.filter(s => !scopes.includes(s));
|
|
96
|
-
|
|
113
|
+
if (this._storageAdapter) {
|
|
114
|
+
this._storageAdapter.save(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
115
|
+
} else {
|
|
116
|
+
localStorage.setItem(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
117
|
+
}
|
|
97
118
|
if (this._currentUser) {
|
|
98
119
|
this._currentUser.scopes = this._grantedScopes;
|
|
99
120
|
this.updateUser(this._currentUser);
|
|
@@ -118,10 +139,12 @@ class AuthWeb {
|
|
|
118
139
|
}
|
|
119
140
|
_logger.logger.log("Refreshing tokens...");
|
|
120
141
|
await this.loginGoogle(this._grantedScopes.length > 0 ? this._grantedScopes : DEFAULT_SCOPES);
|
|
121
|
-
|
|
142
|
+
const tokens = {
|
|
122
143
|
accessToken: this._currentUser.accessToken,
|
|
123
144
|
idToken: this._currentUser.idToken
|
|
124
145
|
};
|
|
146
|
+
this._tokenListeners.forEach(l => l(tokens));
|
|
147
|
+
return tokens;
|
|
125
148
|
}
|
|
126
149
|
mapError(error) {
|
|
127
150
|
if (error instanceof Error) {
|
|
@@ -192,7 +215,11 @@ class AuthWeb {
|
|
|
192
215
|
const code = params.get("code");
|
|
193
216
|
if (idToken) {
|
|
194
217
|
this._grantedScopes = scopes;
|
|
195
|
-
|
|
218
|
+
if (this._storageAdapter) {
|
|
219
|
+
this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
|
|
220
|
+
} else {
|
|
221
|
+
localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
|
|
222
|
+
}
|
|
196
223
|
const user = {
|
|
197
224
|
provider: "google",
|
|
198
225
|
idToken,
|
|
@@ -264,18 +291,29 @@ class AuthWeb {
|
|
|
264
291
|
logout() {
|
|
265
292
|
this._currentUser = undefined;
|
|
266
293
|
this._grantedScopes = [];
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
this.removeFromCache(CACHE_KEY);
|
|
295
|
+
this.removeFromCache(SCOPES_KEY);
|
|
269
296
|
this.notify();
|
|
270
297
|
}
|
|
271
298
|
updateUser(user) {
|
|
272
299
|
this._currentUser = user;
|
|
273
|
-
|
|
300
|
+
if (this._storageAdapter) {
|
|
301
|
+
this._storageAdapter.save(CACHE_KEY, JSON.stringify(user));
|
|
302
|
+
} else {
|
|
303
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(user));
|
|
304
|
+
}
|
|
274
305
|
this.notify();
|
|
275
306
|
}
|
|
276
307
|
setLoggingEnabled(enabled) {
|
|
277
308
|
_logger.logger.setEnabled(enabled);
|
|
278
309
|
}
|
|
310
|
+
setStorageAdapter(adapter) {
|
|
311
|
+
this._storageAdapter = adapter;
|
|
312
|
+
if (adapter) {
|
|
313
|
+
this.loadFromCache();
|
|
314
|
+
this.notify();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
279
317
|
name = "Auth";
|
|
280
318
|
dispose() {}
|
|
281
319
|
equals(other) {
|