@novastera-oss/nitro-metamask 0.2.7 → 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 (58) hide show
  1. package/LICENSE +21 -201
  2. package/NitroMetamask.podspec +1 -1
  3. package/README.md +75 -228
  4. package/android/CMakeLists.txt +3 -0
  5. package/android/build.gradle +24 -18
  6. package/android/fix-prefab.gradle +1 -1
  7. package/android/gradle.properties +2 -2
  8. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +409 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrometamask/MetamaskContextHolder.kt +4 -25
  10. package/android/src/main/java/com/margelo/nitro/nitrometamask/NitroMetamaskPackage.kt +36 -7
  11. package/ios/Bridge.h +2 -2
  12. package/ios/HybridNitroMetamask.swift +241 -0
  13. package/lib/commonjs/index.js +20 -0
  14. package/lib/commonjs/index.js.map +1 -0
  15. package/lib/commonjs/package.json +1 -0
  16. package/lib/commonjs/specs/nitro-metamask.nitro.js +6 -0
  17. package/lib/commonjs/specs/nitro-metamask.nitro.js.map +1 -0
  18. package/lib/module/index.js +16 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/specs/nitro-metamask.nitro.js +4 -0
  21. package/lib/module/specs/nitro-metamask.nitro.js.map +1 -0
  22. package/lib/typescript/src/index.d.ts +15 -0
  23. package/lib/typescript/src/index.d.ts.map +1 -0
  24. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +23 -0
  25. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -0
  26. package/nitro.json +4 -4
  27. package/nitrogen/generated/android/NitroMetamask+autolinking.cmake +2 -2
  28. package/nitrogen/generated/android/NitroMetamaskOnLoad.cpp +4 -4
  29. package/nitrogen/generated/android/c++/{JHybridMetamaskConnectorSpec.cpp → JHybridNitroMetamaskSpec.cpp} +31 -10
  30. package/nitrogen/generated/android/c++/{JHybridMetamaskConnectorSpec.hpp → JHybridNitroMetamaskSpec.hpp} +12 -10
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/{HybridMetamaskConnectorSpec.kt → HybridNitroMetamaskSpec.kt} +14 -6
  32. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.cpp +9 -9
  33. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +38 -13
  34. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +6 -5
  35. package/nitrogen/generated/ios/NitroMetamaskAutolinking.mm +3 -3
  36. package/nitrogen/generated/ios/NitroMetamaskAutolinking.swift +6 -6
  37. package/nitrogen/generated/ios/c++/{HybridMetamaskConnectorSpecSwift.cpp → HybridNitroMetamaskSpecSwift.cpp} +2 -2
  38. package/nitrogen/generated/ios/c++/{HybridMetamaskConnectorSpecSwift.hpp → HybridNitroMetamaskSpecSwift.hpp} +28 -13
  39. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +59 -0
  40. package/nitrogen/generated/ios/swift/{HybridMetamaskConnectorSpec_cxx.swift → HybridNitroMetamaskSpec_cxx.swift} +55 -18
  41. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +24 -0
  42. package/nitrogen/generated/shared/c++/{HybridMetamaskConnectorSpec.hpp → HybridNitroMetamaskSpec.hpp} +13 -10
  43. package/package.json +55 -39
  44. package/react-native.config.js +4 -1
  45. package/src/index.ts +14 -5
  46. package/src/specs/nitro-metamask.nitro.ts +21 -0
  47. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridMetamaskConnector.kt +0 -126
  48. package/ios/HybridMetamaskConnector.swift +0 -97
  49. package/lib/MetamaskConnector.nitro.d.ts +0 -12
  50. package/lib/MetamaskConnector.nitro.js +0 -1
  51. package/lib/index.d.ts +0 -3
  52. package/lib/index.js +0 -2
  53. package/lib/specs/Example.nitro.d.ts +0 -0
  54. package/lib/specs/Example.nitro.js +0 -2
  55. package/nitrogen/generated/ios/swift/HybridMetamaskConnectorSpec.swift +0 -57
  56. package/nitrogen/generated/shared/c++/HybridMetamaskConnectorSpec.cpp +0 -22
  57. package/src/MetamaskConnector.nitro.ts +0 -13
  58. package/src/specs/Example.nitro.ts +0 -1
@@ -48,4 +48,4 @@ afterEvaluate {
48
48
  }
49
49
  }
50
50
  }
51
- }
51
+ }
@@ -1,5 +1,5 @@
1
1
  NitroMetamask_kotlinVersion=2.1.20
2
2
  NitroMetamask_minSdkVersion=23
3
- NitroMetamask_targetSdkVersion=36
4
- NitroMetamask_compileSdkVersion=36
3
+ NitroMetamask_targetSdkVersion=35
4
+ NitroMetamask_compileSdkVersion=34
5
5
  NitroMetamask_ndkVersion=27.1.12297006
@@ -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
+ }
@@ -2,35 +2,14 @@ package com.margelo.nitro.nitrometamask
2
2
 
3
3
  import android.content.Context
4
4
 
5
- /**
6
- * Context holder for MetaMask SDK initialization.
7
- *
8
- * Nitro does not provide Android Context access, so we must manage it ourselves.
9
- * This pattern is used by all Nitro modules that need Context (VisionCamera, MMKV, etc.)
10
- *
11
- * The context is initialized from NitroMetamaskPackage when React Native loads the module.
12
- */
13
5
  object MetamaskContextHolder {
14
- @Volatile
15
- private var appContext: Context? = null
6
+ private var context: Context? = null
16
7
 
17
- /**
18
- * Initialize the context holder with the React Native application context.
19
- * This should be called once from NitroMetamaskPackage.getModule()
20
- */
21
- fun initialize(context: Context) {
22
- appContext = context.applicationContext
8
+ fun initialize(ctx: Context) {
9
+ context = ctx.applicationContext
23
10
  }
24
11
 
25
- /**
26
- * Get the application context.
27
- * Throws if not initialized - this ensures we fail fast if the package wasn't loaded correctly.
28
- */
29
12
  fun get(): Context {
30
- return appContext
31
- ?: throw IllegalStateException(
32
- "MetamaskContextHolder not initialized. " +
33
- "Make sure NitroMetamaskPackage is properly registered in your React Native app."
34
- )
13
+ return context ?: throw IllegalStateException("Context not initialized")
35
14
  }
36
15
  }
@@ -1,20 +1,49 @@
1
1
  package com.margelo.nitro.nitrometamask
2
2
 
3
+ import com.facebook.react.BaseReactPackage
3
4
  import com.facebook.react.bridge.NativeModule
4
5
  import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
5
7
  import com.facebook.react.module.model.ReactModuleInfoProvider
6
- import com.facebook.react.BaseReactPackage
8
+ import com.margelo.nitro.nitrometamask.NitroMetamaskOnLoad
7
9
 
8
10
  class NitroMetamaskPackage : BaseReactPackage() {
9
- override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
- // Initialize MetamaskContextHolder with React Native application context
11
- // This is the ONLY way to get Context in Nitro modules - Nitro doesn't provide Context APIs
12
- // The context is stored in our own holder and accessed by HybridMetamaskConnector
13
- MetamaskContextHolder.initialize(reactContext)
11
+ @Volatile
12
+ private var contextInitialized = false
13
+
14
+ override fun getModule(
15
+ name: String,
16
+ reactContext: ReactApplicationContext
17
+ ): NativeModule? {
18
+ // Initialize context on first call (thread-safe)
19
+ if (!contextInitialized) {
20
+ synchronized(this) {
21
+ if (!contextInitialized) {
22
+ MetamaskContextHolder.initialize(reactContext)
23
+ contextInitialized = true
24
+ }
25
+ }
26
+ }
14
27
  return null
15
28
  }
16
29
 
17
- override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { HashMap() }
30
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
31
+ // Register a dummy module name to ensure getModule() is called
32
+ // This guarantees context initialization
33
+ return ReactModuleInfoProvider {
34
+ mapOf(
35
+ "NitroMetamaskPackage" to ReactModuleInfo(
36
+ "NitroMetamaskPackage",
37
+ "NitroMetamaskPackage",
38
+ false, // canOverrideExistingModule
39
+ true, // needsEagerInit
40
+ true, // hasConstants
41
+ false, // isCxxModule
42
+ true // isTurboModule
43
+ )
44
+ )
45
+ }
46
+ }
18
47
 
19
48
  companion object {
20
49
  init {
package/ios/Bridge.h CHANGED
@@ -1,8 +1,8 @@
1
1
  //
2
2
  // Bridge.h
3
- // NitroMetamask
3
+ // nitro-metamask
4
4
  //
5
- // Created by Marc Rousavy on 22.07.24.
5
+ // Created by DarkSorrow on 05/01/2026
6
6
  //
7
7
 
8
8
  #pragma once