catalyst-core-internal 0.1.0 → 0.1.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 (29) hide show
  1. package/README.md +4 -4
  2. package/bin/catalyst.js +8 -1
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +12 -1
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +18 -3
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +5 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +240 -0
  9. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +121 -0
  10. package/dist/native/bridge/useBaseHook.js +1 -1
  11. package/dist/native/buildAppAndroid.js +2 -2
  12. package/dist/native/buildAppIos.js +10 -17
  13. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  14. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  15. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  16. package/dist/native/internalPluginUtils.js +1 -0
  17. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  18. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  19. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  20. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +13 -2
  21. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  22. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +6 -0
  23. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -0
  24. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  25. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  26. package/dist/native/pluginComposerAndroid.js +9 -0
  27. package/dist/native/pluginComposerIos.js +7 -0
  28. package/dist/scripts/plugins.js +1 -0
  29. package/package.json +3 -2
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)
@@ -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,5 @@
1
+ package io.yourname.androidproject.plugins
2
+
3
+ interface CatalystPlugin {
4
+ fun handle(command: String, data: Any?, bridge: PluginBridgeContext)
5
+ }
@@ -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,240 @@
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: Any?,
18
+ val requestId: String?
19
+ )
20
+
21
+ private class PluginBridgeRuntimeError(
22
+ override val message: String,
23
+ val code: String,
24
+ cause: Throwable? = null
25
+ ) : Exception(message, 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
+ internal fun parseRequest(payload: String?): PluginRequest {
70
+ if (payload.isNullOrBlank()) {
71
+ throw IllegalArgumentException("Payload is required")
72
+ }
73
+ val messageSize = payload.toByteArray(Charsets.UTF_8).size
74
+ if (messageSize > CatalystConstants.Bridge.MAX_MESSAGE_SIZE) {
75
+ throw IllegalArgumentException("Payload exceeds maximum size")
76
+ }
77
+
78
+ val body = JSONObject(payload)
79
+ return PluginRequest(
80
+ pluginId = readRequiredString(body, "pluginId"),
81
+ command = readRequiredString(body, "command"),
82
+ data = if (body.has("data") && !body.isNull("data")) body.get("data") else null,
83
+ requestId = readOptionalString(body, "requestId")
84
+ )
85
+ }
86
+ }
87
+
88
+ private val pluginIdToClassName = GeneratedPluginIndex.pluginIdToClassName
89
+ private val pluginToCommands = GeneratedPluginIndex.pluginToCommands
90
+
91
+ @JavascriptInterface
92
+ fun emit(payload: String?) {
93
+ var request: PluginRequest? = null
94
+
95
+ try {
96
+ request = parseRequest(payload)
97
+
98
+ if (request.pluginId.isEmpty()) {
99
+ sendBridgeError("pluginId is required", ERROR_CODE_INVALID_PAYLOAD, request)
100
+ return
101
+ }
102
+ if (request.command.isEmpty()) {
103
+ sendBridgeError("command is required", ERROR_CODE_INVALID_PAYLOAD, request)
104
+ return
105
+ }
106
+
107
+ if (!hasPlugin(request.pluginId)) {
108
+ sendBridgeError("Unsupported plugin: ${request.pluginId}", ERROR_CODE_PLUGIN_NOT_FOUND, request)
109
+ return
110
+ }
111
+
112
+ if (!hasCommand(request.pluginId, request.command)) {
113
+ sendBridgeError(
114
+ "Unsupported command '${request.command}' for plugin '${request.pluginId}'",
115
+ ERROR_CODE_COMMAND_NOT_SUPPORTED,
116
+ request
117
+ )
118
+ return
119
+ }
120
+
121
+ val plugin = try {
122
+ getPluginForId(request.pluginId)
123
+ } catch (error: PluginBridgeRuntimeError) {
124
+ sendBridgeError(error.message, error.code, request)
125
+ return
126
+ }
127
+
128
+ val callbackContext = PluginBridgeContext(
129
+ activity = activity,
130
+ webView = webView,
131
+ properties = properties,
132
+ pluginId = request.pluginId,
133
+ command = request.command,
134
+ requestId = request.requestId
135
+ )
136
+
137
+ plugin.handle(request.command, request.data, callbackContext)
138
+ } catch (error: IllegalArgumentException) {
139
+ sendBridgeError(error.message ?: "Invalid payload", ERROR_CODE_INVALID_PAYLOAD, request)
140
+ } catch (error: JSONException) {
141
+ sendBridgeError("Invalid JSON payload: ${error.message}", ERROR_CODE_INVALID_PAYLOAD, request)
142
+ } catch (error: Exception) {
143
+ Log.e(TAG, "Plugin command failed for ${request?.pluginId ?: "<unknown>"}.${request?.command ?: "<unknown>"}", error)
144
+ sendBridgeError("Plugin execution failed: ${error.message}", ERROR_CODE_PLUGIN_EXECUTION_FAILED, request)
145
+ }
146
+ }
147
+
148
+ private fun sendBridgeError(message: String, code: String, request: PluginRequest?) {
149
+ PluginBridgeContext(
150
+ activity = activity,
151
+ webView = webView,
152
+ properties = properties,
153
+ pluginId = SYSTEM_PLUGIN_ID,
154
+ command = request?.command,
155
+ requestId = request?.requestId
156
+ ).callback(
157
+ ERROR_EVENT,
158
+ JSONObject().apply {
159
+ put("message", message)
160
+ put("code", code)
161
+ put("pluginId", request?.pluginId ?: SYSTEM_PLUGIN_ID)
162
+ request?.command?.takeIf { it.isNotEmpty() }?.let { put("command", it) }
163
+ }
164
+ )
165
+ }
166
+
167
+ private fun hasPlugin(pluginId: String): Boolean {
168
+ return pluginIdToClassName.containsKey(pluginId)
169
+ }
170
+
171
+ private fun hasCommand(pluginId: String, command: String): Boolean {
172
+ return pluginToCommands[pluginId]?.contains(command) ?: false
173
+ }
174
+
175
+ private fun getPluginForId(pluginId: String): CatalystPlugin {
176
+ val className = pluginIdToClassName[pluginId]
177
+ ?: throw PluginBridgeRuntimeError(
178
+ "No plugin registered for id: $pluginId",
179
+ ERROR_CODE_PLUGIN_NOT_REGISTERED
180
+ )
181
+
182
+ return try {
183
+ val clazz = Class.forName(className)
184
+ val instance = clazz.getDeclaredConstructor().newInstance()
185
+ instance as? CatalystPlugin
186
+ ?: throw IllegalStateException("Plugin class '$className' must implement CatalystPlugin")
187
+ } catch (error: Exception) {
188
+ Log.e(TAG, "Failed to instantiate plugin class $className for plugin $pluginId", error)
189
+ throw PluginBridgeRuntimeError(
190
+ "Failed to instantiate plugin class '$className' for plugin '$pluginId': ${error.message ?: error.javaClass.simpleName}",
191
+ ERROR_CODE_PLUGIN_INSTANTIATION_FAILED,
192
+ error
193
+ )
194
+ }
195
+ }
196
+ }
197
+
198
+ class PluginBridgeContext(
199
+ val activity: Activity,
200
+ val webView: WebView,
201
+ val properties: Properties,
202
+ val pluginId: String,
203
+ val command: String?,
204
+ val requestId: String?
205
+ ) {
206
+ val context: Context
207
+ get() = activity
208
+
209
+ fun callback(
210
+ eventName: String,
211
+ data: Any?,
212
+ command: String? = this.command
213
+ ) {
214
+ require(eventName.isNotBlank()) { "Callback eventName is required" }
215
+
216
+ val pluginLiteral = JSONObject.quote(pluginId)
217
+ val eventLiteral = JSONObject.quote(eventName)
218
+ val dataLiteral = toJavaScriptLiteral(data)
219
+ val requestLiteral = requestId?.let(JSONObject::quote) ?: "null"
220
+ val commandLiteral = command?.takeIf { it.isNotBlank() }?.let(JSONObject::quote) ?: "null"
221
+
222
+ webView.post {
223
+ webView.evaluateJavascript(
224
+ "window.PluginBridgeWeb && window.PluginBridgeWeb.callback($pluginLiteral, $eventLiteral, $dataLiteral, $requestLiteral, $commandLiteral);",
225
+ null
226
+ )
227
+ }
228
+ }
229
+
230
+ private fun toJavaScriptLiteral(value: Any?): String {
231
+ return when (value) {
232
+ null -> "null"
233
+ is JSONObject -> value.toString()
234
+ is JSONArray -> value.toString()
235
+ is Number, is Boolean -> value.toString()
236
+ is String -> JSONObject.quote(value)
237
+ else -> JSONObject.wrap(value)?.toString() ?: "null"
238
+ }
239
+ }
240
+ }
@@ -0,0 +1,121 @@
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
+ }
@@ -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-internal/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