@novastera-oss/nitro-metamask 0.3.3 → 0.4.2

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 (39) hide show
  1. package/README.md +73 -12
  2. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +349 -175
  3. package/app.plugin.js +121 -0
  4. package/ios/HybridNitroMetamask.swift +71 -110
  5. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +31 -6
  6. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
  7. package/nitrogen/generated/android/NitroMetamask+autolinking.cmake +2 -0
  8. package/nitrogen/generated/android/c++/JConnectResult.hpp +3 -3
  9. package/nitrogen/generated/android/c++/JConnectSignResult.hpp +65 -0
  10. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.cpp +48 -7
  11. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +4 -2
  12. package/nitrogen/generated/android/c++/JVariant_NullType_Long.cpp +26 -0
  13. package/nitrogen/generated/android/c++/JVariant_NullType_Long.hpp +69 -0
  14. package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
  15. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
  16. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectResult.kt +2 -2
  17. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectSignResult.kt +44 -0
  18. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt +11 -2
  19. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_Long.kt +59 -0
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_String.kt +59 -0
  21. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.cpp +24 -0
  22. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +192 -0
  23. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +5 -0
  24. package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +24 -3
  25. package/nitrogen/generated/ios/swift/ConnectResult.swift +2 -2
  26. package/nitrogen/generated/ios/swift/ConnectSignResult.swift +40 -0
  27. package/nitrogen/generated/ios/swift/Func_void_ConnectSignResult.swift +47 -0
  28. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__int64_t_.swift +59 -0
  29. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +59 -0
  30. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +4 -2
  31. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +67 -8
  32. package/nitrogen/generated/ios/swift/Variant_NullType_Int64.swift +18 -0
  33. package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
  34. package/nitrogen/generated/shared/c++/ConnectResult.hpp +5 -5
  35. package/nitrogen/generated/shared/c++/ConnectSignResult.hpp +91 -0
  36. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +2 -0
  37. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +9 -2
  38. package/package.json +18 -4
  39. package/src/specs/nitro-metamask.nitro.ts +32 -6
@@ -5,9 +5,12 @@ import android.content.pm.PackageManager
5
5
  import android.net.Uri
6
6
  import android.util.Log
7
7
  import com.margelo.nitro.core.Promise
8
+ import com.margelo.nitro.core.NullType
8
9
  import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
9
10
  import com.margelo.nitro.nitrometamask.ConnectResult
10
11
  import com.margelo.nitro.nitrometamask.MetamaskContextHolder
12
+ import com.margelo.nitro.nitrometamask.Variant_NullType_String
13
+ import com.margelo.nitro.nitrometamask.Variant_NullType_Long
11
14
  import io.metamask.androidsdk.Ethereum
12
15
  import io.metamask.androidsdk.Result
13
16
  import io.metamask.androidsdk.DappMetadata
@@ -22,6 +25,10 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
22
25
  @Volatile
23
26
  private var dappUrl: String? = null
24
27
 
28
+ // Configurable deep link scheme - if not set, will attempt auto-detection
29
+ @Volatile
30
+ private var configuredDeepLinkScheme: String? = null
31
+
25
32
  // Ethereum SDK instance - lazy initialization
26
33
  @Volatile
27
34
  private var ethereumInstance: Ethereum? = null
@@ -30,6 +37,10 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
30
37
  @Volatile
31
38
  private var lastUsedUrl: String? = null
32
39
 
40
+ // Cache the detected deep link scheme to avoid repeated detection
41
+ @Volatile
42
+ private var cachedDeepLinkScheme: String? = null
43
+
33
44
  // Get or create Ethereum SDK instance
34
45
  // Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
35
46
  // The SDK automatically detects and uses the deep link from AndroidManifest.xml
@@ -71,16 +82,165 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
71
82
  }
72
83
 
73
84
 
74
- override fun configure(dappUrl: String?) {
85
+ override fun configure(dappUrl: String?, deepLinkScheme: String?) {
75
86
  synchronized(this) {
76
87
  val urlToUse = dappUrl ?: "https://novastera.com"
88
+ val schemeToUse = deepLinkScheme?.takeIf { it.isNotEmpty() }
89
+
90
+ var changed = false
77
91
  if (this.dappUrl != urlToUse) {
78
92
  this.dappUrl = urlToUse
93
+ changed = true
94
+ }
95
+ if (this.configuredDeepLinkScheme != schemeToUse) {
96
+ this.configuredDeepLinkScheme = schemeToUse
97
+ // Clear cached detection when manually configured
98
+ cachedDeepLinkScheme = null
99
+ changed = true
100
+ }
101
+
102
+ if (changed) {
79
103
  // Invalidate existing instance to force recreation with new URL
80
104
  ethereumInstance = null
81
105
  lastUsedUrl = null
82
- Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse. Deep link return is handled automatically via AndroidManifest.xml")
106
+ if (schemeToUse != null) {
107
+ Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse, deep link scheme set to $schemeToUse")
108
+ } else {
109
+ Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse. Deep link scheme will be auto-detected from AndroidManifest.xml")
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get the deep link scheme - uses configured value first, then attempts auto-detection.
117
+ * Directly reads intent filters from PackageManager to find the scheme with host="mmsdk"
118
+ * Returns the scheme if found, null otherwise
119
+ *
120
+ * The scheme is cached after first detection to avoid repeated queries.
121
+ */
122
+ private fun getDeepLinkScheme(context: android.content.Context): String? {
123
+ // Use configured scheme if available
124
+ configuredDeepLinkScheme?.let { return it }
125
+
126
+ // Return cached detected scheme if available
127
+ cachedDeepLinkScheme?.let { return it }
128
+
129
+ return try {
130
+ val packageManager = context.packageManager
131
+ val packageName = context.packageName
132
+
133
+ // Query for activities that can handle VIEW intents with BROWSABLE category
134
+ val viewIntent = Intent(Intent.ACTION_VIEW).apply {
135
+ addCategory(android.content.Intent.CATEGORY_DEFAULT)
136
+ addCategory(android.content.Intent.CATEGORY_BROWSABLE)
137
+ }
138
+
139
+ val resolveList = packageManager.queryIntentActivities(viewIntent, PackageManager.MATCH_DEFAULT_ONLY)
140
+
141
+ // Look for activities in our package
142
+ for (resolveInfo in resolveList) {
143
+ if (resolveInfo.activityInfo?.packageName == packageName) {
144
+ val filter = resolveInfo.filter ?: continue
145
+
146
+ // Check if this filter has the required actions and categories
147
+ if (!filter.hasAction(Intent.ACTION_VIEW)) continue
148
+ if (!filter.hasCategory(android.content.Intent.CATEGORY_DEFAULT)) continue
149
+ if (!filter.hasCategory(android.content.Intent.CATEGORY_BROWSABLE)) continue
150
+
151
+ // Get all data schemes from this filter
152
+ val schemeCount = filter.countDataSchemes()
153
+ for (schemeIdx in 0 until schemeCount) {
154
+ val scheme = filter.getDataScheme(schemeIdx)
155
+ if (scheme != null) {
156
+ // Check if this scheme has mmsdk host in any authority
157
+ val authorityCount = filter.countDataAuthorities()
158
+ var hasMmsdkHost = false
159
+ for (authIdx in 0 until authorityCount) {
160
+ val authority = filter.getDataAuthority(authIdx)
161
+ if (authority != null && authority.host == "mmsdk") {
162
+ hasMmsdkHost = true
163
+ break
164
+ }
165
+ }
166
+
167
+ if (hasMmsdkHost) {
168
+ // Verify this scheme with mmsdk host resolves to our package
169
+ val testUri = Uri.parse("$scheme://mmsdk")
170
+ val testIntent = Intent(Intent.ACTION_VIEW, testUri).apply {
171
+ addCategory(android.content.Intent.CATEGORY_DEFAULT)
172
+ addCategory(android.content.Intent.CATEGORY_BROWSABLE)
173
+ }
174
+
175
+ // Verify this intent resolves to our package
176
+ val testResolveList = packageManager.queryIntentActivities(testIntent, PackageManager.MATCH_DEFAULT_ONLY)
177
+ for (testResolveInfo in testResolveList) {
178
+ if (testResolveInfo.activityInfo?.packageName == packageName) {
179
+ // Cache the detected scheme
180
+ cachedDeepLinkScheme = scheme
181
+ Log.d("NitroMetamask", "Detected deep link scheme: $scheme from activity ${resolveInfo.activityInfo?.name}")
182
+ return scheme
183
+ }
184
+ }
185
+ Log.w("NitroMetamask", "Scheme $scheme with mmsdk host found but does not resolve to package $packageName")
186
+ }
187
+ }
188
+ }
189
+ }
83
190
  }
191
+
192
+ Log.w("NitroMetamask", "Could not detect deep link scheme from AndroidManifest.xml. Searched ${resolveList.size} activities in package $packageName")
193
+ null
194
+ } catch (e: Exception) {
195
+ Log.w("NitroMetamask", "Error detecting deep link scheme: ${e.message}", e)
196
+ null
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Bring app back to foreground after MetaMask operations.
202
+ * Uses the deep link scheme detected from AndroidManifest.xml to trigger the return.
203
+ * This works by launching the same deep link that MetaMask app would use.
204
+ *
205
+ * Note: Deep links work from background, but getLaunchIntentForPackage() is blocked.
206
+ * So we only use deep link, never fallback to launch intent.
207
+ */
208
+ private fun bringAppToForeground() {
209
+ try {
210
+ val context = MetamaskContextHolder.get()
211
+ // Must run on main thread - use Handler to ensure we're on main thread
212
+ android.os.Handler(android.os.Looper.getMainLooper()).post {
213
+ try {
214
+ val deepLinkScheme = getDeepLinkScheme(context)
215
+ if (deepLinkScheme != null) {
216
+ // Use the configured or detected deep link scheme to bring app to foreground
217
+ // This is the same deep link that MetaMask app would trigger
218
+ // Deep links work from background (unlike getLaunchIntentForPackage)
219
+ val intent = Intent(Intent.ACTION_VIEW).apply {
220
+ data = Uri.parse("$deepLinkScheme://mmsdk")
221
+ addFlags(
222
+ Intent.FLAG_ACTIVITY_NEW_TASK or
223
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
224
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
225
+ )
226
+ setPackage(context.packageName)
227
+ }
228
+ context.startActivity(intent)
229
+ Log.d("NitroMetamask", "Brought app to foreground using deep link: $deepLinkScheme://mmsdk")
230
+ } else {
231
+ // Cannot use getLaunchIntentForPackage() - Android blocks it from background
232
+ // MetaMask should handle the return via deep link automatically
233
+ Log.w("NitroMetamask", "Could not determine deep link scheme. Please configure it via configure(dappUrl, deepLinkScheme) or ensure AndroidManifest.xml has the correct intent filter.")
234
+ }
235
+ } catch (e: Exception) {
236
+ // Silently fail - better than crashing
237
+ // This is a defensive mechanism, not critical
238
+ Log.e("NitroMetamask", "Failed to bring app to foreground: ${e.message}", e)
239
+ }
240
+ }
241
+ } catch (e: Exception) {
242
+ // Silently fail - this is a defensive mechanism, not critical
243
+ Log.e("NitroMetamask", "Error scheduling bringAppToForeground: ${e.message}", e)
84
244
  }
85
245
  }
86
246
 
@@ -106,15 +266,14 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
106
266
  val chainIdString = ethereum.chainId
107
267
  ?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
108
268
 
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)
269
+ // Parse chainId from hex string (e.g., "0x1") or decimal string to Long
270
+ // chainId is an integer, so we use Long (bigint in TS maps to Long in Kotlin)
111
271
  val chainId = try {
112
- val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
113
- chainIdString.substring(2).toLong(16).toInt()
272
+ if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
273
+ chainIdString.substring(2).toLong(16)
114
274
  } else {
115
- chainIdString.toLong().toInt()
275
+ chainIdString.toLong()
116
276
  }
117
- chainIdInt.toDouble()
118
277
  } catch (e: NumberFormatException) {
119
278
  throw IllegalStateException("Invalid chainId format: $chainIdString")
120
279
  }
@@ -178,22 +337,9 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
178
337
  val signature = result.value as? String
179
338
  ?: throw Exception("Invalid signature response format")
180
339
 
181
- // Bring app to foreground after receiving the result
340
+ // Bring app back to foreground immediately after receiving signature
182
341
  // 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
- }
342
+ bringAppToForeground()
197
343
 
198
344
  signature
199
345
  }
@@ -214,148 +360,29 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
214
360
  }
215
361
  }
216
362
 
217
- override fun connectSign(nonce: String, exp: Long): Promise<String> {
363
+ override fun connectSign(nonce: String, exp: Long): Promise<ConnectSignResult> {
218
364
  // Use Promise.async with coroutines for best practice in Nitro modules
219
365
  // Reference: https://nitro.margelo.com/docs/types/promises
220
366
  // Based on MetaMask Android SDK: ethereum.connectSign(message)
221
367
  // Reference: https://github.com/MetaMask/metamask-android-sdk
222
- // This convenience method connects (if needed) and signs a JSON message
368
+ // The SDK's connectSign method handles connection and signing in one call
223
369
  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
370
+ try {
371
+ // Construct JSON message with only nonce and exp
372
+ // We don't include address or chainID - just encrypt nonce and exp
341
373
  val message = org.json.JSONObject().apply {
342
- put("address", address)
343
- put("chainID", chainId)
344
374
  put("nonce", nonce)
345
375
  put("exp", exp)
346
376
  }.toString()
347
377
 
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
- )
378
+ Log.d("NitroMetamask", "connectSign: Constructed message with nonce and exp: $message")
354
379
 
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 ->
380
+ // Use the SDK's connectSign method - it will connect if needed and sign the message
381
+ // This is the recommended approach per MetaMask Android SDK documentation
382
+ // The SDK will handle bringing the app back to foreground via deep linking
383
+ val result = suspendCancellableCoroutine<Result> { continuation ->
384
+ Log.d("NitroMetamask", "connectSign: Calling ethereum.connectSign with message")
385
+ ethereum.connectSign(message) { callbackResult ->
359
386
  Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
360
387
  if (continuation.isActive) {
361
388
  continuation.resume(callbackResult)
@@ -365,44 +392,191 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
365
392
  }
366
393
  }
367
394
 
368
- Log.d("NitroMetamask", "connectSign: Processing signResult")
369
- when (signResult) {
395
+ Log.d("NitroMetamask", "connectSign: Processing result")
396
+ when (result) {
370
397
  is Result.Success.Item -> {
371
- val signature = signResult.value as? String
398
+ val signature = result.value as? String
372
399
  ?: throw Exception("Invalid signature response format")
373
400
 
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)
401
+ // After connectSign, the SDK state might not be immediately updated
402
+ // Try to explicitly fetch the account and chainId to ensure they're available
403
+ // This will trigger the SDK to update its state if needed
404
+ try {
405
+ val addressResult = suspendCancellableCoroutine<Result> { continuation ->
406
+ ethereum.getEthAccounts { callbackResult ->
407
+ if (continuation.isActive) {
408
+ continuation.resume(callbackResult)
409
+ }
410
+ }
411
+ }
412
+ val chainIdResult = suspendCancellableCoroutine<Result> { continuation ->
413
+ ethereum.getChainId { callbackResult ->
414
+ if (continuation.isActive) {
415
+ continuation.resume(callbackResult)
416
+ }
417
+ }
418
+ }
419
+
420
+ // getEthAccounts returns an array of addresses, we need to extract the first one
421
+ // The SDK may return Result.Success.Item (JSON string) or Result.Success.Items (List)
422
+ Log.d("NitroMetamask", "connectSign: addressResult type: ${addressResult.javaClass.simpleName}")
423
+ val address = when (addressResult) {
424
+ is Result.Success.Item -> {
425
+ val value = addressResult.value
426
+ Log.d("NitroMetamask", "connectSign: addressResult.Item value type: ${value.javaClass.simpleName}, value: $value")
427
+ // Check if it's a JSON array string that needs parsing
428
+ if (value.startsWith("[") && value.endsWith("]")) {
429
+ try {
430
+ val jsonArray = org.json.JSONArray(value)
431
+ val firstAddr = if (jsonArray.length() > 0) jsonArray.getString(0) else null
432
+ if (firstAddr != null && firstAddr.isNotEmpty()) {
433
+ ethereum.updateAccount(firstAddr)
434
+ Log.d("NitroMetamask", "connectSign: Extracted address from JSON array: $firstAddr")
435
+ }
436
+ firstAddr
437
+ } catch (e: Exception) {
438
+ Log.w("NitroMetamask", "connectSign: Failed to parse address array: ${e.message}")
439
+ // If it's not a JSON array, treat it as a single address
440
+ if (value.isNotEmpty()) {
441
+ ethereum.updateAccount(value)
442
+ }
443
+ value
444
+ }
445
+ } else {
446
+ // Single address string
447
+ if (value.isNotEmpty()) {
448
+ ethereum.updateAccount(value)
449
+ }
450
+ value
451
+ }
452
+ }
453
+ is Result.Success.Items -> {
454
+ // Array of addresses - get the first one
455
+ Log.d("NitroMetamask", "connectSign: addressResult.Items value size: ${addressResult.value.size}")
456
+ val firstAddr = addressResult.value.firstOrNull()
457
+ if (firstAddr != null && firstAddr.isNotEmpty()) {
458
+ ethereum.updateAccount(firstAddr)
459
+ Log.d("NitroMetamask", "connectSign: Extracted address from Items: $firstAddr")
460
+ }
461
+ firstAddr
462
+ }
463
+ else -> {
464
+ Log.w("NitroMetamask", "connectSign: Unexpected addressResult type: ${addressResult.javaClass.simpleName}")
465
+ null
466
+ }
467
+ }
468
+ val chainIdStr = when (chainIdResult) {
469
+ is Result.Success.Item -> {
470
+ val chainIdValue = chainIdResult.value as? String
471
+ // Update the SDK state with the chainId
472
+ if (chainIdValue != null && chainIdValue.isNotEmpty()) {
473
+ ethereum.updateChainId(chainIdValue)
474
+ }
475
+ chainIdValue
383
476
  }
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}")
477
+ else -> null
388
478
  }
479
+
480
+ // Parse chainId from hex string (e.g., "0x1") to Long
481
+ val chainId = chainIdStr?.let { chainId ->
482
+ try {
483
+ if (chainId.startsWith("0x") || chainId.startsWith("0X")) {
484
+ chainId.substring(2).toLong(16)
485
+ } else {
486
+ chainId.toLong()
487
+ }
488
+ } catch (e: NumberFormatException) {
489
+ Log.w("NitroMetamask", "Invalid chainId format: $chainId", e)
490
+ null
491
+ }
492
+ }
493
+
494
+ // Bring app back to foreground immediately after receiving signature
495
+ // This must be done on the main thread
496
+ bringAppToForeground()
497
+
498
+ // Validate that we have all required values
499
+ if (address == null || address.isEmpty()) {
500
+ throw IllegalStateException("Failed to retrieve address after connectSign. The signature was received but the address could not be determined.")
501
+ }
502
+ if (chainId == null) {
503
+ throw IllegalStateException("Failed to retrieve chainId after connectSign. The signature was received but the chainId could not be determined.")
504
+ }
505
+
506
+ Log.d("NitroMetamask", "connectSign: Returning ConnectSignResult with signature, address=$address, chainId=$chainId")
507
+
508
+ // Return ConnectSignResult with signature, address, and chainId
509
+ ConnectSignResult(
510
+ signature = signature,
511
+ address = address,
512
+ chainId = chainId
513
+ )
514
+ } catch (e: Exception) {
515
+ Log.e("NitroMetamask", "connectSign: Error fetching address/chainId: ${e.message}", e)
516
+ throw e
389
517
  }
390
-
391
- signature
392
518
  }
393
519
  is Result.Success.ItemMap -> {
394
- // Handle ItemMap case (shouldn't happen for personal_sign, but make exhaustive)
395
520
  throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
396
521
  }
397
522
  is Result.Success.Items -> {
398
- // Handle Items case (shouldn't happen for personal_sign, but make exhaustive)
399
523
  throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
400
524
  }
401
525
  is Result.Error -> {
402
- val errorMessage = signResult.error?.message ?: signResult.error?.toString() ?: "MetaMask signing failed"
526
+ val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connectSign failed"
527
+ Log.e("NitroMetamask", "connectSign: Error from MetaMask SDK: $errorMessage")
403
528
  throw Exception(errorMessage)
404
529
  }
405
530
  }
531
+ } catch (e: Exception) {
532
+ Log.e("NitroMetamask", "connectSign: Unexpected error", e)
533
+ throw e
534
+ }
535
+ }
536
+ }
537
+
538
+ override fun getAddress(): Promise<Variant_NullType_String> {
539
+ return Promise.async {
540
+ // Read from ethereumState.value directly to get the most up-to-date value
541
+ // The SDK uses LiveData, so the cached properties might not be updated immediately
542
+ val state = ethereum.ethereumState.value
543
+ val address = state?.selectedAddress?.takeIf { it.isNotEmpty() } ?: ethereum.selectedAddress
544
+ Log.d("NitroMetamask", "getAddress: ethereumState.value?.selectedAddress = ${state?.selectedAddress}, ethereum.selectedAddress = ${ethereum.selectedAddress}, final = $address")
545
+ if (address == null || address.isEmpty()) {
546
+ Log.w("NitroMetamask", "getAddress: Address is null or empty")
547
+ // Use NullType.NULL singleton as per Nitro documentation: https://nitro.margelo.com/docs/types/nulls
548
+ Variant_NullType_String.First(NullType.NULL)
549
+ } else {
550
+ Log.d("NitroMetamask", "getAddress: Returning address: $address")
551
+ Variant_NullType_String.create(address)
552
+ }
553
+ }
554
+ }
555
+
556
+ override fun getChainId(): Promise<Variant_NullType_Long> {
557
+ return Promise.async {
558
+ // Read from ethereumState.value directly to get the most up-to-date value
559
+ // The SDK uses LiveData, so the cached properties might not be updated immediately
560
+ val state = ethereum.ethereumState.value
561
+ val chainIdString = state?.chainId?.takeIf { it.isNotEmpty() } ?: ethereum.chainId
562
+ Log.d("NitroMetamask", "getChainId: ethereumState.value?.chainId = ${state?.chainId}, ethereum.chainId = ${ethereum.chainId}, final = $chainIdString")
563
+ if (chainIdString == null || chainIdString.isEmpty()) {
564
+ Log.w("NitroMetamask", "getChainId: ChainId is null or empty")
565
+ // Use NullType.NULL singleton as per Nitro documentation: https://nitro.margelo.com/docs/types/nulls
566
+ Variant_NullType_Long.First(NullType.NULL)
567
+ } else {
568
+ try {
569
+ val chainIdLong = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
570
+ chainIdString.substring(2).toLong(16)
571
+ } else {
572
+ chainIdString.toLong()
573
+ }
574
+ Log.d("NitroMetamask", "getChainId: Returning chainId: $chainIdLong")
575
+ Variant_NullType_Long.create(chainIdLong)
576
+ } catch (e: NumberFormatException) {
577
+ Log.w("NitroMetamask", "Invalid chainId format: $chainIdString", e)
578
+ Variant_NullType_Long.First(NullType.NULL)
579
+ }
406
580
  }
407
581
  }
408
582
  }