@novastera-oss/nitro-metamask 0.3.2 → 0.4.1

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 (44) hide show
  1. package/README.md +124 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +583 -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/app.plugin.js +121 -0
  7. package/ios/HybridNitroMetamask.swift +107 -1
  8. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +36 -1
  9. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
  10. package/nitrogen/generated/android/NitroMetamask+autolinking.cmake +2 -0
  11. package/nitrogen/generated/android/c++/JConnectResult.hpp +3 -3
  12. package/nitrogen/generated/android/c++/JConnectSignResult.hpp +65 -0
  13. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.cpp +62 -0
  14. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +4 -0
  15. package/nitrogen/generated/android/c++/JVariant_NullType_Long.cpp +26 -0
  16. package/nitrogen/generated/android/c++/JVariant_NullType_Long.hpp +69 -0
  17. package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
  18. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
  19. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectResult.kt +2 -2
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectSignResult.kt +44 -0
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt +17 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_Long.kt +59 -0
  23. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_String.kt +59 -0
  24. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.cpp +24 -0
  25. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +217 -0
  26. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +6 -0
  27. package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +37 -1
  28. package/nitrogen/generated/ios/swift/ConnectResult.swift +2 -2
  29. package/nitrogen/generated/ios/swift/ConnectSignResult.swift +40 -0
  30. package/nitrogen/generated/ios/swift/Func_void_ConnectSignResult.swift +47 -0
  31. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__int64_t_.swift +59 -0
  32. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +59 -0
  33. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +4 -0
  34. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +96 -0
  35. package/nitrogen/generated/ios/swift/Variant_NullType_Int64.swift +18 -0
  36. package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
  37. package/nitrogen/generated/shared/c++/ConnectResult.hpp +5 -5
  38. package/nitrogen/generated/shared/c++/ConnectSignResult.hpp +91 -0
  39. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +4 -0
  40. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +11 -1
  41. package/package.json +4 -3
  42. package/react-native.config.js +1 -1
  43. package/src/specs/nitro-metamask.nitro.ts +37 -1
  44. package/android/src/main/java/com/nitrometamask/HybridNitroMetamask.kt +0 -146
@@ -0,0 +1,583 @@
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.core.NullType
9
+ import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
10
+ import com.margelo.nitro.nitrometamask.ConnectResult
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
14
+ import io.metamask.androidsdk.Ethereum
15
+ import io.metamask.androidsdk.Result
16
+ import io.metamask.androidsdk.DappMetadata
17
+ import io.metamask.androidsdk.SDKOptions
18
+ import io.metamask.androidsdk.EthereumRequest
19
+ import kotlinx.coroutines.suspendCancellableCoroutine
20
+ import kotlin.coroutines.resume
21
+
22
+ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
23
+ // Configurable dapp URL - defaults to novastera.com if not set
24
+ // This is only used for SDK validation - the deep link return is handled via AndroidManifest.xml
25
+ @Volatile
26
+ private var dappUrl: String? = null
27
+
28
+ // Configurable deep link scheme - if not set, will attempt auto-detection
29
+ @Volatile
30
+ private var configuredDeepLinkScheme: String? = null
31
+
32
+ // Ethereum SDK instance - lazy initialization
33
+ @Volatile
34
+ private var ethereumInstance: Ethereum? = null
35
+
36
+ // Track the URL used when creating the current SDK instance
37
+ @Volatile
38
+ private var lastUsedUrl: String? = null
39
+
40
+ // Cache the detected deep link scheme to avoid repeated detection
41
+ @Volatile
42
+ private var cachedDeepLinkScheme: String? = null
43
+
44
+ // Get or create Ethereum SDK instance
45
+ // Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
46
+ // The SDK automatically detects and uses the deep link from AndroidManifest.xml
47
+ // Reference: https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt
48
+ private val ethereum: Ethereum
49
+ get() {
50
+ val currentUrl = dappUrl ?: "https://novastera.com"
51
+ val existing = ethereumInstance
52
+ val lastUrl = lastUsedUrl
53
+
54
+ // If not initialized or URL changed, recreate SDK
55
+ if (existing == null || lastUrl != currentUrl) {
56
+ synchronized(this) {
57
+ // Double-check after acquiring lock
58
+ val existingAfterLock = ethereumInstance
59
+ val lastUrlAfterLock = lastUsedUrl
60
+ if (existingAfterLock == null || lastUrlAfterLock != currentUrl) {
61
+ val context = MetamaskContextHolder.get()
62
+
63
+ // DappMetadata.url must be a valid HTTP/HTTPS URL for SDK validation
64
+ // This is separate from the deep link scheme which is auto-detected from AndroidManifest.xml
65
+ // The deep link return to your app is handled automatically via the manifest
66
+ val dappMetadata = DappMetadata(
67
+ name = "Nitro MetaMask Connector",
68
+ url = currentUrl
69
+ )
70
+ val sdkOptions = SDKOptions(
71
+ infuraAPIKey = null,
72
+ readonlyRPCMap = null
73
+ )
74
+
75
+ ethereumInstance = Ethereum(context, dappMetadata, sdkOptions)
76
+ lastUsedUrl = currentUrl
77
+ Log.d("NitroMetamask", "Ethereum SDK initialized with DappMetadata.url=$currentUrl. Deep link auto-detected from AndroidManifest.xml")
78
+ }
79
+ }
80
+ }
81
+ return ethereumInstance!!
82
+ }
83
+
84
+
85
+ override fun configure(dappUrl: String?, deepLinkScheme: String?) {
86
+ synchronized(this) {
87
+ val urlToUse = dappUrl ?: "https://novastera.com"
88
+ val schemeToUse = deepLinkScheme?.takeIf { it.isNotEmpty() }
89
+
90
+ var changed = false
91
+ if (this.dappUrl != urlToUse) {
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) {
103
+ // Invalidate existing instance to force recreation with new URL
104
+ ethereumInstance = null
105
+ lastUsedUrl = null
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
+ }
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)
244
+ }
245
+ }
246
+
247
+ override fun connect(): Promise<ConnectResult> {
248
+ // Use Promise.async with coroutines for best practice in Nitro modules
249
+ // Reference: https://nitro.margelo.com/docs/types/promises
250
+ return Promise.async {
251
+ // Convert callback-based connect() to suspend function using suspendCancellableCoroutine
252
+ // This handles cancellation properly when JS GC disposes the promise
253
+ val result = suspendCancellableCoroutine<Result> { continuation ->
254
+ ethereum.connect { callbackResult ->
255
+ if (continuation.isActive) {
256
+ continuation.resume(callbackResult)
257
+ }
258
+ }
259
+ }
260
+
261
+ when (result) {
262
+ is Result.Success.Item -> {
263
+ // After successful connection, get account info from SDK
264
+ val address = ethereum.selectedAddress
265
+ ?: throw IllegalStateException("MetaMask SDK returned no address after connection")
266
+ val chainIdString = ethereum.chainId
267
+ ?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
268
+
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)
271
+ val chainId = try {
272
+ if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
273
+ chainIdString.substring(2).toLong(16)
274
+ } else {
275
+ chainIdString.toLong()
276
+ }
277
+ } catch (e: NumberFormatException) {
278
+ throw IllegalStateException("Invalid chainId format: $chainIdString")
279
+ }
280
+
281
+ ConnectResult(
282
+ address = address,
283
+ chainId = chainId
284
+ )
285
+ }
286
+ is Result.Success.ItemMap -> {
287
+ // Handle ItemMap case (shouldn't happen for connect, but make exhaustive)
288
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask connect")
289
+ }
290
+ is Result.Success.Items -> {
291
+ // Handle Items case (shouldn't happen for connect, but make exhaustive)
292
+ throw IllegalStateException("Unexpected Items result from MetaMask connect")
293
+ }
294
+ is Result.Error -> {
295
+ // Result.Error contains the error directly
296
+ val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connection failed"
297
+ throw Exception(errorMessage)
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ override fun signMessage(message: String): Promise<String> {
304
+ // Use Promise.async with coroutines for best practice in Nitro modules
305
+ // Reference: https://nitro.margelo.com/docs/types/promises
306
+ return Promise.async {
307
+ // Verify connection state before attempting to sign
308
+ // MetaMask SDK requires an active connection to sign messages
309
+ val address = ethereum.selectedAddress
310
+ if (address.isNullOrEmpty()) {
311
+ throw IllegalStateException("No connected account. Please call connect() first to establish a connection with MetaMask.")
312
+ }
313
+
314
+ // Create EthereumRequest for personal_sign
315
+ // Based on MetaMask Android SDK docs: params are [account, message]
316
+ // Reference: https://github.com/MetaMask/metamask-android-sdk
317
+ // EthereumRequest constructor expects method as String
318
+ val request = EthereumRequest(
319
+ method = "personal_sign",
320
+ params = listOf(address, message)
321
+ )
322
+
323
+ // Convert callback-based sendRequest() to suspend function
324
+ // The SDK will automatically handle deep link return to the app
325
+ val result = suspendCancellableCoroutine<Result> { continuation ->
326
+ ethereum.sendRequest(request) { callbackResult ->
327
+ if (continuation.isActive) {
328
+ continuation.resume(callbackResult)
329
+ }
330
+ }
331
+ }
332
+
333
+ when (result) {
334
+ is Result.Success.Item -> {
335
+ // Extract signature from response
336
+ // The signature should be a hex-encoded string (0x-prefixed)
337
+ val signature = result.value as? String
338
+ ?: throw Exception("Invalid signature response format")
339
+
340
+ // Bring app back to foreground immediately after receiving signature
341
+ // This must be done on the main thread
342
+ bringAppToForeground()
343
+
344
+ signature
345
+ }
346
+ is Result.Success.ItemMap -> {
347
+ // Handle ItemMap case (shouldn't happen for signMessage, but make exhaustive)
348
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask signMessage")
349
+ }
350
+ is Result.Success.Items -> {
351
+ // Handle Items case (shouldn't happen for signMessage, but make exhaustive)
352
+ throw IllegalStateException("Unexpected Items result from MetaMask signMessage")
353
+ }
354
+ is Result.Error -> {
355
+ // Result.Error contains the error directly
356
+ val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask signing failed"
357
+ throw Exception(errorMessage)
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ override fun connectSign(nonce: String, exp: Long): Promise<ConnectSignResult> {
364
+ // Use Promise.async with coroutines for best practice in Nitro modules
365
+ // Reference: https://nitro.margelo.com/docs/types/promises
366
+ // Based on MetaMask Android SDK: ethereum.connectSign(message)
367
+ // Reference: https://github.com/MetaMask/metamask-android-sdk
368
+ // The SDK's connectSign method handles connection and signing in one call
369
+ return Promise.async {
370
+ try {
371
+ // Construct JSON message with only nonce and exp
372
+ // We don't include address or chainID - just encrypt nonce and exp
373
+ val message = org.json.JSONObject().apply {
374
+ put("nonce", nonce)
375
+ put("exp", exp)
376
+ }.toString()
377
+
378
+ Log.d("NitroMetamask", "connectSign: Constructed message with nonce and exp: $message")
379
+
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 ->
386
+ Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
387
+ if (continuation.isActive) {
388
+ continuation.resume(callbackResult)
389
+ } else {
390
+ Log.w("NitroMetamask", "connectSign: Continuation not active, ignoring callback")
391
+ }
392
+ }
393
+ }
394
+
395
+ Log.d("NitroMetamask", "connectSign: Processing result")
396
+ when (result) {
397
+ is Result.Success.Item -> {
398
+ val signature = result.value as? String
399
+ ?: throw Exception("Invalid signature response format")
400
+
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
476
+ }
477
+ else -> null
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
517
+ }
518
+ }
519
+ is Result.Success.ItemMap -> {
520
+ throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
521
+ }
522
+ is Result.Success.Items -> {
523
+ throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
524
+ }
525
+ is Result.Error -> {
526
+ val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connectSign failed"
527
+ Log.e("NitroMetamask", "connectSign: Error from MetaMask SDK: $errorMessage")
528
+ throw Exception(errorMessage)
529
+ }
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
+ }
580
+ }
581
+ }
582
+ }
583
+ }
@@ -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