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.
- package/README.md +4 -4
- package/bin/catalyst.js +8 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +12 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +18 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +5 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +240 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +121 -0
- package/dist/native/bridge/useBaseHook.js +1 -1
- package/dist/native/buildAppAndroid.js +2 -2
- package/dist/native/buildAppIos.js +10 -17
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
- package/dist/native/internalPluginUtils.js +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +13 -2
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +6 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +9 -0
- package/dist/native/pluginComposerIos.js +7 -0
- package/dist/scripts/plugins.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
## Table of Contents
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
-
|
|
24
|
-
-
|
|
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
|
|
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 =
|
|
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
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -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()
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -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 (::
|
|
449
|
-
|
|
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,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
|