@novastera-oss/nitro-metamask 0.3.2 → 0.3.3

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 (22) hide show
  1. package/README.md +80 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +409 -0
  4. package/android/src/main/java/com/{nitrometamask → margelo/nitro/nitrometamask}/MetamaskContextHolder.kt +1 -1
  5. package/android/src/main/java/com/{nitrometamask → margelo/nitro/nitrometamask}/NitroMetamaskPackage.kt +1 -1
  6. package/ios/HybridNitroMetamask.swift +145 -0
  7. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +10 -0
  8. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
  9. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.cpp +21 -0
  10. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +2 -0
  11. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt +8 -0
  12. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +25 -0
  13. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +1 -0
  14. package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +16 -1
  15. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +2 -0
  16. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +37 -0
  17. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +2 -0
  18. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +4 -1
  19. package/package.json +2 -2
  20. package/react-native.config.js +1 -1
  21. package/src/specs/nitro-metamask.nitro.ts +10 -0
  22. package/android/src/main/java/com/nitrometamask/HybridNitroMetamask.kt +0 -146
package/README.md CHANGED
@@ -20,6 +20,86 @@ Novastera authentication with native mobile libraries
20
20
  npm install @novastera-oss/nitro-metamask react-native-nitro-modules
21
21
  ```
22
22
 
23
+ ### Android Configuration
24
+
25
+ **Required:** Add a deep link intent filter to enable MetaMask to return to your app after connecting or signing.
26
+
27
+ **File:** `android/app/src/main/AndroidManifest.xml`
28
+
29
+ Add this inside your `MainActivity` `<activity>` tag:
30
+
31
+ ```xml
32
+ <activity
33
+ android:name=".MainActivity"
34
+ android:launchMode="singleTask"
35
+ ...>
36
+ <!-- Existing intent filters -->
37
+
38
+ <!-- Deep link intent filter for MetaMask callback -->
39
+ <intent-filter>
40
+ <action android:name="android.intent.action.VIEW" />
41
+ <category android:name="android.intent.category.DEFAULT" />
42
+ <category android:name="android.intent.category.BROWSABLE" />
43
+ <data android:scheme="nitrometamask" android:host="mmsdk" />
44
+ </intent-filter>
45
+ </activity>
46
+ ```
47
+
48
+ **Important:**
49
+ - Ensure `android:launchMode="singleTask"` is set on your MainActivity (recommended for deep linking)
50
+ - This allows MetaMask to return to your app after the user approves the connection or signature
51
+
52
+ ### iOS Configuration
53
+
54
+ For iOS, the MetaMask SDK handles deep linking automatically. No additional configuration is required in `Info.plist`.
55
+
56
+ ## Usage
57
+
58
+ ```typescript
59
+ import { NitroMetamask } from '@novastera-oss/nitro-metamask';
60
+
61
+ // Optional: Configure dapp URL (only needed if you have a website)
62
+ // If not called, defaults to "https://novastera.com"
63
+ // This URL is ONLY used for SDK validation - the deep link return is handled automatically
64
+ NitroMetamask.configure('https://yourdomain.com'); // Optional
65
+
66
+ // Connect to MetaMask
67
+ const connectResult = await NitroMetamask.connect();
68
+ console.log('Connected:', connectResult.address, 'Chain:', connectResult.chainId);
69
+
70
+ // Sign a message (requires connection first)
71
+ const signature = await NitroMetamask.signMessage('Hello from my app!');
72
+
73
+ // Connect and sign in one call (convenience method)
74
+ // This constructs a JSON message with address, chainID, nonce, and exp
75
+ const nonce = 'random-nonce-123';
76
+ const exp = BigInt(Date.now() + 42000); // 42 seconds from now (use BigInt for timestamp)
77
+ const signature = await NitroMetamask.connectSign(nonce, exp);
78
+ ```
79
+
80
+ ### How Deep Linking Works
81
+
82
+ **Important:** The MetaMask SDK requires a valid HTTP/HTTPS URL for `DappMetadata.url` validation, but this is **separate** from the deep link that returns to your app.
83
+
84
+ **Two separate things:**
85
+ 1. **DappMetadata.url** (optional, configurable):
86
+ - Used only for SDK validation - the SDK checks it's a valid HTTP/HTTPS URL
87
+ - Defaults to `"https://novastera.com"` if not configured
88
+ - You can call `NitroMetamask.configure('https://yourdomain.com')` if you have a website
89
+ - If you don't have a website, the default works fine - it's just for validation
90
+ - [Reference](https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt)
91
+
92
+ 2. **Deep Link Return** (automatic):
93
+ - Automatically detected from your `AndroidManifest.xml` intent filter
94
+ - The SDK reads `<data android:scheme="..." android:host="mmsdk" />` and uses it to return to your app
95
+ - This is what actually makes MetaMask return to your app after operations
96
+ - No configuration needed - it's handled automatically
97
+
98
+ **Summary:**
99
+ - The `configure()` URL is just for SDK validation (you can use the default if you don't have a website)
100
+ - The deep link return is handled automatically via your `AndroidManifest.xml`
101
+ - Your app will return correctly as long as the manifest is configured properly
102
+
23
103
  ## Credits
24
104
 
25
105
  Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module).
@@ -36,7 +36,7 @@ def getExtOrIntegerDefault(name) {
36
36
  }
37
37
 
38
38
  android {
39
- namespace "com.nitrometamask"
39
+ namespace "com.margelo.nitro.nitrometamask"
40
40
 
41
41
  ndkVersion getExtOrDefault("ndkVersion")
42
42
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
@@ -152,6 +152,6 @@ if (isNewArchitectureEnabled()) {
152
152
  react {
153
153
  jsRootDir = file("../src/")
154
154
  libraryName = "NitroMetamask"
155
- codegenJavaPackageName = "com.nitrometamask"
155
+ codegenJavaPackageName = "com.margelo.nitro.nitrometamask"
156
156
  }
157
157
  }
@@ -0,0 +1,409 @@
1
+ package com.margelo.nitro.nitrometamask
2
+
3
+ import android.content.Intent
4
+ import android.content.pm.PackageManager
5
+ import android.net.Uri
6
+ import android.util.Log
7
+ import com.margelo.nitro.core.Promise
8
+ import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
9
+ import com.margelo.nitro.nitrometamask.ConnectResult
10
+ import com.margelo.nitro.nitrometamask.MetamaskContextHolder
11
+ import io.metamask.androidsdk.Ethereum
12
+ import io.metamask.androidsdk.Result
13
+ import io.metamask.androidsdk.DappMetadata
14
+ import io.metamask.androidsdk.SDKOptions
15
+ import io.metamask.androidsdk.EthereumRequest
16
+ import kotlinx.coroutines.suspendCancellableCoroutine
17
+ import kotlin.coroutines.resume
18
+
19
+ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
20
+ // Configurable dapp URL - defaults to novastera.com if not set
21
+ // This is only used for SDK validation - the deep link return is handled via AndroidManifest.xml
22
+ @Volatile
23
+ private var dappUrl: String? = null
24
+
25
+ // Ethereum SDK instance - lazy initialization
26
+ @Volatile
27
+ private var ethereumInstance: Ethereum? = null
28
+
29
+ // Track the URL used when creating the current SDK instance
30
+ @Volatile
31
+ private var lastUsedUrl: String? = null
32
+
33
+ // Get or create Ethereum SDK instance
34
+ // Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
35
+ // The SDK automatically detects and uses the deep link from AndroidManifest.xml
36
+ // Reference: https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt
37
+ private val ethereum: Ethereum
38
+ get() {
39
+ val currentUrl = dappUrl ?: "https://novastera.com"
40
+ val existing = ethereumInstance
41
+ val lastUrl = lastUsedUrl
42
+
43
+ // If not initialized or URL changed, recreate SDK
44
+ if (existing == null || lastUrl != currentUrl) {
45
+ synchronized(this) {
46
+ // Double-check after acquiring lock
47
+ val existingAfterLock = ethereumInstance
48
+ val lastUrlAfterLock = lastUsedUrl
49
+ if (existingAfterLock == null || lastUrlAfterLock != currentUrl) {
50
+ val context = MetamaskContextHolder.get()
51
+
52
+ // DappMetadata.url must be a valid HTTP/HTTPS URL for SDK validation
53
+ // This is separate from the deep link scheme which is auto-detected from AndroidManifest.xml
54
+ // The deep link return to your app is handled automatically via the manifest
55
+ val dappMetadata = DappMetadata(
56
+ name = "Nitro MetaMask Connector",
57
+ url = currentUrl
58
+ )
59
+ val sdkOptions = SDKOptions(
60
+ infuraAPIKey = null,
61
+ readonlyRPCMap = null
62
+ )
63
+
64
+ ethereumInstance = Ethereum(context, dappMetadata, sdkOptions)
65
+ lastUsedUrl = currentUrl
66
+ Log.d("NitroMetamask", "Ethereum SDK initialized with DappMetadata.url=$currentUrl. Deep link auto-detected from AndroidManifest.xml")
67
+ }
68
+ }
69
+ }
70
+ return ethereumInstance!!
71
+ }
72
+
73
+
74
+ override fun configure(dappUrl: String?) {
75
+ synchronized(this) {
76
+ val urlToUse = dappUrl ?: "https://novastera.com"
77
+ if (this.dappUrl != urlToUse) {
78
+ this.dappUrl = urlToUse
79
+ // Invalidate existing instance to force recreation with new URL
80
+ ethereumInstance = null
81
+ lastUsedUrl = null
82
+ Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse. Deep link return is handled automatically via AndroidManifest.xml")
83
+ }
84
+ }
85
+ }
86
+
87
+ override fun connect(): Promise<ConnectResult> {
88
+ // Use Promise.async with coroutines for best practice in Nitro modules
89
+ // Reference: https://nitro.margelo.com/docs/types/promises
90
+ return Promise.async {
91
+ // Convert callback-based connect() to suspend function using suspendCancellableCoroutine
92
+ // This handles cancellation properly when JS GC disposes the promise
93
+ val result = suspendCancellableCoroutine<Result> { continuation ->
94
+ ethereum.connect { callbackResult ->
95
+ if (continuation.isActive) {
96
+ continuation.resume(callbackResult)
97
+ }
98
+ }
99
+ }
100
+
101
+ when (result) {
102
+ is Result.Success.Item -> {
103
+ // After successful connection, get account info from SDK
104
+ val address = ethereum.selectedAddress
105
+ ?: throw IllegalStateException("MetaMask SDK returned no address after connection")
106
+ val chainIdString = ethereum.chainId
107
+ ?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
108
+
109
+ // Parse chainId from hex string (e.g., "0x1") or decimal string to number
110
+ // Nitro requires chainId to be Double (number in TS maps to Double in Kotlin)
111
+ val chainId = try {
112
+ val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
113
+ chainIdString.substring(2).toLong(16).toInt()
114
+ } else {
115
+ chainIdString.toLong().toInt()
116
+ }
117
+ chainIdInt.toDouble()
118
+ } catch (e: NumberFormatException) {
119
+ throw IllegalStateException("Invalid chainId format: $chainIdString")
120
+ }
121
+
122
+ ConnectResult(
123
+ address = address,
124
+ chainId = chainId
125
+ )
126
+ }
127
+ is Result.Success.ItemMap -> {
128
+ // Handle ItemMap case (shouldn't happen for connect, but make exhaustive)
129
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask connect")
130
+ }
131
+ is Result.Success.Items -> {
132
+ // Handle Items case (shouldn't happen for connect, but make exhaustive)
133
+ throw IllegalStateException("Unexpected Items result from MetaMask connect")
134
+ }
135
+ is Result.Error -> {
136
+ // Result.Error contains the error directly
137
+ val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connection failed"
138
+ throw Exception(errorMessage)
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ override fun signMessage(message: String): Promise<String> {
145
+ // Use Promise.async with coroutines for best practice in Nitro modules
146
+ // Reference: https://nitro.margelo.com/docs/types/promises
147
+ return Promise.async {
148
+ // Verify connection state before attempting to sign
149
+ // MetaMask SDK requires an active connection to sign messages
150
+ val address = ethereum.selectedAddress
151
+ if (address.isNullOrEmpty()) {
152
+ throw IllegalStateException("No connected account. Please call connect() first to establish a connection with MetaMask.")
153
+ }
154
+
155
+ // Create EthereumRequest for personal_sign
156
+ // Based on MetaMask Android SDK docs: params are [account, message]
157
+ // Reference: https://github.com/MetaMask/metamask-android-sdk
158
+ // EthereumRequest constructor expects method as String
159
+ val request = EthereumRequest(
160
+ method = "personal_sign",
161
+ params = listOf(address, message)
162
+ )
163
+
164
+ // Convert callback-based sendRequest() to suspend function
165
+ // The SDK will automatically handle deep link return to the app
166
+ val result = suspendCancellableCoroutine<Result> { continuation ->
167
+ ethereum.sendRequest(request) { callbackResult ->
168
+ if (continuation.isActive) {
169
+ continuation.resume(callbackResult)
170
+ }
171
+ }
172
+ }
173
+
174
+ when (result) {
175
+ is Result.Success.Item -> {
176
+ // Extract signature from response
177
+ // The signature should be a hex-encoded string (0x-prefixed)
178
+ val signature = result.value as? String
179
+ ?: throw Exception("Invalid signature response format")
180
+
181
+ // Bring app to foreground after receiving the result
182
+ // This must be done on the main thread
183
+ val context = MetamaskContextHolder.get()
184
+ android.os.Handler(android.os.Looper.getMainLooper()).post {
185
+ try {
186
+ val intent = Intent(Intent.ACTION_VIEW).apply {
187
+ data = Uri.parse("nitrometamask://mmsdk")
188
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
189
+ setPackage(context.packageName)
190
+ }
191
+ context.startActivity(intent)
192
+ Log.d("NitroMetamask", "Brought app to foreground after signing")
193
+ } catch (e: Exception) {
194
+ Log.w("NitroMetamask", "Failed to bring app to foreground: ${e.message}")
195
+ }
196
+ }
197
+
198
+ signature
199
+ }
200
+ is Result.Success.ItemMap -> {
201
+ // Handle ItemMap case (shouldn't happen for signMessage, but make exhaustive)
202
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask signMessage")
203
+ }
204
+ is Result.Success.Items -> {
205
+ // Handle Items case (shouldn't happen for signMessage, but make exhaustive)
206
+ throw IllegalStateException("Unexpected Items result from MetaMask signMessage")
207
+ }
208
+ is Result.Error -> {
209
+ // Result.Error contains the error directly
210
+ val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask signing failed"
211
+ throw Exception(errorMessage)
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ override fun connectSign(nonce: String, exp: Long): Promise<String> {
218
+ // Use Promise.async with coroutines for best practice in Nitro modules
219
+ // Reference: https://nitro.margelo.com/docs/types/promises
220
+ // Based on MetaMask Android SDK: ethereum.connectSign(message)
221
+ // Reference: https://github.com/MetaMask/metamask-android-sdk
222
+ // This convenience method connects (if needed) and signs a JSON message
223
+ return Promise.async {
224
+ // First, ensure we're connected to get address and chainId
225
+ val address = ethereum.selectedAddress
226
+ val chainIdString = ethereum.chainId
227
+
228
+ // If not connected, connect first
229
+ if (address.isNullOrEmpty() || chainIdString.isNullOrEmpty()) {
230
+ val connectResult = suspendCancellableCoroutine<Result> { continuation ->
231
+ ethereum.connect { callbackResult ->
232
+ if (continuation.isActive) {
233
+ continuation.resume(callbackResult)
234
+ }
235
+ }
236
+ }
237
+
238
+ when (connectResult) {
239
+ is Result.Success.Item -> {
240
+ // Connection successful, get address and chainId
241
+ val connectedAddress = ethereum.selectedAddress
242
+ ?: throw IllegalStateException("MetaMask SDK returned no address after connection")
243
+ val connectedChainId = ethereum.chainId
244
+ ?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
245
+
246
+ // Parse chainId to number
247
+ val chainId = try {
248
+ val chainIdInt = if (connectedChainId.startsWith("0x") || connectedChainId.startsWith("0X")) {
249
+ connectedChainId.substring(2).toLong(16).toInt()
250
+ } else {
251
+ connectedChainId.toLong().toInt()
252
+ }
253
+ chainIdInt
254
+ } catch (e: NumberFormatException) {
255
+ throw IllegalStateException("Invalid chainId format: $connectedChainId")
256
+ }
257
+
258
+ // Construct JSON message
259
+ val message = org.json.JSONObject().apply {
260
+ put("address", connectedAddress)
261
+ put("chainID", chainId)
262
+ put("nonce", nonce)
263
+ put("exp", exp)
264
+ }.toString()
265
+
266
+ // Sign the message using sendRequest with personal_sign (same as signMessage)
267
+ // This ensures proper deep link handling for returning to the app
268
+ val request = EthereumRequest(
269
+ method = "personal_sign",
270
+ params = listOf(connectedAddress, message)
271
+ )
272
+
273
+ val signResult = suspendCancellableCoroutine<Result> { signContinuation ->
274
+ ethereum.sendRequest(request) { callbackResult ->
275
+ if (signContinuation.isActive) {
276
+ signContinuation.resume(callbackResult)
277
+ }
278
+ }
279
+ }
280
+
281
+ when (signResult) {
282
+ is Result.Success.Item -> {
283
+ val signature = signResult.value as? String
284
+ ?: throw Exception("Invalid signature response format")
285
+
286
+ // Bring app to foreground after receiving the result
287
+ // This must be done on the main thread
288
+ val context = MetamaskContextHolder.get()
289
+ android.os.Handler(android.os.Looper.getMainLooper()).post {
290
+ try {
291
+ val intent = Intent(Intent.ACTION_VIEW).apply {
292
+ data = Uri.parse("nitrometamask://mmsdk")
293
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
294
+ setPackage(context.packageName)
295
+ }
296
+ context.startActivity(intent)
297
+ Log.d("NitroMetamask", "Brought app to foreground after connectSign")
298
+ } catch (e: Exception) {
299
+ Log.w("NitroMetamask", "Failed to bring app to foreground: ${e.message}")
300
+ }
301
+ }
302
+
303
+ signature
304
+ }
305
+ is Result.Success.ItemMap -> {
306
+ // Handle ItemMap case (shouldn't happen for personal_sign, but make exhaustive)
307
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
308
+ }
309
+ is Result.Success.Items -> {
310
+ // Handle Items case (shouldn't happen for personal_sign, but make exhaustive)
311
+ throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
312
+ }
313
+ is Result.Error -> {
314
+ val errorMessage = signResult.error?.message ?: signResult.error?.toString() ?: "MetaMask signing failed"
315
+ throw Exception(errorMessage)
316
+ }
317
+ }
318
+ }
319
+ is Result.Error -> {
320
+ val errorMessage = connectResult.error?.message ?: connectResult.error?.toString() ?: "MetaMask connection failed"
321
+ throw Exception(errorMessage)
322
+ }
323
+ else -> {
324
+ throw IllegalStateException("Unexpected result type from MetaMask connect")
325
+ }
326
+ }
327
+ } else {
328
+ // Already connected, construct message and sign
329
+ val chainId = try {
330
+ val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
331
+ chainIdString.substring(2).toLong(16).toInt()
332
+ } else {
333
+ chainIdString.toLong().toInt()
334
+ }
335
+ chainIdInt
336
+ } catch (e: NumberFormatException) {
337
+ throw IllegalStateException("Invalid chainId format: $chainIdString")
338
+ }
339
+
340
+ // Construct JSON message
341
+ val message = org.json.JSONObject().apply {
342
+ put("address", address)
343
+ put("chainID", chainId)
344
+ put("nonce", nonce)
345
+ put("exp", exp)
346
+ }.toString()
347
+
348
+ // Sign the message using sendRequest with personal_sign (same as signMessage)
349
+ // This ensures proper deep link handling for returning to the app
350
+ val request = EthereumRequest(
351
+ method = "personal_sign",
352
+ params = listOf(address, message)
353
+ )
354
+
355
+ // The SDK will automatically handle deep link return to the app
356
+ val signResult = suspendCancellableCoroutine<Result> { continuation ->
357
+ Log.d("NitroMetamask", "connectSign: Sending personal_sign request")
358
+ ethereum.sendRequest(request) { callbackResult ->
359
+ Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
360
+ if (continuation.isActive) {
361
+ continuation.resume(callbackResult)
362
+ } else {
363
+ Log.w("NitroMetamask", "connectSign: Continuation not active, ignoring callback")
364
+ }
365
+ }
366
+ }
367
+
368
+ Log.d("NitroMetamask", "connectSign: Processing signResult")
369
+ when (signResult) {
370
+ is Result.Success.Item -> {
371
+ val signature = signResult.value as? String
372
+ ?: throw Exception("Invalid signature response format")
373
+
374
+ // Bring app to foreground after receiving the result
375
+ // This must be done on the main thread
376
+ val context = MetamaskContextHolder.get()
377
+ android.os.Handler(android.os.Looper.getMainLooper()).post {
378
+ try {
379
+ val intent = Intent(Intent.ACTION_VIEW).apply {
380
+ data = Uri.parse("nitrometamask://mmsdk")
381
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
382
+ setPackage(context.packageName)
383
+ }
384
+ context.startActivity(intent)
385
+ Log.d("NitroMetamask", "Brought app to foreground after connectSign (already connected)")
386
+ } catch (e: Exception) {
387
+ Log.w("NitroMetamask", "Failed to bring app to foreground: ${e.message}")
388
+ }
389
+ }
390
+
391
+ signature
392
+ }
393
+ is Result.Success.ItemMap -> {
394
+ // Handle ItemMap case (shouldn't happen for personal_sign, but make exhaustive)
395
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
396
+ }
397
+ is Result.Success.Items -> {
398
+ // Handle Items case (shouldn't happen for personal_sign, but make exhaustive)
399
+ throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
400
+ }
401
+ is Result.Error -> {
402
+ val errorMessage = signResult.error?.message ?: signResult.error?.toString() ?: "MetaMask signing failed"
403
+ throw Exception(errorMessage)
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
@@ -1,4 +1,4 @@
1
- package com.nitrometamask
1
+ package com.margelo.nitro.nitrometamask
2
2
 
3
3
  import android.content.Context
4
4
 
@@ -1,4 +1,4 @@
1
- package com.nitrometamask
1
+ package com.margelo.nitro.nitrometamask
2
2
 
3
3
  import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
@@ -4,6 +4,17 @@ import Foundation
4
4
 
5
5
  final class HybridNitroMetamask: HybridNitroMetamaskSpec {
6
6
  private let sdk = MetaMaskSDK.shared
7
+
8
+ // Configurable dapp URL - stored for consistency with Android
9
+ // iOS SDK handles deep linking automatically via Info.plist
10
+ private var dappUrl: String? = nil
11
+
12
+ func configure(dappUrl: String?) {
13
+ // iOS SDK handles deep linking automatically via Info.plist
14
+ // Store the URL for consistency with Android implementation
15
+ self.dappUrl = dappUrl
16
+ NSLog("NitroMetamask: configure: Dapp URL set to \(dappUrl ?? "default"). Deep link handled automatically via Info.plist")
17
+ }
7
18
 
8
19
  func connect() -> Promise<ConnectResult> {
9
20
  // Use Promise.async with Swift async/await for best practice in Nitro modules
@@ -93,4 +104,138 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
93
104
  }
94
105
  }
95
106
  }
107
+
108
+ func connectSign(nonce: String, exp: Int64) -> Promise<String> {
109
+ // Use Promise.async with Swift async/await for best practice in Nitro modules
110
+ // Reference: https://nitro.margelo.com/docs/types/promises
111
+ // Based on MetaMask iOS SDK: connect and sign in one call
112
+ // Reference: https://github.com/MetaMask/metamask-ios-sdk
113
+ return Promise.async {
114
+ // First, ensure we're connected
115
+ var address = self.sdk.account
116
+ var chainIdHex = self.sdk.chainId
117
+
118
+ // If not connected, connect first
119
+ if address == nil || address?.isEmpty == true || chainIdHex == nil || chainIdHex?.isEmpty == true {
120
+ let connectResult = try await self.sdk.connect()
121
+
122
+ switch connectResult {
123
+ case .success:
124
+ address = self.sdk.account
125
+ chainIdHex = self.sdk.chainId
126
+
127
+ guard let account = address, !account.isEmpty else {
128
+ throw NSError(
129
+ domain: "MetamaskConnector",
130
+ code: -1,
131
+ userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no address after connection"]
132
+ )
133
+ }
134
+
135
+ guard let chainId = chainIdHex, !chainId.isEmpty,
136
+ let chainIdInt = Int(chainId.replacingOccurrences(of: "0x", with: ""), radix: 16) else {
137
+ throw NSError(
138
+ domain: "MetamaskConnector",
139
+ code: -1,
140
+ userInfo: [NSLocalizedDescriptionKey: "Invalid chainId format"]
141
+ )
142
+ }
143
+
144
+ // Construct JSON message
145
+ let messageDict: [String: Any] = [
146
+ "address": account,
147
+ "chainID": chainIdInt,
148
+ "nonce": nonce,
149
+ "exp": exp
150
+ ]
151
+
152
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: messageDict),
153
+ let message = String(data: jsonData, encoding: .utf8) else {
154
+ throw NSError(
155
+ domain: "MetamaskConnector",
156
+ code: -1,
157
+ userInfo: [NSLocalizedDescriptionKey: "Failed to create JSON message"]
158
+ )
159
+ }
160
+
161
+ // Create EthereumRequest for personal_sign
162
+ let params: [String] = [account, message]
163
+ let request = EthereumRequest(
164
+ method: .personalSign,
165
+ params: params
166
+ )
167
+
168
+ // Make the request using the SDK's async request method
169
+ let result = try await self.sdk.request(request)
170
+
171
+ // Extract signature from response
172
+ if let signature = result as? String {
173
+ return signature
174
+ } else if let dict = result as? [String: Any], let sig = dict["signature"] as? String ?? dict["result"] as? String {
175
+ return sig
176
+ } else {
177
+ throw NSError(
178
+ domain: "MetamaskConnector",
179
+ code: -1,
180
+ userInfo: [NSLocalizedDescriptionKey: "Invalid signature response format"]
181
+ )
182
+ }
183
+
184
+ case .failure(let error):
185
+ throw error
186
+ }
187
+ } else {
188
+ // Already connected, construct message and sign
189
+ guard let account = address, !account.isEmpty,
190
+ let chainId = chainIdHex, !chainId.isEmpty,
191
+ let chainIdInt = Int(chainId.replacingOccurrences(of: "0x", with: ""), radix: 16) else {
192
+ throw NSError(
193
+ domain: "MetamaskConnector",
194
+ code: -1,
195
+ userInfo: [NSLocalizedDescriptionKey: "Invalid connection state"]
196
+ )
197
+ }
198
+
199
+ // Construct JSON message
200
+ let messageDict: [String: Any] = [
201
+ "address": account,
202
+ "chainID": chainIdInt,
203
+ "nonce": nonce,
204
+ "exp": Int64(exp)
205
+ ]
206
+
207
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: messageDict),
208
+ let message = String(data: jsonData, encoding: .utf8) else {
209
+ throw NSError(
210
+ domain: "MetamaskConnector",
211
+ code: -1,
212
+ userInfo: [NSLocalizedDescriptionKey: "Failed to create JSON message"]
213
+ )
214
+ }
215
+
216
+ // Create EthereumRequest for personal_sign
217
+ let params: [String] = [account, message]
218
+ let request = EthereumRequest(
219
+ method: .personalSign,
220
+ params: params
221
+ )
222
+
223
+ // Make the request using the SDK's async request method
224
+ let result = try await self.sdk.request(request)
225
+
226
+ // Extract signature from response
227
+ if let signature = result as? String {
228
+ return signature
229
+ } else if let dict = result as? [String: Any], let sig = dict["signature"] as? String ?? dict["result"] as? String {
230
+ return sig
231
+ } else {
232
+ throw NSError(
233
+ domain: "MetamaskConnector",
234
+ code: -1,
235
+ userInfo: [NSLocalizedDescriptionKey: "Invalid signature response format"]
236
+ )
237
+ }
238
+ }
239
+ }
240
+ }
96
241
  }
@@ -7,7 +7,17 @@ export interface NitroMetamask extends HybridObject<{
7
7
  ios: 'swift';
8
8
  android: 'kotlin';
9
9
  }> {
10
+ /**
11
+ * Configure the dapp URL for MetaMask SDK validation.
12
+ * This URL is only used for SDK validation - the deep link return is handled automatically via AndroidManifest.xml.
13
+ *
14
+ * @param dappUrl - A valid HTTP/HTTPS URL (e.g., "https://yourdomain.com").
15
+ * If not provided, defaults to "https://novastera.com".
16
+ * This is separate from the deep link scheme which is auto-detected from your manifest.
17
+ */
18
+ configure(dappUrl?: string): void;
10
19
  connect(): Promise<ConnectResult>;
11
20
  signMessage(message: string): Promise<string>;
21
+ connectSign(nonce: string, exp: bigint): Promise<string>;
12
22
  }
13
23
  //# sourceMappingURL=nitro-metamask.nitro.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nitro-metamask.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/nitro-metamask.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACtF,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAA;IACjC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC9C"}
1
+ {"version":3,"file":"nitro-metamask.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/nitro-metamask.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACtF;;;;;;;OAOG;IACH,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAA;IACjC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7C,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACzD"}
@@ -15,6 +15,7 @@ namespace margelo::nitro::nitrometamask { struct ConnectResult; }
15
15
  #include <NitroModules/JPromise.hpp>
16
16
  #include "JConnectResult.hpp"
17
17
  #include <string>
18
+ #include <optional>
18
19
 
19
20
  namespace margelo::nitro::nitrometamask {
20
21
 
@@ -48,6 +49,10 @@ namespace margelo::nitro::nitrometamask {
48
49
 
49
50
 
50
51
  // Methods
52
+ void JHybridNitroMetamaskSpec::configure(const std::optional<std::string>& dappUrl) {
53
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* dappUrl */)>("configure");
54
+ method(_javaPart, dappUrl.has_value() ? jni::make_jstring(dappUrl.value()) : nullptr);
55
+ }
51
56
  std::shared_ptr<Promise<ConnectResult>> JHybridNitroMetamaskSpec::connect() {
52
57
  static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("connect");
53
58
  auto __result = method(_javaPart);
@@ -80,5 +85,21 @@ namespace margelo::nitro::nitrometamask {
80
85
  return __promise;
81
86
  }();
82
87
  }
88
+ std::shared_ptr<Promise<std::string>> JHybridNitroMetamaskSpec::connectSign(const std::string& nonce, int64_t exp) {
89
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* nonce */, int64_t /* exp */)>("connectSign");
90
+ auto __result = method(_javaPart, jni::make_jstring(nonce), exp);
91
+ return [&]() {
92
+ auto __promise = Promise<std::string>::create();
93
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
94
+ auto __result = jni::static_ref_cast<jni::JString>(__boxedResult);
95
+ __promise->resolve(__result->toStdString());
96
+ });
97
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
98
+ jni::JniException __jniError(__throwable);
99
+ __promise->reject(std::make_exception_ptr(__jniError));
100
+ });
101
+ return __promise;
102
+ }();
103
+ }
83
104
 
84
105
  } // namespace margelo::nitro::nitrometamask
@@ -54,8 +54,10 @@ namespace margelo::nitro::nitrometamask {
54
54
 
55
55
  public:
56
56
  // Methods
57
+ void configure(const std::optional<std::string>& dappUrl) override;
57
58
  std::shared_ptr<Promise<ConnectResult>> connect() override;
58
59
  std::shared_ptr<Promise<std::string>> signMessage(const std::string& message) override;
60
+ std::shared_ptr<Promise<std::string>> connectSign(const std::string& nonce, int64_t exp) override;
59
61
 
60
62
  private:
61
63
  friend HybridBase;
@@ -46,6 +46,10 @@ abstract class HybridNitroMetamaskSpec: HybridObject() {
46
46
 
47
47
 
48
48
  // Methods
49
+ @DoNotStrip
50
+ @Keep
51
+ abstract fun configure(dappUrl: String?): Unit
52
+
49
53
  @DoNotStrip
50
54
  @Keep
51
55
  abstract fun connect(): Promise<ConnectResult>
@@ -53,6 +57,10 @@ abstract class HybridNitroMetamaskSpec: HybridObject() {
53
57
  @DoNotStrip
54
58
  @Keep
55
59
  abstract fun signMessage(message: String): Promise<String>
60
+
61
+ @DoNotStrip
62
+ @Keep
63
+ abstract fun connectSign(nonce: String, exp: Long): Promise<String>
56
64
 
57
65
  private external fun initHybrid(): HybridData
58
66
 
@@ -26,6 +26,7 @@ namespace NitroMetamask { class HybridNitroMetamaskSpec_cxx; }
26
26
  #include <exception>
27
27
  #include <functional>
28
28
  #include <memory>
29
+ #include <optional>
29
30
  #include <string>
30
31
 
31
32
  /**
@@ -34,6 +35,21 @@ namespace NitroMetamask { class HybridNitroMetamaskSpec_cxx; }
34
35
  */
35
36
  namespace margelo::nitro::nitrometamask::bridge::swift {
36
37
 
38
+ // pragma MARK: std::optional<std::string>
39
+ /**
40
+ * Specialized version of `std::optional<std::string>`.
41
+ */
42
+ using std__optional_std__string_ = std::optional<std::string>;
43
+ inline std::optional<std::string> create_std__optional_std__string_(const std::string& value) noexcept {
44
+ return std::optional<std::string>(value);
45
+ }
46
+ inline bool has_value_std__optional_std__string_(const std::optional<std::string>& optional) noexcept {
47
+ return optional.has_value();
48
+ }
49
+ inline std::string get_std__optional_std__string_(const std::optional<std::string>& optional) noexcept {
50
+ return *optional;
51
+ }
52
+
37
53
  // pragma MARK: std::shared_ptr<Promise<ConnectResult>>
38
54
  /**
39
55
  * Specialized version of `std::shared_ptr<Promise<ConnectResult>>`.
@@ -136,6 +152,15 @@ namespace margelo::nitro::nitrometamask::bridge::swift {
136
152
  using std__weak_ptr_HybridNitroMetamaskSpec_ = std::weak_ptr<HybridNitroMetamaskSpec>;
137
153
  inline std__weak_ptr_HybridNitroMetamaskSpec_ weakify_std__shared_ptr_HybridNitroMetamaskSpec_(const std::shared_ptr<HybridNitroMetamaskSpec>& strong) noexcept { return strong; }
138
154
 
155
+ // pragma MARK: Result<void>
156
+ using Result_void_ = Result<void>;
157
+ inline Result_void_ create_Result_void_() noexcept {
158
+ return Result<void>::withValue();
159
+ }
160
+ inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept {
161
+ return Result<void>::withError(error);
162
+ }
163
+
139
164
  // pragma MARK: Result<std::shared_ptr<Promise<ConnectResult>>>
140
165
  using Result_std__shared_ptr_Promise_ConnectResult___ = Result<std::shared_ptr<Promise<ConnectResult>>>;
141
166
  inline Result_std__shared_ptr_Promise_ConnectResult___ create_Result_std__shared_ptr_Promise_ConnectResult___(const std::shared_ptr<Promise<ConnectResult>>& value) noexcept {
@@ -20,6 +20,7 @@ namespace margelo::nitro::nitrometamask { class HybridNitroMetamaskSpec; }
20
20
  #include <NitroModules/Result.hpp>
21
21
  #include <exception>
22
22
  #include <memory>
23
+ #include <optional>
23
24
  #include <string>
24
25
 
25
26
  // C++ helpers for Swift
@@ -15,9 +15,10 @@ namespace NitroMetamask { class HybridNitroMetamaskSpec_cxx; }
15
15
  // Forward declaration of `ConnectResult` to properly resolve imports.
16
16
  namespace margelo::nitro::nitrometamask { struct ConnectResult; }
17
17
 
18
+ #include <string>
19
+ #include <optional>
18
20
  #include "ConnectResult.hpp"
19
21
  #include <NitroModules/Promise.hpp>
20
- #include <string>
21
22
 
22
23
  #include "NitroMetamask-Swift-Cxx-Umbrella.hpp"
23
24
 
@@ -63,6 +64,12 @@ namespace margelo::nitro::nitrometamask {
63
64
 
64
65
  public:
65
66
  // Methods
67
+ inline void configure(const std::optional<std::string>& dappUrl) override {
68
+ auto __result = _swiftPart.configure(dappUrl);
69
+ if (__result.hasError()) [[unlikely]] {
70
+ std::rethrow_exception(__result.error());
71
+ }
72
+ }
66
73
  inline std::shared_ptr<Promise<ConnectResult>> connect() override {
67
74
  auto __result = _swiftPart.connect();
68
75
  if (__result.hasError()) [[unlikely]] {
@@ -79,6 +86,14 @@ namespace margelo::nitro::nitrometamask {
79
86
  auto __value = std::move(__result.value());
80
87
  return __value;
81
88
  }
89
+ inline std::shared_ptr<Promise<std::string>> connectSign(const std::string& nonce, int64_t exp) override {
90
+ auto __result = _swiftPart.connectSign(nonce, std::forward<decltype(exp)>(exp));
91
+ if (__result.hasError()) [[unlikely]] {
92
+ std::rethrow_exception(__result.error());
93
+ }
94
+ auto __value = std::move(__result.value());
95
+ return __value;
96
+ }
82
97
 
83
98
  private:
84
99
  NitroMetamask::HybridNitroMetamaskSpec_cxx _swiftPart;
@@ -14,8 +14,10 @@ public protocol HybridNitroMetamaskSpec_protocol: HybridObject {
14
14
 
15
15
 
16
16
  // Methods
17
+ func configure(dappUrl: String?) throws -> Void
17
18
  func connect() throws -> Promise<ConnectResult>
18
19
  func signMessage(message: String) throws -> Promise<String>
20
+ func connectSign(nonce: String, exp: Int64) throws -> Promise<String>
19
21
  }
20
22
 
21
23
  public extension HybridNitroMetamaskSpec_protocol {
@@ -117,6 +117,24 @@ open class HybridNitroMetamaskSpec_cxx {
117
117
 
118
118
 
119
119
  // Methods
120
+ @inline(__always)
121
+ public final func configure(dappUrl: bridge.std__optional_std__string_) -> bridge.Result_void_ {
122
+ do {
123
+ try self.__implementation.configure(dappUrl: { () -> String? in
124
+ if bridge.has_value_std__optional_std__string_(dappUrl) {
125
+ let __unwrapped = bridge.get_std__optional_std__string_(dappUrl)
126
+ return String(__unwrapped)
127
+ } else {
128
+ return nil
129
+ }
130
+ }())
131
+ return bridge.create_Result_void_()
132
+ } catch (let __error) {
133
+ let __exceptionPtr = __error.toCpp()
134
+ return bridge.create_Result_void_(__exceptionPtr)
135
+ }
136
+ }
137
+
120
138
  @inline(__always)
121
139
  public final func connect() -> bridge.Result_std__shared_ptr_Promise_ConnectResult___ {
122
140
  do {
@@ -154,4 +172,23 @@ open class HybridNitroMetamaskSpec_cxx {
154
172
  return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr)
155
173
  }
156
174
  }
175
+
176
+ @inline(__always)
177
+ public final func connectSign(nonce: std.string, exp: Int64) -> bridge.Result_std__shared_ptr_Promise_std__string___ {
178
+ do {
179
+ let __result = try self.__implementation.connectSign(nonce: String(nonce), exp: exp)
180
+ let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in
181
+ let __promise = bridge.create_std__shared_ptr_Promise_std__string__()
182
+ let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise)
183
+ __result
184
+ .then({ __result in __promiseHolder.resolve(std.string(__result)) })
185
+ .catch({ __error in __promiseHolder.reject(__error.toCpp()) })
186
+ return __promise
187
+ }()
188
+ return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp)
189
+ } catch (let __error) {
190
+ let __exceptionPtr = __error.toCpp()
191
+ return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr)
192
+ }
193
+ }
157
194
  }
@@ -14,8 +14,10 @@ namespace margelo::nitro::nitrometamask {
14
14
  HybridObject::loadHybridMethods();
15
15
  // load custom methods/properties
16
16
  registerHybrids(this, [](Prototype& prototype) {
17
+ prototype.registerHybridMethod("configure", &HybridNitroMetamaskSpec::configure);
17
18
  prototype.registerHybridMethod("connect", &HybridNitroMetamaskSpec::connect);
18
19
  prototype.registerHybridMethod("signMessage", &HybridNitroMetamaskSpec::signMessage);
20
+ prototype.registerHybridMethod("connectSign", &HybridNitroMetamaskSpec::connectSign);
19
21
  });
20
22
  }
21
23
 
@@ -16,9 +16,10 @@
16
16
  // Forward declaration of `ConnectResult` to properly resolve imports.
17
17
  namespace margelo::nitro::nitrometamask { struct ConnectResult; }
18
18
 
19
+ #include <string>
20
+ #include <optional>
19
21
  #include "ConnectResult.hpp"
20
22
  #include <NitroModules/Promise.hpp>
21
- #include <string>
22
23
 
23
24
  namespace margelo::nitro::nitrometamask {
24
25
 
@@ -51,8 +52,10 @@ namespace margelo::nitro::nitrometamask {
51
52
 
52
53
  public:
53
54
  // Methods
55
+ virtual void configure(const std::optional<std::string>& dappUrl) = 0;
54
56
  virtual std::shared_ptr<Promise<ConnectResult>> connect() = 0;
55
57
  virtual std::shared_ptr<Promise<std::string>> signMessage(const std::string& message) = 0;
58
+ virtual std::shared_ptr<Promise<std::string>> connectSign(const std::string& nonce, int64_t exp) = 0;
56
59
 
57
60
  protected:
58
61
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novastera-oss/nitro-metamask",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Novastera metamask authentication with native mobile libraries",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -12,7 +12,7 @@
12
12
  "clean": "git clean -dfX",
13
13
  "release": "semantic-release",
14
14
  "build": "npm run typecheck && bob build",
15
- "codegen": "nitrogen --logLevel=\"debug\" && npm run build && node post-script.js"
15
+ "codegen": "nitrogen --logLevel=\"debug\" && npm run build"
16
16
  },
17
17
  "keywords": [
18
18
  "react-native",
@@ -11,7 +11,7 @@ module.exports = {
11
11
  * @type {import('@react-native-community/cli-types').AndroidDependencyParams}
12
12
  */
13
13
  android: {
14
- packageImportPath: 'import com.nitrometamask.NitroMetamaskPackage;',
14
+ packageImportPath: 'import com.margelo.nitro.nitrometamask.NitroMetamaskPackage;',
15
15
  packageInstance: 'new NitroMetamaskPackage()',
16
16
  },
17
17
  },
@@ -6,6 +6,16 @@ export interface ConnectResult {
6
6
  }
7
7
 
8
8
  export interface NitroMetamask extends HybridObject<{ ios: 'swift', android: 'kotlin' }> {
9
+ /**
10
+ * Configure the dapp URL for MetaMask SDK validation.
11
+ * This URL is only used for SDK validation - the deep link return is handled automatically via AndroidManifest.xml.
12
+ *
13
+ * @param dappUrl - A valid HTTP/HTTPS URL (e.g., "https://yourdomain.com").
14
+ * If not provided, defaults to "https://novastera.com".
15
+ * This is separate from the deep link scheme which is auto-detected from your manifest.
16
+ */
17
+ configure(dappUrl?: string): void
9
18
  connect(): Promise<ConnectResult>
10
19
  signMessage(message: string): Promise<string>
20
+ connectSign(nonce: string, exp: bigint): Promise<string>
11
21
  }
@@ -1,146 +0,0 @@
1
- package com.nitrometamask
2
-
3
- import com.margelo.nitro.core.Promise
4
- import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
5
- import com.margelo.nitro.nitrometamask.ConnectResult
6
- import io.metamask.androidsdk.Ethereum
7
- import io.metamask.androidsdk.Result
8
- import io.metamask.androidsdk.DappMetadata
9
- import io.metamask.androidsdk.SDKOptions
10
- import io.metamask.androidsdk.EthereumRequest
11
- import kotlinx.coroutines.suspendCancellableCoroutine
12
- import kotlin.coroutines.resume
13
-
14
- class HybridNitroMetamask : HybridNitroMetamaskSpec() {
15
- // Initialize Ethereum SDK with Context, DappMetadata, and SDKOptions
16
- // Based on: https://github.com/MetaMask/metamask-android-sdk
17
- // Using MetamaskContextHolder for Context access (Nitro doesn't provide Context APIs)
18
- // This pattern matches how other Nitro modules handle Context (VisionCamera, MMKV, etc.)
19
- private val ethereum: Ethereum by lazy {
20
- val context = MetamaskContextHolder.get()
21
-
22
- val dappMetadata = DappMetadata(
23
- name = "Nitro MetaMask Connector",
24
- url = "https://novastera.com"
25
- )
26
- // SDKOptions constructor requires infuraAPIKey and readonlyRPCMap parameters
27
- // They can be null for basic usage without Infura or custom RPC
28
- val sdkOptions = SDKOptions(
29
- infuraAPIKey = null,
30
- readonlyRPCMap = null
31
- )
32
-
33
- Ethereum(context, dappMetadata, sdkOptions)
34
- }
35
-
36
- override fun connect(): Promise<ConnectResult> {
37
- // Use Promise.async with coroutines for best practice in Nitro modules
38
- // Reference: https://nitro.margelo.com/docs/types/promises
39
- return Promise.async {
40
- // Convert callback-based connect() to suspend function using suspendCancellableCoroutine
41
- // This handles cancellation properly when JS GC disposes the promise
42
- val result = suspendCancellableCoroutine<Result> { continuation ->
43
- ethereum.connect { callbackResult ->
44
- if (continuation.isActive) {
45
- continuation.resume(callbackResult)
46
- }
47
- }
48
- }
49
-
50
- when (result) {
51
- is Result.Success.Item -> {
52
- // After successful connection, get account info from SDK
53
- val address = ethereum.selectedAddress
54
- ?: throw IllegalStateException("MetaMask SDK returned no address after connection")
55
- val chainIdString = ethereum.chainId
56
- ?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
57
-
58
- // Parse chainId from hex string (e.g., "0x1") or decimal string to number
59
- // Nitro requires chainId to be Double (number in TS maps to Double in Kotlin)
60
- val chainId = try {
61
- val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
62
- chainIdString.substring(2).toLong(16).toInt()
63
- } else {
64
- chainIdString.toLong().toInt()
65
- }
66
- chainIdInt.toDouble()
67
- } catch (e: NumberFormatException) {
68
- throw IllegalStateException("Invalid chainId format: $chainIdString")
69
- }
70
-
71
- ConnectResult(
72
- address = address,
73
- chainId = chainId
74
- )
75
- }
76
- is Result.Success.ItemMap -> {
77
- // Handle ItemMap case (shouldn't happen for connect, but make exhaustive)
78
- throw IllegalStateException("Unexpected ItemMap result from MetaMask connect")
79
- }
80
- is Result.Success.Items -> {
81
- // Handle Items case (shouldn't happen for connect, but make exhaustive)
82
- throw IllegalStateException("Unexpected Items result from MetaMask connect")
83
- }
84
- is Result.Error -> {
85
- // Result.Error contains the error directly
86
- val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connection failed"
87
- throw Exception(errorMessage)
88
- }
89
- }
90
- }
91
- }
92
-
93
- override fun signMessage(message: String): Promise<String> {
94
- // Use Promise.async with coroutines for best practice in Nitro modules
95
- // Reference: https://nitro.margelo.com/docs/types/promises
96
- return Promise.async {
97
- // Verify connection state before attempting to sign
98
- // MetaMask SDK requires an active connection to sign messages
99
- val address = ethereum.selectedAddress
100
- if (address.isNullOrEmpty()) {
101
- throw IllegalStateException("No connected account. Please call connect() first to establish a connection with MetaMask.")
102
- }
103
-
104
- // Create EthereumRequest for personal_sign
105
- // Based on MetaMask Android SDK docs: params are [account, message]
106
- // Reference: https://github.com/MetaMask/metamask-android-sdk
107
- // EthereumRequest constructor expects method as String
108
- val request = EthereumRequest(
109
- method = "personal_sign",
110
- params = listOf(address, message)
111
- )
112
-
113
- // Convert callback-based sendRequest() to suspend function
114
- val result = suspendCancellableCoroutine<Result> { continuation ->
115
- ethereum.sendRequest(request) { callbackResult ->
116
- if (continuation.isActive) {
117
- continuation.resume(callbackResult)
118
- }
119
- }
120
- }
121
-
122
- when (result) {
123
- is Result.Success.Item -> {
124
- // Extract signature from response
125
- // The signature should be a hex-encoded string (0x-prefixed)
126
- val signature = result.value as? String
127
- ?: throw Exception("Invalid signature response format")
128
- signature
129
- }
130
- is Result.Success.ItemMap -> {
131
- // Handle ItemMap case (shouldn't happen for signMessage, but make exhaustive)
132
- throw IllegalStateException("Unexpected ItemMap result from MetaMask signMessage")
133
- }
134
- is Result.Success.Items -> {
135
- // Handle Items case (shouldn't happen for signMessage, but make exhaustive)
136
- throw IllegalStateException("Unexpected Items result from MetaMask signMessage")
137
- }
138
- is Result.Error -> {
139
- // Result.Error contains the error directly
140
- val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask signing failed"
141
- throw Exception(errorMessage)
142
- }
143
- }
144
- }
145
- }
146
- }