catalyst-core 0.1.0-canary.6 → 0.1.0-canary.8

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 (46) hide show
  1. package/.release-meta.json +4 -0
  2. package/README.md +4 -4
  3. package/bin/catalyst.js +8 -1
  4. package/changelog.md +27 -0
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +12 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +18 -3
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  11. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  12. package/dist/native/bridge/WebBridge.js +5 -5
  13. package/dist/native/bridge/useBaseHook.js +1 -1
  14. package/dist/native/buildAppAndroid.js +2 -2
  15. package/dist/native/buildAppIos.js +10 -17
  16. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  17. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  18. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  19. package/dist/native/internalPluginUtils.js +1 -0
  20. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  21. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  22. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  23. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +13 -2
  24. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  25. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +6 -0
  26. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -0
  27. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  28. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  29. package/dist/native/pluginComposerAndroid.js +9 -0
  30. package/dist/native/pluginComposerIos.js +7 -0
  31. package/dist/scripts/plugins.js +1 -0
  32. package/mcp_v2/conversion-tasks.json +326 -0
  33. package/mcp_v2/knowledge-base.json +1068 -0
  34. package/mcp_v2/lib/helpers.js +159 -0
  35. package/mcp_v2/mcp.js +562 -0
  36. package/mcp_v2/package.json +13 -0
  37. package/mcp_v2/schema.sql +88 -0
  38. package/mcp_v2/setup.js +276 -0
  39. package/mcp_v2/tools/build.js +686 -0
  40. package/mcp_v2/tools/config.js +453 -0
  41. package/mcp_v2/tools/conversion.js +799 -0
  42. package/mcp_v2/tools/debug.js +113 -0
  43. package/mcp_v2/tools/knowledge.js +219 -0
  44. package/mcp_v2/tools/sync.js +23 -0
  45. package/mcp_v2/tools/tasks.js +945 -0
  46. package/package.json +3 -2
@@ -0,0 +1,4 @@
1
+ {
2
+ "tag": "canary",
3
+ "version": "0.1.0-canary.8"
4
+ }
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
 
8
8
  ## Table of Contents
9
9
 
10
- - Overview
11
- - Installation
10
+ - Overview
11
+ - Installation
12
12
 
13
13
  ## Overview
14
14
 
@@ -20,8 +20,8 @@ This version adds support for Universal app for both Android and iOS. Please che
20
20
 
21
21
  **System Requirements**
22
22
 
23
- - Node version 20.4.0 or later
24
- - Compatible with **macOS** and **Linux**
23
+ - Node version 20.4.0 or later
24
+ - Compatible with **macOS** and **Linux**
25
25
 
26
26
  **Automatic Installation**
27
27
 
package/bin/catalyst.js CHANGED
@@ -13,6 +13,8 @@ const validCommands = [
13
13
  "serve",
14
14
  "devBuild",
15
15
  "devServe",
16
+ "plugin",
17
+ "plugins",
16
18
  "buildApp",
17
19
  "buildApp:ios",
18
20
  "buildApp:android",
@@ -67,11 +69,14 @@ const scriptIndex = args.findIndex(
67
69
  x === "serve" ||
68
70
  x === "devBuild" ||
69
71
  x === "devServe" ||
72
+ x === "plugin" ||
73
+ x === "plugins" ||
70
74
  isPlatformCommand(x, "buildApp") ||
71
75
  isPlatformCommand(x, "setupEmulator")
72
76
  )
73
77
  const script = scriptIndex === -1 ? args[0] : args[scriptIndex]
74
78
  const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []
79
+ const resolvedScript = script === "plugin" ? "plugins" : script
75
80
 
76
81
  if (validCommands.includes(script)) {
77
82
  // Handle platform-specific or combined commands
@@ -90,7 +95,9 @@ if (validCommands.includes(script)) {
90
95
  // Original commands
91
96
  const result = spawnSync(
92
97
  process.execPath,
93
- nodeArgs.concat(require.resolve("../dist/scripts/" + script)).concat(args.slice(scriptIndex + 1)),
98
+ nodeArgs
99
+ .concat(require.resolve("../dist/scripts/" + resolvedScript))
100
+ .concat(args.slice(scriptIndex + 1)),
94
101
  { stdio: "inherit" }
95
102
  )
96
103
  handleProcessResult(result)
package/changelog.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-canary.8] - 2026-04-15
4
+
5
+ - Introduced Catalyst’s internal plugin architecture, adding the `PluginBridge` web API, native plugin runtimes for Android and iOS, and manifest-driven build composition for modular native capabilities.
6
+ - Added config-based plugin enablement through `WEBVIEW_CONFIG.plugins` and a `catalyst plugins` CLI for discovering and managing internal framework plugins from the app layer.
7
+ - Shipped the first cross-platform migrated plugin, `device_info`
8
+
9
+ ## [0.1.0-beta.1] - 2026-04-15
10
+
11
+ - Promoted `0.1.0-canary.7` to the first proper beta release after stabilization.
12
+ - No code changes from `0.1.0-canary.7`; this release marks the same build as production-ready beta.
13
+
14
+ ## [0.1.0-canary.7] - 2026-03-31
15
+
16
+ - Introduced Catalyst MCP v2 with a new setup flow, database schema, knowledge base, and source-aware migration tooling for stronger project guidance and conversion workflows.
17
+ - Expanded MCP/framework knowledge coverage across SEO, observability, webpack, React Compiler, CLI, file conventions, and native hooks, while improving setup and verification messaging across supported MCP clients.
18
+
19
+ ## [0.1.0-canary.6] - 2026-03-09
20
+
21
+ - Hardened URL whitelisting with thread-safety improvements, broader test coverage, and related iOS build fixes to make access-control behavior more reliable.
22
+ - Improved compatibility and runtime resilience by softening bridge environment mismatch failures and preserving backward compatibility for `useDataProtection` on older native binaries.
23
+
24
+ ## [0.1.0-canary.5] - 2026-02-27
25
+
26
+ - Strengthened native app security with backup restrictions, screen-capture protection, web data clearing, and related Android/iOS test coverage.
27
+ - Improved universal app runtime behavior with safe-area inset support, edge-to-edge rendering, and notification permission override fixes.
28
+ - Expanded platform support with offline fallback handling, notification/access-control refinements, localhost HTTP allowances for local development, and file-picker/HTTPS server improvements.
29
+
3
30
  ## [0.1.0-canary.4] - 2026-02-12
4
31
 
5
32
  - Added Google Sign-In support for both Android and iOS in Catalyst, enabling a unified native authentication experience for apps built on the framework.
@@ -60,7 +60,7 @@ data class SchemaDefinition(
60
60
  object BridgeMessageValidator {
61
61
 
62
62
  private const val TAG = "BridgeMessageValidator"
63
- private const val MAX_MESSAGE_SIZE = 10 * 1024 * 1024 // 10MB (match iOS)
63
+ private const val MAX_MESSAGE_SIZE = CatalystConstants.Bridge.MAX_MESSAGE_SIZE
64
64
 
65
65
  // Valid commands — single source of truth from CatalystConstants
66
66
  private val validCommands = CatalystConstants.Bridge.VALID_COMMANDS
@@ -233,7 +233,7 @@ class CustomWebView(
233
233
  }
234
234
 
235
235
  // Additional security check: only allow whitelisted interface names
236
- val allowedInterfaces = setOf("NativeBridge", "AndroidBridge")
236
+ val allowedInterfaces = setOf("NativeBridge", "AndroidBridge", "PluginBridge")
237
237
  if (name !in allowedInterfaces) {
238
238
  Log.e(TAG, "❌ Security: Interface name '$name' is not in whitelist. Refusing to add interface.")
239
239
  return
@@ -246,6 +246,17 @@ class CustomWebView(
246
246
  }
247
247
  }
248
248
 
249
+ fun removeJavascriptInterface(name: String) {
250
+ try {
251
+ webView.removeJavascriptInterface(name)
252
+ if (BuildConfig.DEBUG) {
253
+ Log.d(TAG, "🔌 Removed JavaScript interface: $name")
254
+ }
255
+ } catch (e: Exception) {
256
+ Log.w(TAG, "⚠️ Failed to remove JavaScript interface '$name': ${e.message}")
257
+ }
258
+ }
259
+
249
260
  fun destroy() {
250
261
  job.cancel()
251
262
  webView.destroy()
@@ -14,6 +14,7 @@ import org.json.JSONObject
14
14
  import java.util.Properties
15
15
  import io.yourname.androidproject.databinding.ActivityMainBinding
16
16
  import io.yourname.androidproject.NativeBridge
17
+ import io.yourname.androidproject.plugins.PluginBridge
17
18
  import io.yourname.androidproject.utils.BridgeUtils
18
19
  import io.yourname.androidproject.utils.KeyboardUtil
19
20
  import io.yourname.androidproject.utils.NetworkUtils
@@ -39,6 +40,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
39
40
 
40
41
  private lateinit var binding: ActivityMainBinding
41
42
  private lateinit var nativeBridge: NativeBridge
43
+ private lateinit var pluginBridge: PluginBridge
42
44
  private lateinit var customWebView: CustomWebView
43
45
  lateinit var properties: Properties
44
46
  private lateinit var metricsMonitor: MetricsMonitor
@@ -328,6 +330,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
328
330
  Log.e(TAG, "Failed to initialize NativeBridge: ${e.message}")
329
331
  }
330
332
 
333
+ // Setup isolated PluginBridge
334
+ try {
335
+ pluginBridge = PluginBridge(this, customWebView.getWebView(), properties)
336
+ customWebView.addJavascriptInterface(pluginBridge, "PluginBridge")
337
+ } catch (e: Exception) {
338
+ Log.e(TAG, "Failed to initialize PluginBridge: ${e.message}")
339
+ }
340
+
331
341
  setupSafeAreaHandling()
332
342
 
333
343
  // Setup back press handler (modern API)
@@ -445,11 +455,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
445
455
  if (::keyboardUtil.isInitialized) {
446
456
  keyboardUtil.cleanup()
447
457
  }
448
- if (::nativeBridge.isInitialized) {
449
- nativeBridge.cleanup()
458
+ if (::customWebView.isInitialized) {
459
+ if (::pluginBridge.isInitialized) {
460
+ customWebView.removeJavascriptInterface("PluginBridge")
461
+ }
462
+ if (::nativeBridge.isInitialized) {
463
+ customWebView.removeJavascriptInterface("NativeBridge")
464
+ }
465
+ customWebView.destroy()
450
466
  }
451
467
  coroutineContext.cancelChildren()
452
- customWebView.destroy()
453
468
  super.onDestroy()
454
469
  }
455
470
 
@@ -0,0 +1,7 @@
1
+ package io.yourname.androidproject.plugins
2
+
3
+ import org.json.JSONObject
4
+
5
+ interface CatalystPlugin {
6
+ fun handle(command: String, data: JSONObject?, bridge: PluginBridgeContext)
7
+ }
@@ -0,0 +1,6 @@
1
+ package io.yourname.androidproject.plugins
2
+
3
+ object GeneratedPluginIndex {
4
+ val pluginIdToClassName: Map<String, String> = emptyMap()
5
+ val pluginToCommands: Map<String, Set<String>> = emptyMap()
6
+ }
@@ -0,0 +1,253 @@
1
+ package io.yourname.androidproject.plugins
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.util.Log
6
+ import android.webkit.JavascriptInterface
7
+ import android.webkit.WebView
8
+ import io.yourname.androidproject.CatalystConstants
9
+ import org.json.JSONArray
10
+ import org.json.JSONException
11
+ import org.json.JSONObject
12
+ import java.util.Properties
13
+
14
+ internal data class PluginRequest(
15
+ val pluginId: String,
16
+ val command: String,
17
+ val data: JSONObject?,
18
+ val requestId: String?
19
+ )
20
+
21
+ private class PluginBridgeRuntimeError(
22
+ val publicMessage: String,
23
+ val code: String,
24
+ cause: Throwable? = null
25
+ ) : Exception(publicMessage, cause)
26
+
27
+ class PluginBridge(
28
+ private val activity: Activity,
29
+ private val webView: WebView,
30
+ private val properties: Properties
31
+ ) {
32
+ companion object {
33
+ private const val TAG = "PluginBridge"
34
+ private const val ERROR_EVENT = "PLUGIN_BRIDGE_ERROR"
35
+ private const val SYSTEM_PLUGIN_ID = "__bridge__"
36
+ private const val ERROR_CODE_INVALID_PAYLOAD = "INVALID_PAYLOAD"
37
+ private const val ERROR_CODE_PLUGIN_NOT_FOUND = "PLUGIN_NOT_FOUND"
38
+ private const val ERROR_CODE_COMMAND_NOT_SUPPORTED = "COMMAND_NOT_SUPPORTED"
39
+ private const val ERROR_CODE_PLUGIN_NOT_REGISTERED = "PLUGIN_NOT_REGISTERED"
40
+ private const val ERROR_CODE_PLUGIN_INSTANTIATION_FAILED = "PLUGIN_INSTANTIATION_FAILED"
41
+ private const val ERROR_CODE_PLUGIN_EXECUTION_FAILED = "PLUGIN_EXECUTION_FAILED"
42
+
43
+ private fun readRequiredString(body: JSONObject, key: String): String {
44
+ if (!body.has(key) || body.isNull(key)) {
45
+ return ""
46
+ }
47
+
48
+ val rawValue = body.get(key)
49
+ if (rawValue !is String) {
50
+ throw IllegalArgumentException("$key must be a string")
51
+ }
52
+
53
+ return rawValue.trim()
54
+ }
55
+
56
+ private fun readOptionalString(body: JSONObject, key: String): String? {
57
+ if (!body.has(key) || body.isNull(key)) {
58
+ return null
59
+ }
60
+
61
+ val rawValue = body.get(key)
62
+ if (rawValue !is String) {
63
+ throw IllegalArgumentException("$key must be a string when provided")
64
+ }
65
+
66
+ return rawValue.trim().ifEmpty { null }
67
+ }
68
+
69
+ private fun readOptionalObject(body: JSONObject, key: String): JSONObject? {
70
+ if (!body.has(key) || body.isNull(key)) {
71
+ return null
72
+ }
73
+
74
+ val rawValue = body.get(key)
75
+ if (rawValue !is JSONObject) {
76
+ throw IllegalArgumentException("$key must be an object when provided")
77
+ }
78
+
79
+ return rawValue
80
+ }
81
+
82
+ internal fun parseRequest(payload: String?): PluginRequest {
83
+ if (payload.isNullOrBlank()) {
84
+ throw IllegalArgumentException("Payload is required")
85
+ }
86
+ val messageSize = payload.toByteArray(Charsets.UTF_8).size
87
+ if (messageSize > CatalystConstants.Bridge.MAX_MESSAGE_SIZE) {
88
+ throw IllegalArgumentException("Payload exceeds maximum size")
89
+ }
90
+
91
+ val body = JSONObject(payload)
92
+ return PluginRequest(
93
+ pluginId = readRequiredString(body, "pluginId"),
94
+ command = readRequiredString(body, "command"),
95
+ data = readOptionalObject(body, "data"),
96
+ requestId = readOptionalString(body, "requestId")
97
+ )
98
+ }
99
+ }
100
+
101
+ private val pluginIdToClassName = GeneratedPluginIndex.pluginIdToClassName
102
+ private val pluginToCommands = GeneratedPluginIndex.pluginToCommands
103
+
104
+ @JavascriptInterface
105
+ fun emit(payload: String?) {
106
+ var request: PluginRequest? = null
107
+
108
+ try {
109
+ request = parseRequest(payload)
110
+
111
+ if (request.pluginId.isEmpty()) {
112
+ sendBridgeError("pluginId is required", ERROR_CODE_INVALID_PAYLOAD, request)
113
+ return
114
+ }
115
+ if (request.command.isEmpty()) {
116
+ sendBridgeError("command is required", ERROR_CODE_INVALID_PAYLOAD, request)
117
+ return
118
+ }
119
+
120
+ if (!hasPlugin(request.pluginId)) {
121
+ sendBridgeError("Unsupported plugin: ${request.pluginId}", ERROR_CODE_PLUGIN_NOT_FOUND, request)
122
+ return
123
+ }
124
+
125
+ if (!hasCommand(request.pluginId, request.command)) {
126
+ sendBridgeError(
127
+ "Unsupported command '${request.command}' for plugin '${request.pluginId}'",
128
+ ERROR_CODE_COMMAND_NOT_SUPPORTED,
129
+ request
130
+ )
131
+ return
132
+ }
133
+
134
+ val plugin = try {
135
+ getPluginForId(request.pluginId)
136
+ } catch (error: PluginBridgeRuntimeError) {
137
+ sendBridgeError(error.publicMessage, error.code, request)
138
+ return
139
+ }
140
+
141
+ val callbackContext = PluginBridgeContext(
142
+ activity = activity,
143
+ webView = webView,
144
+ properties = properties,
145
+ pluginId = request.pluginId,
146
+ command = request.command,
147
+ requestId = request.requestId
148
+ )
149
+
150
+ plugin.handle(request.command, request.data, callbackContext)
151
+ } catch (error: IllegalArgumentException) {
152
+ sendBridgeError(error.message ?: "Invalid payload", ERROR_CODE_INVALID_PAYLOAD, request)
153
+ } catch (error: JSONException) {
154
+ sendBridgeError("Invalid JSON payload", ERROR_CODE_INVALID_PAYLOAD, request)
155
+ } catch (error: Exception) {
156
+ Log.e(TAG, "Plugin command failed for ${request?.pluginId ?: "<unknown>"}.${request?.command ?: "<unknown>"}", error)
157
+ sendBridgeError("Plugin execution failed", ERROR_CODE_PLUGIN_EXECUTION_FAILED, request)
158
+ }
159
+ }
160
+
161
+ private fun sendBridgeError(message: String, code: String, request: PluginRequest?) {
162
+ PluginBridgeContext(
163
+ activity = activity,
164
+ webView = webView,
165
+ properties = properties,
166
+ pluginId = SYSTEM_PLUGIN_ID,
167
+ command = request?.command,
168
+ requestId = request?.requestId
169
+ ).callback(
170
+ ERROR_EVENT,
171
+ JSONObject().apply {
172
+ put("message", message)
173
+ put("code", code)
174
+ put("pluginId", request?.pluginId ?: SYSTEM_PLUGIN_ID)
175
+ request?.command?.takeIf { it.isNotEmpty() }?.let { put("command", it) }
176
+ }
177
+ )
178
+ }
179
+
180
+ private fun hasPlugin(pluginId: String): Boolean {
181
+ return pluginIdToClassName.containsKey(pluginId)
182
+ }
183
+
184
+ private fun hasCommand(pluginId: String, command: String): Boolean {
185
+ return pluginToCommands[pluginId]?.contains(command) ?: false
186
+ }
187
+
188
+ private fun getPluginForId(pluginId: String): CatalystPlugin {
189
+ val className = pluginIdToClassName[pluginId]
190
+ ?: throw PluginBridgeRuntimeError(
191
+ "Plugin is not registered",
192
+ ERROR_CODE_PLUGIN_NOT_REGISTERED
193
+ )
194
+
195
+ return try {
196
+ val clazz = Class.forName(className)
197
+ val instance = clazz.getDeclaredConstructor().newInstance()
198
+ instance as? CatalystPlugin
199
+ ?: throw IllegalStateException("Plugin class '$className' must implement CatalystPlugin")
200
+ } catch (error: Exception) {
201
+ Log.e(TAG, "Failed to instantiate plugin class $className for plugin $pluginId", error)
202
+ throw PluginBridgeRuntimeError(
203
+ "Plugin could not be initialized",
204
+ ERROR_CODE_PLUGIN_INSTANTIATION_FAILED,
205
+ error
206
+ )
207
+ }
208
+ }
209
+ }
210
+
211
+ class PluginBridgeContext(
212
+ val activity: Activity,
213
+ val webView: WebView,
214
+ val properties: Properties,
215
+ val pluginId: String,
216
+ val command: String?,
217
+ val requestId: String?
218
+ ) {
219
+ val context: Context
220
+ get() = activity
221
+
222
+ fun callback(
223
+ eventName: String,
224
+ data: Any?,
225
+ command: String? = this.command
226
+ ) {
227
+ require(eventName.isNotBlank()) { "Callback eventName is required" }
228
+
229
+ val pluginLiteral = JSONObject.quote(pluginId)
230
+ val eventLiteral = JSONObject.quote(eventName)
231
+ val dataLiteral = toJavaScriptLiteral(data)
232
+ val requestLiteral = requestId?.let(JSONObject::quote) ?: "null"
233
+ val commandLiteral = command?.takeIf { it.isNotBlank() }?.let(JSONObject::quote) ?: "null"
234
+
235
+ webView.post {
236
+ webView.evaluateJavascript(
237
+ "window.PluginBridgeWeb && window.PluginBridgeWeb.callback($pluginLiteral, $eventLiteral, $dataLiteral, $requestLiteral, $commandLiteral);",
238
+ null
239
+ )
240
+ }
241
+ }
242
+
243
+ private fun toJavaScriptLiteral(value: Any?): String {
244
+ return when (value) {
245
+ null -> "null"
246
+ is JSONObject -> value.toString()
247
+ is JSONArray -> value.toString()
248
+ is Number, is Boolean -> value.toString()
249
+ is String -> JSONObject.quote(value)
250
+ else -> JSONObject.wrap(value)?.toString() ?: "null"
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,139 @@
1
+ package io.yourname.androidproject.plugins
2
+
3
+ import io.yourname.androidproject.CatalystConstants
4
+ import org.json.JSONException
5
+ import org.json.JSONObject
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Assert.fail
10
+ import org.junit.Test
11
+
12
+ class PluginBridgeTest {
13
+
14
+ @Test
15
+ fun `parseRequest accepts valid payload and trims string fields`() {
16
+ val request = PluginBridge.parseRequest(
17
+ """
18
+ {
19
+ "pluginId": " device-info-plugin ",
20
+ "command": " getDeviceInfo ",
21
+ "data": { "includeSecurity": true },
22
+ "requestId": " req-123 "
23
+ }
24
+ """.trimIndent()
25
+ )
26
+
27
+ assertEquals("device-info-plugin", request.pluginId)
28
+ assertEquals("getDeviceInfo", request.command)
29
+ assertEquals("req-123", request.requestId)
30
+ assertEquals(true, (request.data as JSONObject).getBoolean("includeSecurity"))
31
+ }
32
+
33
+ @Test
34
+ fun `parseRequest treats blank requestId as null`() {
35
+ val request = PluginBridge.parseRequest(
36
+ """
37
+ {
38
+ "pluginId": "device-info-plugin",
39
+ "command": "getDeviceInfo",
40
+ "requestId": " "
41
+ }
42
+ """.trimIndent()
43
+ )
44
+
45
+ assertNull(request.requestId)
46
+ }
47
+
48
+ @Test
49
+ fun `parseRequest rejects blank payload`() {
50
+ try {
51
+ PluginBridge.parseRequest(" ")
52
+ fail("Expected invalid blank payload to throw")
53
+ } catch (error: IllegalArgumentException) {
54
+ assertEquals("Payload is required", error.message)
55
+ }
56
+ }
57
+
58
+ @Test
59
+ fun `parseRequest rejects oversized payload`() {
60
+ val oversizedData = "x".repeat(CatalystConstants.Bridge.MAX_MESSAGE_SIZE + 256)
61
+ val payload = """
62
+ {
63
+ "pluginId": "device-info-plugin",
64
+ "command": "getDeviceInfo",
65
+ "data": "$oversizedData"
66
+ }
67
+ """.trimIndent()
68
+
69
+ try {
70
+ PluginBridge.parseRequest(payload)
71
+ fail("Expected oversized payload to throw")
72
+ } catch (error: IllegalArgumentException) {
73
+ assertEquals("Payload exceeds maximum size", error.message)
74
+ }
75
+ }
76
+
77
+ @Test
78
+ fun `parseRequest rejects non string pluginId`() {
79
+ try {
80
+ PluginBridge.parseRequest(
81
+ """
82
+ {
83
+ "pluginId": 42,
84
+ "command": "getDeviceInfo"
85
+ }
86
+ """.trimIndent()
87
+ )
88
+ fail("Expected non-string pluginId to throw")
89
+ } catch (error: IllegalArgumentException) {
90
+ assertEquals("pluginId must be a string", error.message)
91
+ }
92
+ }
93
+
94
+ @Test
95
+ fun `parseRequest rejects non string requestId`() {
96
+ try {
97
+ PluginBridge.parseRequest(
98
+ """
99
+ {
100
+ "pluginId": "device-info-plugin",
101
+ "command": "getDeviceInfo",
102
+ "requestId": 42
103
+ }
104
+ """.trimIndent()
105
+ )
106
+ fail("Expected non-string requestId to throw")
107
+ } catch (error: IllegalArgumentException) {
108
+ assertEquals("requestId must be a string when provided", error.message)
109
+ }
110
+ }
111
+
112
+ @Test
113
+ fun `parseRequest rejects invalid JSON`() {
114
+ try {
115
+ PluginBridge.parseRequest("{")
116
+ fail("Expected invalid JSON to throw")
117
+ } catch (error: JSONException) {
118
+ assertTrue(error.message?.isNotBlank() == true)
119
+ }
120
+ }
121
+
122
+ @Test
123
+ fun `parseRequest rejects non object data`() {
124
+ try {
125
+ PluginBridge.parseRequest(
126
+ """
127
+ {
128
+ "pluginId": "device-info-plugin",
129
+ "command": "getDeviceInfo",
130
+ "data": "unsafe"
131
+ }
132
+ """.trimIndent()
133
+ )
134
+ fail("Expected non-object data to throw")
135
+ } catch (error: IllegalArgumentException) {
136
+ assertEquals("data must be an object when provided", error.message)
137
+ }
138
+ }
139
+ }
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _NativeInterfaces=require("./constants/NativeInterfaces.js");var _NativeBridge=_interopRequireDefault(require("./utils/NativeBridge.js"));var _CameraUtils=_interopRequireDefault(require("./utils/CameraUtils.js"));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}class WebBridge{constructor(){this.handlers=new Map();this.initialized=false;this._logInitialization();}/**
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _NativeInterfaces=require("./constants/NativeInterfaces.js");var _NativeBridge=_interopRequireDefault(require("./utils/NativeBridge.js"));var _CameraUtils=_interopRequireDefault(require("./utils/CameraUtils.js"));var _PluginBridge=_interopRequireDefault(require("../plugin-bridge/PluginBridge.js"));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const DEVICE_INFO_PLUGIN={pluginId:"io.catalyst.device_info",command:"getDeviceInfo",successEvent:"onSuccess",errorEvent:"onError"};const DEVICE_INFO_REQUEST_TIMEOUT_MS=5000;const DEVICE_INFO_PLUGIN_FALLBACK_CODES=new Set(["PLUGIN_NOT_FOUND","COMMAND_NOT_SUPPORTED","PLUGIN_NOT_REGISTERED"]);const DEVICE_INFO_PLUGIN_FALLBACK_MESSAGES=["pluginbridge is not available in this environment","pluginbridge is not available","device info plugin request timed out"];const normalizeDeviceInfoPayload=payload=>{if(typeof payload!=="string"){return payload;}try{return JSON.parse(payload);}catch{return{message:payload};}};const createDeviceInfoError=(payload,fallbackMessage)=>{const parsed=normalizeDeviceInfoPayload(payload);const message=parsed?.message||parsed?.error||fallbackMessage;const error=new Error(message);if(parsed?.code){error.code=parsed.code;}if(parsed?.pluginId){error.pluginId=parsed.pluginId;}if(parsed?.command){error.command=parsed.command;}return error;};class WebBridge{constructor(){this.handlers=new Map();this.initialized=false;this._logInitialization();}/**
2
2
  * Log initialization info
3
3
  */_logInitialization(){console.log("🌉 WebBridge initialized");console.log("🌉 Available callback interfaces:",_NativeInterfaces.ALL_CALLBACKS.length);if(process.env.NODE_ENV==="development"){(0,_NativeInterfaces.debugInterfaces)();}}/**
4
4
  * Static method to initialize WebBridge on window object
@@ -51,9 +51,9 @@ if(this.handlers.has(interfaceName)){console.warn(`🌉 Interface ${interfaceNam
51
51
  * Request camera permission
52
52
  * @param {Object} config - Permission configuration (includeDetails)
53
53
  * @returns {Promise<Object>} - Promise that resolves with permission status or rejects with error
54
- */requestCameraPermission=(config={})=>{return _CameraUtils.default.requestCameraPermission(config,this.register,this.unregister);};/**
54
+ */requestCameraPermission=(config={})=>{return _CameraUtils.default.requestCameraPermission(config,this.register,this.unregister);};getDeviceInfoFromLegacyBridge=parseDeviceInfo=>{return new Promise((resolve,reject)=>{let timeoutId=null;const cleanup=()=>{if(timeoutId!=null){clearTimeout(timeoutId);}this.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_SUCCESS);this.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_ERROR);};this.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_SUCCESS,data=>{cleanup();resolve(parseDeviceInfo(data));});this.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_ERROR,error=>{cleanup();reject(new Error(error));});try{const dispatched=_NativeBridge.default.device.getDeviceInfo();if(dispatched===false){cleanup();reject(new Error("Legacy device info bridge failed to dispatch"));return;}timeoutId=setTimeout(()=>{cleanup();reject(new Error("Legacy device info request timed out"));},DEVICE_INFO_REQUEST_TIMEOUT_MS);}catch(error){cleanup();reject(error);}});};getDeviceInfoFromPluginBridge=parseDeviceInfo=>{return new Promise((resolve,reject)=>{_PluginBridge.default.init();let isSettled=false;let timeoutId=null;const cleanups=[];const cleanup=()=>{if(timeoutId!=null){clearTimeout(timeoutId);}while(cleanups.length>0){const unregister=cleanups.pop();unregister();}};const settle=callback=>{if(isSettled){return;}isSettled=true;cleanup();callback();};cleanups.push(_PluginBridge.default.register({pluginId:DEVICE_INFO_PLUGIN.pluginId,eventName:DEVICE_INFO_PLUGIN.successEvent,command:DEVICE_INFO_PLUGIN.command,handler:payload=>{settle(()=>{try{resolve(parseDeviceInfo(payload));}catch(error){reject(error);}});}}));cleanups.push(_PluginBridge.default.register({pluginId:DEVICE_INFO_PLUGIN.pluginId,eventName:DEVICE_INFO_PLUGIN.errorEvent,command:DEVICE_INFO_PLUGIN.command,handler:payload=>{settle(()=>{reject(createDeviceInfoError(payload,"Device info plugin failed"));});}}));cleanups.push(_PluginBridge.default.register({pluginId:"__bridge__",eventName:"PLUGIN_BRIDGE_ERROR",command:DEVICE_INFO_PLUGIN.command,handler:payload=>{const pluginId=normalizeDeviceInfoPayload(payload)?.pluginId;if(pluginId!==DEVICE_INFO_PLUGIN.pluginId){return;}settle(()=>{reject(createDeviceInfoError(payload,"Device info plugin unavailable"));});}}));try{_PluginBridge.default.emit({pluginId:DEVICE_INFO_PLUGIN.pluginId,command:DEVICE_INFO_PLUGIN.command});timeoutId=setTimeout(()=>{settle(()=>{reject(new Error("Device info plugin request timed out"));});},DEVICE_INFO_REQUEST_TIMEOUT_MS);}catch(error){cleanup();reject(error);}});};shouldFallbackToLegacyDeviceInfo=error=>{if(!error){return false;}if(DEVICE_INFO_PLUGIN_FALLBACK_CODES.has(error.code)){return true;}if(typeof error.message!=="string"){return false;}const normalizedMessage=error.message.toLowerCase();return DEVICE_INFO_PLUGIN_FALLBACK_MESSAGES.some(message=>normalizedMessage.includes(message));};/**
55
55
  * Get device information
56
56
  * @returns {Promise<Object>} - Promise that resolves with device info or rejects with error
57
- */getDeviceInfo=()=>{const{platform,isNativeEnvironment}=_NativeBridge.default.getEnvironmentInfo();// Handle web environment
58
- if(!isNativeEnvironment||platform==="web"){return new Promise(resolve=>{const webDeviceInfo={model:navigator.userAgent,manufacturer:"browser",platform:"web",screenWidth:screen.width,screenHeight:screen.height,screenDensity:window.devicePixelRatio};resolve(webDeviceInfo);});}// Key mapping from native to web keys
59
- const DEVICE_INFO_KEY_MAP={android:{model:"model",manufacturer:"manufacturer",platform:"platform",screenWidth:"screenWidth",screenHeight:"screenHeight",screenDensity:"screenDensity",appInfo:"appInfo"},ios:{model:"model",manufacturer:"manufacturer",platform:"platform",screenWidth:"screenWidth",screenHeight:"screenHeight",screenDensity:"screenDensity",appInfo:"appInfo"}};const parseDeviceInfo=data=>{const rawDeviceInfo=typeof data==="string"?JSON.parse(data):data;const platformMapping=DEVICE_INFO_KEY_MAP[platform]||{};return Object.fromEntries(Object.entries(rawDeviceInfo).map(([key,value])=>[platformMapping[key]||key,value]));};return new Promise((resolve,reject)=>{const cleanup=()=>{this.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_SUCCESS);this.unregister(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_ERROR);};this.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_SUCCESS,data=>{cleanup();resolve(parseDeviceInfo(data));});this.register(_NativeInterfaces.NATIVE_CALLBACKS.ON_DEVICE_INFO_ERROR,error=>{cleanup();reject(new Error(error));});try{_NativeBridge.default.device.getDeviceInfo();}catch(error){cleanup();reject(error);}});};}var _default=exports.default=WebBridge;
57
+ */getDeviceInfo=async()=>{const{platform,isNativeEnvironment}=_NativeBridge.default.getEnvironmentInfo();// Handle web environment
58
+ if(!isNativeEnvironment||platform==="web"){return{model:navigator.userAgent,manufacturer:"browser",platform:"web",screenWidth:screen.width,screenHeight:screen.height,screenDensity:window.devicePixelRatio};}// Key mapping from native to web keys
59
+ const DEVICE_INFO_KEY_MAP={android:{model:"model",manufacturer:"manufacturer",platform:"platform",screenWidth:"screenWidth",screenHeight:"screenHeight",screenDensity:"screenDensity",appInfo:"appInfo"},ios:{model:"model",manufacturer:"manufacturer",platform:"platform",screenWidth:"screenWidth",screenHeight:"screenHeight",screenDensity:"screenDensity",appInfo:"appInfo"}};const parseDeviceInfo=data=>{const rawDeviceInfo=typeof data==="string"?JSON.parse(data):data;const platformMapping=DEVICE_INFO_KEY_MAP[platform]||{};return Object.fromEntries(Object.entries(rawDeviceInfo).map(([key,value])=>[platformMapping[key]||key,value]));};try{return await this.getDeviceInfoFromPluginBridge(parseDeviceInfo);}catch(error){if(!this.shouldFallbackToLegacyDeviceInfo(error)){throw error;}return this.getDeviceInfoFromLegacyBridge(parseDeviceInfo);}};}var _default=exports.default=WebBridge;
@@ -20,7 +20,7 @@ if((0,_errors.isDevelopment)()){console.group(`🚨 ${hookName} Error`);console.
20
20
  const setDataAndComplete=(0,_react.useCallback)(newData=>{setData(newData);setLoading(false);completeProgress();setError(null);if((0,_errors.isDevelopment)()){console.log(`✅ ${hookName} Success:`,newData);}},[hookName,completeProgress]);const clear=(0,_react.useCallback)(()=>{setData(null);setError(null);resetProgress();if((0,_errors.isDevelopment)()){console.log(`🗑️ ${hookName} Cleared`);}},[hookName,resetProgress]);// Fire-and-forget native call — no-ops silently on web, routes errors through handleNativeError on native
21
21
  const callNative=(0,_react.useCallback)(fn=>{if(!isNative()){if((0,_errors.isDevelopment)()){console.warn(`${hookName} callNative skipped — not in native environment`);}return;}try{fn();}catch(err){if((0,_errors.isDevelopment)()){console.warn(`${hookName} callNative failed silently:`,err);}}},[hookName,isNative]);// Operation wrapper that handles common patterns
22
22
  const executeOperation=(0,_react.useCallback)((operationCallback,operationName="operation")=>{try{if(isWeb()){console.warn(`${hookName} requires web fallback implementation (isWeb: true)`);return;}if(!isNative()){console.error(`${hookName} executeOperation: Native bridge not available`);return;}setLoading(true);setError(null);startProgress("starting",`Starting ${operationName}...`);if((0,_errors.isDevelopment)()){console.log(`🚀 ${hookName} ${operationName} started`);}// Execute the actual operation
23
- operationCallback();}catch(err){handleNativeError(err);console.error(`❌ ${hookName} ${operationName} failed:`,err);}},[hookName,isWeb,startProgress,handleNativeError]);// Environment flags (computed values, not functions)
23
+ operationCallback();}catch(err){handleNativeError(err);console.error(`❌ ${hookName} ${operationName} failed:`,err);}},[hookName,isWeb,isNative,startProgress,handleNativeError]);// Environment flags (computed values, not functions)
24
24
  const environmentFlags={isWeb:isWeb(),isNative:isNative()};// Return standardized interface
25
25
  return{// Data state
26
26
  data,// Loading states
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-extra-semi */"use strict";var _renameAndroidProject=require("./renameAndroidProject.js");var _child_process=require("child_process");var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _utils=require("./utils.js");var _TerminalProgress=_interopRequireDefault(require("./TerminalProgress.js"));// Import the AAB builder
1
+ /* eslint-disable no-extra-semi */"use strict";var _renameAndroidProject=require("./renameAndroidProject.js");var _child_process=require("child_process");var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _utils=require("./utils.js");var _TerminalProgress=_interopRequireDefault(require("./TerminalProgress.js"));var _pluginComposerAndroid=require("./pluginComposerAndroid.js");var _internalPluginUtils=require("./internalPluginUtils.js");// Import the AAB builder
2
2
  function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const configPath=`${process.env.PWD}/config/config.json`;const publicPath=`${process.env.PWD}/public`;const catalystCorePath=_path.default.dirname(require.resolve("catalyst-core/package.json"));const pwd=_path.default.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";// Default values for AAB building
3
3
  const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";// Use actual project name in catalyst
4
4
  const DEFAULT_OVERWRITE_EXISTING=true;const steps={config:"Initialize Configuration",tools:"Validate Android Tools",emulator:"Check and Start Emulator",copyAssets:"Copy Build Assets",build:"Build and Install Application",aab:"Build Signed AAB"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new _TerminalProgress.default(steps,"Catalyst Android Build",progressConfig);async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){throw new Error("WebView Config missing in "+configPath);}if(!WEBVIEW_CONFIG.android){throw new Error("Android config missing in WebView Config");}// Log build type information
@@ -131,7 +131,7 @@ const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physica
131
131
  targetDevice={type:"physical",id:physicalDevice.id,model:physicalDevice.model};progress.log(`Using physical device: ${physicalDevice.model}`,"success");}else{// Physical device failed, fallback to emulator
132
132
  progress.log("Physical device installation test failed, falling back to emulator","warning");targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}}else{// No physical device, use emulator (current behavior)
133
133
  targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}progress.complete("emulator");}else{progress.log("Skipping device setup for release build","info");}// Copy build assets
134
- progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);await processNotifications(WEBVIEW_CONFIG);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");// Build based on type
134
+ progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=(0,_internalPluginUtils.resolvePluginConfig)(WEBVIEW_CONFIG);(0,_pluginComposerAndroid.composeAndroidPlugins)({corePluginsRoot:(0,_internalPluginUtils.resolveInternalPluginsRoot)(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(WEBVIEW_CONFIG);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");// Build based on type
135
135
  let movedApkPath=null;if(buildType==="release"){// Build signed AAB for release
136
136
  progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");// Move APK to output directory
137
137
  movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}else{// Install debug app for development