catalyst-core 0.1.0-canary.9 → 0.2.0-beta.2

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 (148) hide show
  1. package/README.md +1 -1
  2. package/bin/catalyst.js +8 -1
  3. package/changelog.md +6 -0
  4. package/dist/native/androidProject/app/build.gradle.kts +34 -8
  5. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
  27. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
  28. package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
  29. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
  30. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
  31. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  32. package/dist/native/androidSetup.js +1 -0
  33. package/dist/native/bridge/WebBridge.js +5 -5
  34. package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
  35. package/dist/native/bridge/hooks/index.js +1 -0
  36. package/dist/native/bridge/hooks/useCamera.js +9 -0
  37. package/dist/native/bridge/hooks/useDataProtection.js +2 -0
  38. package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
  39. package/dist/native/bridge/hooks/useFilePicker.js +2 -0
  40. package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
  41. package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
  42. package/dist/native/bridge/hooks/useIntent.js +1 -0
  43. package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
  44. package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
  45. package/dist/native/bridge/hooks/useNotification.js +2 -0
  46. package/dist/native/bridge/hooks/useSafeArea.js +2 -0
  47. package/dist/native/bridge/hooks/useVideoStream.js +6 -0
  48. package/dist/native/bridge/hooks/utils.js +1 -0
  49. package/dist/native/bridge/hooks.js +1 -137
  50. package/dist/native/bridge/useBaseHook.js +12 -7
  51. package/dist/native/bridge/utils/NativeBridge.js +41 -6
  52. package/dist/native/buildAndroid/assets.js +52 -0
  53. package/dist/native/buildAndroid/build.js +19 -0
  54. package/dist/native/buildAndroid/config.js +1 -0
  55. package/dist/native/buildAndroid/index.js +1 -0
  56. package/dist/native/buildAppAndroid.js +2 -150
  57. package/dist/native/buildAppIos.js +2 -312
  58. package/dist/native/buildIos/assets.js +8 -0
  59. package/dist/native/buildIos/build.js +40 -0
  60. package/dist/native/buildIos/config.js +71 -0
  61. package/dist/native/buildIos/index.js +6 -0
  62. package/dist/native/buildIos/plugins.js +45 -0
  63. package/dist/native/internalPluginUtils.js +7 -0
  64. package/dist/native/iosnativeWebView/Package.resolved +36 -0
  65. package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
  66. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
  67. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
  68. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
  69. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
  70. package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
  71. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
  72. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
  73. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
  74. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
  75. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
  76. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  77. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  78. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  89. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
  90. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
  91. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
  92. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
  93. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
  94. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
  95. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
  96. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  97. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  98. package/dist/native/pluginComposerAndroid.js +28 -0
  99. package/dist/native/pluginComposerIos.js +16 -0
  100. package/dist/native/renameAndroidProject.js +5 -5
  101. package/dist/native/setupEmulatorIos.js +6 -4
  102. package/dist/native/setupServer.js +2 -3
  103. package/dist/native/utils.js +4 -1
  104. package/dist/offline/catalyst-sw.js +1 -0
  105. package/dist/offline/registerServiceWorker.js +1 -0
  106. package/dist/otel.js +70 -2
  107. package/dist/router/ClientRouter.js +1 -1
  108. package/dist/scripts/build.js +1 -1
  109. package/dist/scripts/cleanCache.js +1 -0
  110. package/dist/scripts/devBuild.js +1 -1
  111. package/dist/scripts/devServe.js +1 -1
  112. package/dist/scripts/generateOfflineManifest.js +1 -0
  113. package/dist/scripts/loadEnvironmentVariables.js +1 -2
  114. package/dist/scripts/plugins.js +1 -0
  115. package/dist/scripts/registerAliases.js +6 -2
  116. package/dist/scripts/scriptUtils.js +2 -1
  117. package/dist/scripts/serve.js +2 -1
  118. package/dist/scripts/start.js +7 -2
  119. package/dist/server/expressServer.js +4 -8
  120. package/dist/server/renderer/document/Body.js +4 -4
  121. package/dist/server/renderer/document/Head.js +2 -2
  122. package/dist/server/renderer/extract.js +31 -27
  123. package/dist/server/renderer/handler.js +47 -24
  124. package/dist/server/renderer/index.js +1 -1
  125. package/dist/server/startServer.js +57 -7
  126. package/dist/server/utils/userAgentUtil.js +9 -3
  127. package/dist/server/utils/validator.js +1 -1
  128. package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
  129. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
  130. package/dist/webpack/babel.config.client.js +1 -1
  131. package/dist/webpack/babel.config.ssr.js +1 -1
  132. package/dist/webpack/base.babel.js +7 -8
  133. package/dist/webpack/development.client.babel.js +7 -16
  134. package/dist/webpack/production.client.babel.js +1 -1
  135. package/dist/webpack/production.ssr.babel.js +5 -1
  136. package/mcp_v2/knowledge-base.json +206 -100
  137. package/mcp_v2/lib/helpers.js +42 -8
  138. package/mcp_v2/lib/seed.js +88 -0
  139. package/mcp_v2/mcp.js +150 -4
  140. package/mcp_v2/setup.js +30 -193
  141. package/mcp_v2/tools/build.js +1 -1
  142. package/mcp_v2/tools/config.js +16 -4
  143. package/mcp_v2/tools/conversion.js +11 -11
  144. package/mcp_v2/tools/github.js +1117 -0
  145. package/mcp_v2/tools/knowledge.js +16 -3
  146. package/mcp_v2/tools/sync.js +179 -10
  147. package/mcp_v2/tools/tasks.js +13 -4
  148. package/package.json +20 -8
@@ -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
+ }
@@ -5,7 +5,8 @@ import io.yourname.androidproject.utils.BridgeUtils
5
5
  import java.io.BufferedReader
6
6
  import java.io.File
7
7
  import java.io.InputStreamReader
8
- import java.net.Socket
8
+ import java.net.InetSocketAddress
9
+ import java.nio.channels.SocketChannel
9
10
 
10
11
  /**
11
12
  * Local Frida and hooking framework detection.
@@ -68,10 +69,20 @@ object FridaDetector {
68
69
  private fun checkFridaPorts(): Boolean {
69
70
  return FRIDA_PORTS.any { port ->
70
71
  try {
71
- Socket().use { socket ->
72
- val address = java.net.InetSocketAddress("127.0.0.1", port)
73
- socket.connect(address, 2000) // 2 second timeout
74
- socket.isConnected
72
+ SocketChannel.open().use { channel ->
73
+ val address = InetSocketAddress("127.0.0.1", port)
74
+ val deadline = System.currentTimeMillis() + 2000
75
+ channel.configureBlocking(false)
76
+ channel.connect(address)
77
+
78
+ while (!channel.finishConnect()) {
79
+ if (System.currentTimeMillis() >= deadline) {
80
+ return@use false
81
+ }
82
+ Thread.sleep(25)
83
+ }
84
+
85
+ true
75
86
  }
76
87
  } catch (e: Exception) {
77
88
  // Connection failed or timed out - no Frida on this port
@@ -76,12 +76,25 @@ object BridgeUtils {
76
76
  // Safe area events
77
77
  ON_SAFE_AREA_INSETS_UPDATED("ON_SAFE_AREA_INSETS_UPDATED"),
78
78
 
79
+ // Video stream events
80
+ ON_VIDEO_STREAM_READY("ON_VIDEO_STREAM_READY"),
81
+ ON_VIDEO_STREAM_STOPPED("ON_VIDEO_STREAM_STOPPED"),
82
+ ON_QR_DETECTED("ON_QR_DETECTED"),
83
+ ON_TORCH_CHANGED("ON_TORCH_CHANGED"),
84
+ ON_ZOOM_CHANGED("ON_ZOOM_CHANGED"),
85
+
79
86
  // Security events
80
87
  ON_SCREEN_SECURE_SET("ON_SCREEN_SECURE_SET"),
81
88
  ON_SCREEN_SECURE_STATUS("ON_SCREEN_SECURE_STATUS"),
82
89
  ON_SCREEN_SECURE_ERROR("ON_SCREEN_SECURE_ERROR"),
83
90
  ON_WEB_DATA_CLEARED("ON_WEB_DATA_CLEARED"),
84
91
  ON_WEB_DATA_CLEAR_ERROR("ON_WEB_DATA_CLEAR_ERROR"),
92
+
93
+ // Transition events
94
+ ON_TRANSITION_COMMITTED("ON_TRANSITION_COMMITTED"),
95
+ ON_TRANSITION_CANCELLED("ON_TRANSITION_CANCELLED"),
96
+ ON_TRANSITION_TIMEOUT("ON_TRANSITION_TIMEOUT"),
97
+
85
98
  }
86
99
 
87
100
  /**
@@ -99,13 +112,25 @@ object BridgeUtils {
99
112
  }
100
113
 
101
114
  try {
102
- val safeData = data?.replace("'", "\\'") ?: ""
103
115
  val jsCode = if (data != null) {
104
- "window.WebBridge.callback('${event.eventName}', '$safeData')"
116
+ // If data looks like a JSON object or array, inject it raw (with U+2028/U+2029
117
+ // escaping) so the JS side receives a live value rather than a double-encoded string.
118
+ // Otherwise wrap with JSONObject.quote() to produce a safe JS string literal.
119
+ val trimmed = data.trimStart()
120
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
121
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
122
+ val safeData = data
123
+ .replace("
", "\\u2028")
124
+ .replace("
", "\\u2029")
125
+ "window.WebBridge.callback('${event.eventName}', $safeData)"
126
+ } else {
127
+ val quotedData = org.json.JSONObject.quote(data)
128
+ "window.WebBridge.callback('${event.eventName}', $quotedData)"
129
+ }
105
130
  } else {
106
131
  "window.WebBridge.callback('${event.eventName}', null)"
107
132
  }
108
-
133
+
109
134
  webView.evaluateJavascript(jsCode, null)
110
135
  Log.d(TAG, "✅ Web notification sent: ${event.eventName}")
111
136
  } catch (e: Exception) {
@@ -146,9 +171,21 @@ object BridgeUtils {
146
171
  * @param jsonData The JSON object to send
147
172
  */
148
173
  fun notifyWebJson(webView: WebView, event: WebEvents, jsonData: JSONObject) {
174
+ // Ensure WebView interactions happen on the main thread
175
+ if (Looper.myLooper() != Looper.getMainLooper()) {
176
+ webView.post { notifyWebJson(webView, event, jsonData) }
177
+ return
178
+ }
179
+
149
180
  try {
150
- val jsonString = jsonData.toString().replace("'", "\\'")
151
- notifyWeb(webView, event, jsonString)
181
+ // Inject JSON directly into the JS call. U+2028/U+2029 are valid JSON whitespace but
182
+ // are treated as line terminators by V8, causing a SyntaxError — escape them first.
183
+ val safeJson = jsonData.toString()
184
+ .replace("
", "\\u2028")
185
+ .replace("
", "\\u2029")
186
+ val jsCode = "window.WebBridge.callback('${event.eventName}', $safeJson)"
187
+ webView.evaluateJavascript(jsCode, null)
188
+ Log.d(TAG, "✅ Web JSON notification sent: ${event.eventName}")
152
189
  } catch (e: Exception) {
153
190
  Log.e(TAG, "❌ Failed to send JSON to web: ${e.message}")
154
191
  notifyWebError(webView, event, "Error processing data: ${e.message}")
@@ -12,7 +12,8 @@ import io.ktor.server.routing.*
12
12
  import kotlinx.coroutines.*
13
13
  import java.io.File
14
14
  import java.io.IOException
15
- import java.net.ServerSocket
15
+ import java.net.InetSocketAddress
16
+ import java.nio.channels.ServerSocketChannel
16
17
  import java.security.SecureRandom
17
18
  import java.util.*
18
19
  import java.util.concurrent.ConcurrentHashMap
@@ -279,7 +280,10 @@ object FrameworkServerUtils {
279
280
 
280
281
  private fun isPortAvailable(port: Int): Boolean {
281
282
  return try {
282
- ServerSocket(port).use { true }
283
+ ServerSocketChannel.open().use { channel ->
284
+ channel.bind(InetSocketAddress("127.0.0.1", port))
285
+ true
286
+ }
283
287
  } catch (e: IOException) {
284
288
  false
285
289
  }
@@ -408,4 +412,4 @@ object FrameworkServerUtils {
408
412
  }
409
413
  }
410
414
  }
411
- }
415
+ }
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <stroke
5
+ android:width="3dp"
6
+ android:color="#FF0000" />
7
+ <solid android:color="#33FF0000" />
8
+ </shape>
@@ -13,6 +13,12 @@
13
13
  app:layout_constraintStart_toStartOf="parent"
14
14
  app:layout_constraintEnd_toEndOf="parent">
15
15
 
16
+ <androidx.camera.view.PreviewView
17
+ android:id="@+id/camera_preview"
18
+ android:layout_width="match_parent"
19
+ android:layout_height="match_parent"
20
+ android:visibility="invisible" />
21
+
16
22
  <WebView
17
23
  android:id="@+id/webview"
18
24
  android:layout_width="match_parent"
@@ -24,6 +30,37 @@
24
30
  android:layout_width="wrap_content"
25
31
  android:layout_height="wrap_content"
26
32
  android:visibility="gone" />
33
+
34
+ <RelativeLayout
35
+ android:id="@+id/debug_viewfinder_overlay"
36
+ android:layout_width="0dp"
37
+ android:layout_height="0dp"
38
+ android:background="@null"
39
+ android:visibility="gone">
40
+
41
+ <TextView
42
+ android:id="@+id/debug_qr_status"
43
+ android:layout_width="wrap_content"
44
+ android:layout_height="wrap_content"
45
+ android:layout_alignParentBottom="true"
46
+ android:layout_centerHorizontal="true"
47
+ android:layout_marginBottom="8dp"
48
+ android:paddingHorizontal="10dp"
49
+ android:paddingVertical="4dp"
50
+ android:background="#CC000000"
51
+ android:textColor="#FFFFFF"
52
+ android:textSize="12sp"
53
+ android:text="⬤ –" />
54
+
55
+ </RelativeLayout>
56
+
57
+ <!-- Debug: shows where native thinks the barcode box is in screen space -->
58
+ <View
59
+ android:id="@+id/debug_barcode_overlay"
60
+ android:layout_width="0dp"
61
+ android:layout_height="0dp"
62
+ android:background="#5500FF00"
63
+ android:visibility="gone" />
27
64
  </RelativeLayout>
28
65
 
29
66
  </androidx.constraintlayout.widget.ConstraintLayout>
@@ -85,7 +85,7 @@ class CustomWebviewTest {
85
85
  setProperty("accessControl.allowedUrls", "https://example.com,https://api.example.com")
86
86
  }
87
87
 
88
- val enabled = properties.getProperty("accessControl.enabled", "true").equals("true", ignoreCase = true)
88
+ val enabled = properties.getProperty("accessControl.enabled", "false").equals("true", ignoreCase = true)
89
89
  assertTrue("Access control should be enabled", enabled)
90
90
 
91
91
  val allowedUrls = properties.getProperty("accessControl.allowedUrls", "")
@@ -101,8 +101,13 @@ class CustomWebviewTest {
101
101
  val disabledProps = Properties().apply {
102
102
  setProperty("accessControl.enabled", "false")
103
103
  }
104
- val disabled = disabledProps.getProperty("accessControl.enabled", "true").equals("true", ignoreCase = true)
104
+ val disabled = disabledProps.getProperty("accessControl.enabled", "false").equals("true", ignoreCase = true)
105
105
  assertFalse("Access control should be disabled", disabled)
106
+
107
+ // Test missing config defaults to disabled
108
+ val missingProps = Properties()
109
+ val missing = missingProps.getProperty("accessControl.enabled", "false").equals("true", ignoreCase = true)
110
+ assertFalse("Missing access control config should default to disabled", missing)
106
111
  }
107
112
 
108
113
  @Test
@@ -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
+ }
@@ -4,5 +4,6 @@ if(!_fs.default.existsSync(javaHome)){throw new Error(`Java installation not fou
4
4
  const homeDir=process.env.HOME;const shellProfile=`${homeDir}/.zshrc`;const exportCommand=`\n# Java Configuration\nexport JAVA_HOME=${javaHome}\nexport PATH=$JAVA_HOME/bin:$PATH\n`;try{_fs.default.appendFileSync(shellProfile,exportCommand);}catch(error){throw new Error(`Failed to update ${shellProfile}: ${error.message}. Please add the following lines manually:\n${exportCommand}`);}// Set for current process
5
5
  process.env.JAVA_HOME=javaHome;process.env.PATH=`${javaHome}/bin:${process.env.PATH}`;// Verify Java configuration
6
6
  try{const javaVersion=(0,_utils.runCommand)("java -version");progress.log("Java installed and configured successfully","success");progress.log(`Java Version: ${javaVersion}`,"info");progress.log(`IMPORTANT: Please run the following commands to complete setup:`,"warning");progress.log(`1. source ${shellProfile}`,"info");progress.log(`2. echo $JAVA_HOME`,"info");return true;}catch(error){throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);}}catch(error){progress.log(error.message,"error");progress.log("Manual Setup Instructions:","info");progress.log("1. Run: brew reinstall --cask zulu@17","info");progress.log("2. Add to ~/.zshrc:","info");progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home","info");progress.log(" export PATH=$JAVA_HOME/bin:$PATH","info");progress.log("3. Run: source ~/.zshrc","info");throw error;}}async function validateJavaEnvironment(){progress.start("java");const hasJava=await checkJavaInstallation();if(!hasJava){progress.log("Java installation required. Starting installation process...");const javaInstalled=await installJava();if(!javaInstalled){progress.fail("java","Failed to set up Java environment");throw new Error("Failed to set up Java environment");}}progress.complete("java");}async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){progress.log("WebView Config missing in "+configPath,"error");process.exit(1);}if(!WEBVIEW_CONFIG.android){WEBVIEW_CONFIG.android={};}return{WEBVIEW_CONFIG};}async function saveConfig(newConfig){try{const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const existingConfig=JSON.parse(existingConfigFile);const updatedConfig={...existingConfig,WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,android:newConfig.WEBVIEW_CONFIG.android}};_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB is valid","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator is valid","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}progress.log("Android tools validation completed successfully!","success");return{EMULATOR_PATH,ADB_PATH};}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log(`Error checking emulator: ${error}`,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);// eslint-disable-next-line security/detect-child-process
7
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - EMULATOR_PATH is derived from SDK configuration and emulatorName is local native setup config.
7
8
  (0,_child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`,error=>{if(error){progress.log(`Error starting emulator: ${error}`,"error");}});}async function updateLocalProperties(sdkPath){progress.start("properties");try{(0,_utils.runCommand)(`cd ${pwd}/androidProject && ./gradlew updateSdkPath -PsdkPath="${sdkPath}"`);progress.log("Updated local.properties successfully","success");progress.complete("properties");}catch(error){progress.fail("properties",`Failed to update local.properties: ${error.message}`);throw error;}}async function setupAndroidEnvironment(){try{await validateJavaEnvironment();progress.start("config");await initializeConfig();const config=await(0,_utils.validateAndCompleteConfig)("android",configPath);progress.complete("config");progress.start("androidTools");const{EMULATOR_PATH,ADB_PATH}=validateAndroidTools(config.android);progress.complete("androidTools");progress.start("emulator");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...");await startEmulator(EMULATOR_PATH,config.android);}else{progress.log("Emulator already running, proceeding with installation...","success");}progress.complete("emulator");if(config.android?.sdkPath){await updateLocalProperties(config.android.sdkPath);}progress.start("saveConfig");await saveConfig({WEBVIEW_CONFIG:config});progress.complete("saveConfig");progress.start("setupServer");await(0,_setupServer.setupServer)(configPath);progress.complete("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"android: Android-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"sdkPath: Android SDK path",indent:2,prefix:"├─ ",color:"gray"},{text:"emulatorName: Selected Android emulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}// Execute the main setup
8
9
  setupAndroidEnvironment();